├── .gitignore ├── LICENSE ├── README.md ├── bsconfig.json ├── package-lock.json ├── package.json ├── src ├── vow.re └── vow.rei └── tests └── soundness_tests.re /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | lib 4 | .merlin 5 | .bsb.lock -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Wojciech Czekalski 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vow 2 | 3 | `Vow` is a tiny library which allows you to handle promises more safely in your Bucklescript application. 4 | 5 | A `Vow` can be either `handled` and `unhandled`. All promises of type `vow 'a handled` make sure that you handled Promise rejections. Thanks to that you will avoid the Uncaught promise error. 6 | 7 | ## Installation 8 | 9 | ```sh 10 | npm install --save @wokalski/vow 11 | ``` 12 | 13 | Then add `vow` to `bs-dependencies` in your `bsconfig.json`: 14 | ```js 15 | { 16 | ... 17 | "bs-dependencies": ["@wokalski/vow"] 18 | } 19 | ``` 20 | 21 | ## Side effects 22 | 23 | After series of operations you usually want to "consume" a promise. `Vow.sideEffect` should be used for that. 24 | 25 | It only accepts promises which are properly handled. 26 | 27 | ## Unwrapping 28 | 29 | You can unwrap a handled promise using `Vow.unwrap`. 30 | 31 | ## Nesting vows 32 | 33 | `Js.Promise.t` is unsafe when you nest promises. i.e. `Js.Promise.t (Js.Promise.t 'a)` is unsound. In the runtime it's `Js.Promise.t`. 34 | 35 | This is resolved with `vow`s. If you nest `vow`s they behave as expected. 36 | 37 | However if you put a `Js.Promise.t` inside a `vow` (which are boxed `Js.Promise.t` under the scenes) you're gonna get a `vow` of the following type: 38 | 39 | ```reason 40 | /* in Reason syntax */ 41 | 42 | vow (Js.Promise.t 'a) 'status 43 | ``` 44 | However, under the scenes it'll really be 45 | 46 | ```reason 47 | 48 | vow 'a 'status 49 | ``` 50 | 51 | Therefore `vow` is not sound. 52 | 53 | ## Binding 54 | 55 | In order to use vows you have to bind to your existing APIs using `Vow.wrap`/`Vow.unsafeWrap`. 56 | 57 | If you `unsafeWrap` a promise which does throw your code will be unsound. 58 | 59 | ## Example 60 | 61 | Let's see a real world example of vows with some comments: 62 | 63 | ```reason 64 | let login _: Vow.Result.t authenticationState error Vow.handled => 65 | /* Returns a handled Vow.Result.t */ 66 | Login.logIn () |> 67 | /* Validates the returned value. Since the vow is handled we don't need to catch*/ 68 | Vow.Result.flatMap ( 69 | fun x => 70 | if x##isCancelled { 71 | Vow.Result.fail LoginRequestCancelled 72 | } else { 73 | Vow.Result.return () 74 | } 75 | ) |> 76 | /* Another handled Vow.Result.t */ 77 | Vow.Result.flatMap Login.getCurrentAccessToken () |> 78 | Vow.Result.map ( 79 | fun x => { 80 | let token = x##accessToken; 81 | /* This returns an unhandled Vow.Result.t. 82 | * Note that the 'error types have to match 83 | * Because after one error the subsequent operations 84 | * Are not performed. 85 | */ 86 | Queries.login ::token 87 | } 88 | ) |> 89 | /* Ooops, the `Queries.login` might reject. 90 | * We are forced to handle it in the compile time. 91 | */ 92 | Vow.Result.onError (fun _ => Vow.Result.fail GraphQlSignInError) |> 93 | Vow.Result.flatMap ( 94 | fun x => 95 | switch x { 96 | | Authenticated {token, userId} => 97 | /* The promise we wrap is never rejected */ 98 | Vow.unsafeWrap 99 | KeyChain.( 100 | Js.Promise.all2 ( 101 | setGenericPassword username::"userId" password::userId service::"userId", 102 | setGenericPassword username::"token" password::token service::"token" 103 | ) 104 | ) |> 105 | Vow.map (fun _ => Vow.Result.return x) 106 | | _ => Vow.Result.return x 107 | } 108 | ); 109 | ``` 110 | 111 | ## Author 112 | 113 | [@wokalski](http://twitter.com/wokalski) 114 | 115 | -------------------------------------------------------------------------------- /bsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "refmt": 3, 3 | "name": "@wokalski/vow", 4 | "bsc-flags": [ 5 | "-bs-super-errors" 6 | ], 7 | "sources": [ 8 | "src", 9 | { 10 | "dir": "tests", 11 | "type": "dev" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wokalski/vow", 3 | "version": "0.1.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "bs-platform": { 8 | "version": "4.0.3", 9 | "resolved": "https://registry.npmjs.org/bs-platform/-/bs-platform-4.0.3.tgz", 10 | "integrity": "sha1-RRDByRXMWxabVxflwK2lLT+Z/5g=", 11 | "dev": true 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wokalski/vow", 3 | "description": "Almost sound Promises for Bucklescript", 4 | "keywords": [ 5 | "reason", 6 | "bucklescript", 7 | "promise" 8 | ], 9 | "scripts": { 10 | "build": "bsb -make-world", 11 | "clean": "bsb -clean-world", 12 | "watch": "bsb -make-world -w" 13 | }, 14 | "repository": "wokalski/vow", 15 | "homepage": "https://github.com/wokalski/vow#readme", 16 | "bugs": "https://github.com/wokalski/vow/issues", 17 | "license": "MIT", 18 | "author": { 19 | "name": "Wojtek Czekalski", 20 | "email": "me@wczekalski.com", 21 | "url": "http://twitter.com/wokalski" 22 | }, 23 | "version": "0.1.0", 24 | "devDependencies": { 25 | "bs-platform": "^4.0.3" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/vow.re: -------------------------------------------------------------------------------- 1 | module Vow = { 2 | type handled; 3 | type unhandled; 4 | type container('a) = {value: 'a}; 5 | type t('a, 'status) = {promise: Js.Promise.t(container('a))}; 6 | let innerReturn = v => {value: v}; 7 | let return = x => {promise: Js.Promise.resolve(innerReturn(x))}; 8 | let flatMap = (transform, vow) => { 9 | promise: Js.Promise.then_(x => transform(x.value).promise, vow.promise), 10 | }; 11 | let flatMapUnhandled = (transform, vow) => { 12 | promise: 13 | Js.Promise.then_(inn => transform(inn.value).promise, vow.promise), 14 | }; 15 | let map = (transform, vow) => flatMap(x => return(transform(x)), vow); 16 | let mapUnhandled = (transform, vow) => 17 | flatMapUnhandled(x => return(transform(x)), vow); 18 | let sideEffect = (handler, vow) => { 19 | let _ = 20 | Js.Promise.then_( 21 | x => Js.Promise.resolve(handler(x.value)), 22 | vow.promise, 23 | ); 24 | (); 25 | }; 26 | let onError = (handler, vow) => { 27 | promise: Js.Promise.catch(_ => handler().promise, vow.promise), 28 | }; 29 | let wrap = promise => { 30 | promise: 31 | Js.Promise.then_( 32 | res => Js.Promise.resolve(innerReturn(res)), 33 | promise, 34 | ), 35 | }; 36 | let unsafeWrap = promise => { 37 | promise: 38 | Js.Promise.then_( 39 | res => Js.Promise.resolve(innerReturn(res)), 40 | promise, 41 | ), 42 | }; 43 | let unwrap = ({promise}) => 44 | promise 45 | |> Js.Promise.(then_(({value}: container('a)) => value |> resolve)); 46 | let all2 = ((v1, v2)) => 47 | v1 48 | |> flatMap(v1Result => 49 | v2 |> flatMap(v2Result => return((v1Result, v2Result))) 50 | ); 51 | let all3 = ((v1, v2, v3)) => 52 | all2((v1, v2)) 53 | |> flatMap(((v1Result, v2Result)) => 54 | v3 |> flatMap(v3Result => return((v1Result, v2Result, v3Result))) 55 | ); 56 | let all4 = ((v1, v2, v3, v4)) => 57 | all3((v1, v2, v3)) 58 | |> flatMap(((v1Result, v2Result, v3Result)) => 59 | v4 60 | |> flatMap(v4Result => 61 | return((v1Result, v2Result, v3Result, v4Result)) 62 | ) 63 | ); 64 | let rec all = vows => 65 | switch (vows) { 66 | | [head, ...tail] => 67 | all(tail) 68 | |> flatMap(tailResult => 69 | head |> flatMap(headResult => return([headResult, ...tailResult])) 70 | ) 71 | | [] => return([]) 72 | }; 73 | }; 74 | 75 | module type ResultType = { 76 | open Vow; 77 | type vow('a, 'status) = t('a, 'status); 78 | type t('value, 'error, 'status) = 79 | vow(Belt.Result.t('value, 'error), 'status); 80 | let return: 'value => t('value, 'error, handled); 81 | let fail: 'error => t('value, 'error, handled); 82 | let flatMap: 83 | ('a => t('b, 'error, 'status), t('a, 'error, handled)) => 84 | t('b, 'error, 'status); 85 | let flatMapUnhandled: 86 | ('a => t('b, 'error, 'status), t('a, 'error, unhandled)) => 87 | t('b, 'error, unhandled); 88 | let map: ('a => 'b, t('a, 'error, handled)) => t('b, 'error, 'status); 89 | let mapUnhandled: 90 | ('a => 'b, t('a, 'error, unhandled)) => t('b, 'error, unhandled); 91 | let mapError: 92 | ('a => t('value, 'b, handled), t('value, 'a, 'status)) => 93 | t('value, 'b, 'status); 94 | let sideEffect: 95 | (Belt.Result.t('value, 'error) => unit, t('value, 'error, handled)) => 96 | unit; 97 | let onError: 98 | (unit => t('error, 'value, 'status), t('error, 'value, unhandled)) => 99 | t('error, 'value, 'status); 100 | let wrap: 101 | (Js.Promise.t('value), unit => 'error) => t('value, 'error, handled); 102 | let unwrap: 103 | ( 104 | Belt.Result.t('value, 'error) => vow('a, 'status), 105 | t('value, 'error, handled) 106 | ) => 107 | vow('a, 'status); 108 | let all2: 109 | ((t('v1, 'error, handled), t('v2, 'error, handled))) => 110 | t(('v1, 'v2), 'error, handled); 111 | let all3: 112 | ( 113 | ( 114 | t('v1, 'error, handled), 115 | t('v2, 'error, handled), 116 | t('v3, 'error, handled), 117 | ) 118 | ) => 119 | t(('v1, 'v2, 'v3), 'error, handled); 120 | let all4: 121 | ( 122 | ( 123 | t('v1, 'error, handled), 124 | t('v2, 'error, handled), 125 | t('v3, 'error, handled), 126 | t('v4, 'error, handled), 127 | ) 128 | ) => 129 | t(('v1, 'v2, 'v3, 'v4), 'error, handled); 130 | let all: 131 | list(t('value, 'error, handled)) => t(list('value), 'error, handled); 132 | module Infix: { 133 | let (>>=): 134 | (t('a, 'error, handled), 'a => t('b, 'error, 'status)) => 135 | t('b, 'error, 'status'); 136 | let (>|=): (t('a, 'error, handled), 'a => 'b) => t('b, 'error, handled); 137 | }; 138 | }; 139 | 140 | module Result: ResultType = { 141 | type vow('a, 'status) = Vow.t('a, 'status); 142 | type t('value, 'error, 'status) = 143 | vow(Belt.Result.t('value, 'error), 'status); 144 | let return = value => Vow.return(Belt.Result.Ok(value)); 145 | let fail = error => Vow.return(Belt.Result.Error(error)); 146 | let flatMap = (transform, vow) => 147 | Vow.flatMap( 148 | x => 149 | switch (x) { 150 | | Belt.Result.Ok(x) => transform(x) 151 | | Belt.Result.Error(x) => fail(x) 152 | }, 153 | vow, 154 | ); 155 | let flatMapUnhandled = (transform, vow) => 156 | Vow.flatMapUnhandled( 157 | x => 158 | switch (x) { 159 | | Belt.Result.Ok(x) => transform(x) 160 | | Belt.Result.Error(x) => fail(x) 161 | }, 162 | vow, 163 | ); 164 | let map = (transform, vow) => flatMap(x => return(transform(x)), vow); 165 | let mapUnhandled = (transform, vow) => 166 | flatMapUnhandled(x => return(transform(x)), vow); 167 | let mapError = (transform, vow) => 168 | Vow.flatMap( 169 | x => 170 | switch (x) { 171 | | Belt.Result.Ok(x) => return(x) 172 | | Belt.Result.Error(x) => transform(x) 173 | }, 174 | vow, 175 | ); 176 | let sideEffect = (handler, vow) => Vow.sideEffect(handler, vow); 177 | let onError = (handler, vow) => Vow.onError(handler, vow); 178 | let wrap = (promise, handler) => 179 | Vow.wrap(promise) 180 | |> Vow.flatMapUnhandled(x => return(x)) 181 | |> onError(() => fail(handler())); 182 | let unwrap = (transform, vow) => Vow.flatMap(transform, vow); 183 | let all2 = ((v1, v2)) => 184 | v1 185 | |> flatMap(v1Result => 186 | v2 |> flatMap(v2Result => return((v1Result, v2Result))) 187 | ); 188 | let all3 = ((v1, v2, v3)) => 189 | all2((v1, v2)) 190 | |> flatMap(((v1Result, v2Result)) => 191 | v3 |> flatMap(v3Result => return((v1Result, v2Result, v3Result))) 192 | ); 193 | let all4 = ((v1, v2, v3, v4)) => 194 | all3((v1, v2, v3)) 195 | |> flatMap(((v1Result, v2Result, v3Result)) => 196 | v4 197 | |> flatMap(v4Result => 198 | return((v1Result, v2Result, v3Result, v4Result)) 199 | ) 200 | ); 201 | let rec all = vows => 202 | switch (vows) { 203 | | [head, ...tail] => 204 | all(tail) 205 | |> flatMap(tailResult => 206 | head |> flatMap(headResult => return([headResult, ...tailResult])) 207 | ) 208 | | [] => return([]) 209 | }; 210 | module Infix = { 211 | let (>>=) = (v, t) => flatMap(t, v); 212 | let (>|=) = (v, t) => map(t, v); 213 | }; 214 | }; 215 | 216 | include Vow; 217 | -------------------------------------------------------------------------------- /src/vow.rei: -------------------------------------------------------------------------------- 1 | type handled; 2 | 3 | type unhandled; 4 | 5 | type t('a, 'status); 6 | 7 | /*** 8 | * Returns a value wrapped in a vow 9 | */ 10 | let return: 'a => t('a, handled); 11 | 12 | /*** 13 | * Maps a handled vow with value of type 'a to a vow returned by the transform function 14 | */ 15 | let flatMap: ('a => t('b, 'status), t('a, handled)) => t('b, 'status); 16 | 17 | /*** 18 | * Maps an unhandled vow with value of type 'a to a vow returned by the transform function. 19 | * The returned vow is unhandled. 20 | */ 21 | let flatMapUnhandled: 22 | ('a => t('b, 'status), t('a, unhandled)) => t('b, unhandled); 23 | 24 | /*** 25 | * Maps a handled vow with value of type 'a to a vow of the value 26 | * returned by the transform function 27 | */ 28 | let map: ('a => 'b, t('a, handled)) => t('b, 'status); 29 | 30 | /*** 31 | * Maps a handled vow with value of type 'a to a vow of the value 32 | * returned by the transform function 33 | */ 34 | let mapUnhandled: ('a => 'b, t('a, unhandled)) => t('b, unhandled); 35 | 36 | /*** 37 | * Performs side effects with a handled vow's value and returns unit. 38 | */ 39 | let sideEffect: ('a => unit, t('a, handled)) => unit; 40 | 41 | /*** 42 | * Catches and handles the rejection of a backing promise. 43 | */ 44 | let onError: (unit => t('a, 'status), t('a, unhandled)) => t('a, 'status); 45 | 46 | /*** 47 | * Wraps a promise into a vow. You should use this function for wrapping promises that 48 | * might be rejected 49 | */ 50 | let wrap: Js.Promise.t('a) => t('a, unhandled); 51 | 52 | /*** 53 | * Wraps a non failing promise into a vow. Use this function if your the wrapped promise 54 | * is never rejected. 55 | */ 56 | let unsafeWrap: Js.Promise.t('a) => t('a, handled); 57 | 58 | type container('a) = {value: 'a}; 59 | 60 | /*** 61 | * Returns the underlying JS Promise. 62 | */ 63 | let unwrap: t('a, handled) => Js.Promise.t('a); 64 | 65 | /*** 66 | * Takes a tuple of 2 vows and returns a vow with a tuple of their results 67 | */ 68 | let all2: ((t('v1, handled), t('v2, handled))) => t(('v1, 'v2), handled); 69 | 70 | /*** 71 | * Takes a tuple of 3 vows and returns a vow with a tuple of their results 72 | */ 73 | let all3: 74 | ((t('v1, handled), t('v2, handled), t('v3, handled))) => 75 | t(('v1, 'v2, 'v3), handled); 76 | 77 | /*** 78 | * Takes a tuple of 4 vows and returns a vow with a tuple of their results 79 | */ 80 | let all4: 81 | ( 82 | (t('v1, handled), t('v2, handled), t('v3, handled), t('v4, handled)) 83 | ) => 84 | t(('v1, 'v2, 'v3, 'v4), handled); 85 | 86 | /*** 87 | * Takes a list of vows and returns a vow with a list of their results 88 | */ 89 | let all: list(t('value, handled)) => t(list('value), handled); 90 | 91 | module type ResultType = { 92 | type vow('a, 'status) = t('a, 'status); 93 | type t('value, 'error, 'status) = 94 | vow(Belt.Result.t('value, 'error), 'status); 95 | let return: 'value => t('value, 'error, handled); 96 | let fail: 'error => t('value, 'error, handled); 97 | let flatMap: 98 | ('a => t('b, 'error, 'status), t('a, 'error, handled)) => 99 | t('b, 'error, 'status); 100 | let flatMapUnhandled: 101 | ('a => t('b, 'error, 'status), t('a, 'error, unhandled)) => 102 | t('b, 'error, unhandled); 103 | let map: ('a => 'b, t('a, 'error, handled)) => t('b, 'error, 'status); 104 | let mapUnhandled: 105 | ('a => 'b, t('a, 'error, unhandled)) => t('b, 'error, unhandled); 106 | let mapError: 107 | ('a => t('value, 'b, handled), t('value, 'a, 'status)) => 108 | t('value, 'b, 'status); 109 | let sideEffect: 110 | (Belt.Result.t('value, 'error) => unit, t('value, 'error, handled)) => 111 | unit; 112 | let onError: 113 | (unit => t('error, 'value, 'status), t('error, 'value, unhandled)) => 114 | t('error, 'value, 'status); 115 | let wrap: 116 | (Js.Promise.t('value), unit => 'error) => t('value, 'error, handled); 117 | let unwrap: 118 | ( 119 | Belt.Result.t('value, 'error) => vow('a, 'status), 120 | t('value, 'error, handled) 121 | ) => 122 | vow('a, 'status); 123 | let all2: 124 | ((t('v1, 'error, handled), t('v2, 'error, handled))) => 125 | t(('v1, 'v2), 'error, handled); 126 | let all3: 127 | ( 128 | ( 129 | t('v1, 'error, handled), 130 | t('v2, 'error, handled), 131 | t('v3, 'error, handled), 132 | ) 133 | ) => 134 | t(('v1, 'v2, 'v3), 'error, handled); 135 | let all4: 136 | ( 137 | ( 138 | t('v1, 'error, handled), 139 | t('v2, 'error, handled), 140 | t('v3, 'error, handled), 141 | t('v4, 'error, handled), 142 | ) 143 | ) => 144 | t(('v1, 'v2, 'v3, 'v4), 'error, handled); 145 | let all: 146 | list(t('value, 'error, handled)) => t(list('value), 'error, handled); 147 | module Infix: { 148 | let (>>=): 149 | (t('a, 'error, handled), 'a => t('b, 'error, 'status)) => 150 | t('b, 'error, 'status'); 151 | let (>|=): (t('a, 'error, handled), 'a => 'b) => t('b, 'error, handled); 152 | }; 153 | }; 154 | 155 | module Result: ResultType; 156 | -------------------------------------------------------------------------------- /tests/soundness_tests.re: -------------------------------------------------------------------------------- 1 | Vow.return("hello") |> Vow.sideEffect(Js.log); 2 | 3 | /* Vow inside vow */ 4 | Vow.return(Vow.return("hello")) 5 | |> Vow.sideEffect(x => x |> Vow.sideEffect(Js.log)); 6 | 7 | /* Promise inside vow */ 8 | Vow.return(Js.Promise.resolve("hello")) 9 | |> Vow.sideEffect(r => 10 | Js.Promise.then_(str => Js.Promise.resolve(Js.log(str)), r) |> ignore 11 | ); 12 | 13 | Vow.return(Js.Promise.resolve("hello")) 14 | |> Vow.map(Js.Promise.then_(str => Js.Promise.resolve(str ++ " world"))) 15 | |> Vow.map(r => Js.Promise.then_(str => Js.Promise.resolve(Js.log(str)), r)); 16 | 17 | /* Vow inside promise */ 18 | Js.Promise.resolve(Vow.return("hello")) 19 | |> Js.Promise.then_(strVow => 20 | Js.Promise.resolve(Vow.sideEffect(Js.log, strVow)) 21 | ); 22 | 23 | Vow.return(Js.Promise.resolve("unwrapping")) 24 | |> Vow.unwrap 25 | |> Js.Promise.then_(x => Js.log(x) |> Js.Promise.resolve) 26 | |> ignore; 27 | --------------------------------------------------------------------------------