├── .github └── workflows │ └── pull-request.yml ├── .gitignore ├── Changelog.md ├── PROPOSAL.md ├── README.md ├── bsconfig.json ├── examples ├── FetchExample.js └── FetchExample.res ├── package-lock.json ├── package.json ├── src ├── Promise.js ├── Promise.res └── Promise.resi └── tests ├── PromiseTest.js ├── PromiseTest.res ├── Test.js └── Test.res /.github/workflows/pull-request.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: [pull_request] 3 | jobs: 4 | build: 5 | name: Test 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v1 9 | - uses: actions/setup-node@v1 10 | with: 11 | node-version: '14.x' 12 | registry-url: 'https://registry.npmjs.org' 13 | - run: npm install 14 | - run: npm run build 15 | - run: npm test 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.exe 2 | *.obj 3 | *.out 4 | *.compile 5 | *.native 6 | *.byte 7 | *.cmo 8 | *.annot 9 | *.cmi 10 | *.cmx 11 | *.cmt 12 | *.cmti 13 | *.cma 14 | *.a 15 | *.cmxa 16 | *.obj 17 | *~ 18 | *.annot 19 | *.cmj 20 | *.bak 21 | lib/ 22 | lib/bs 23 | *.mlast 24 | *.mliast 25 | .vscode 26 | .merlin 27 | .bsb.lock 28 | /node_modules/ 29 | .DS_Store 30 | 31 | tests/Scratch.res 32 | tests/Scratch.js 33 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | # master 2 | 3 | # v2.1.0 4 | 5 | - Add the `thenResolve` function, which is essentially the `map` function we removed in v1, but with a better name (thanks @mrmurphy for the suggestion) 6 | 7 | # v2.0 8 | 9 | **Breaking** 10 | 11 | - `catch` was not aligned with `then` and didn't return a `t<'a>`. This change forces users to resolve a value within a `catch` callback. 12 | 13 | ```diff 14 | Promise.resolve(1) 15 | - ->catch(err => { 16 | - () 17 | - }) 18 | + ->catch(err => { 19 | + resolve() 20 | + }) 21 | ``` 22 | 23 | **Note:** This also aligns with the previous `Js.Promise.catch_`. 24 | 25 | # v1.0 26 | 27 | **Breaking** 28 | 29 | - Removed `map` function to stay closer to JS api. To migrate, replace all `map` calls with `then`, and make sure you return a `Js.Promise.t` value in the `then` body, e.g. 30 | 31 | ```diff 32 | Promise.resolve(1) 33 | - ->map(n => { 34 | - n + 1 35 | - }) 36 | + ->then(n => { 37 | + resolve(n + 1) 38 | + }) 39 | ``` 40 | 41 | **Bug fixes** 42 | 43 | - Fixes an issue where `Promise.all*` are mapping to the wrong parameter list (should have been tuples, not variadic args) 44 | 45 | # v0.0.2 46 | 47 | - Initial release 48 | -------------------------------------------------------------------------------- /PROPOSAL.md: -------------------------------------------------------------------------------- 1 | # Js.Promise2 Design 2 | 3 | This document proposes the implementation of the `Js.Promise2` binding to replace `Js.Promise` in the future. 4 | 5 | ## Introduction 6 | 7 | ReScript comes with a `Js.Promise` binding that allows binding to vanilla JS promises. Unfortunately those bindings have two glaring issues that make them unintuitive to use: 8 | 9 | 1. Current APIs are `t-last` instead of `t-first`, making them hard to use with the `->` operator (the recommended way to pipe in ReScript) 10 | 2. Catching errors is unweildy, since it currently uses an abstract type `error` without any guidance on how to extract the information 11 | 12 | There's also another issue with chaining promises that resolve nested promises (`Js.Promise.t>`), which we intentionally didn't fix, because we consider it a rare edge-case. We discuss the problem and the trade-offs with our solution in a separate section. 13 | 14 | First let's talk about the two more important problems in detail. 15 | 16 | ### 1) t-last vs t-first APIs 17 | 18 | Right now all functionality within `Js.Promise` are optimized for `|>` usage. Our bindings are designed to be used with the `->` operator. 19 | 20 | **Example t-last** 21 | 22 | ```rescript 23 | let myPromise = Js.Promise.make((~resolve, ~reject) => resolve(. 2)) 24 | 25 | open Js.Promise 26 | 27 | // Note how we need to use the `_` placeholder to be able to use the -> 28 | // operator with pipe-last apis 29 | myPromise 30 | ->then_(value => { 31 | Js.log(value) 32 | resolve(value + 2) 33 | }, _)->then_(value => { 34 | Js.log(value) 35 | resolve(value + 3) 36 | }, _)->catch(err => { 37 | Js.log2("Failure!!", err) 38 | resolve(-2) 39 | }, _)->ignore 40 | ``` 41 | 42 | We want to change the API in a way that makes it look like this (our new bindings are exposed as the `Promise` module): 43 | 44 | ```rescript 45 | // Note how `make` doesn't need any labeled arguments anymore -> closer to the JS api! 46 | let myPromise = Promise.make((resolve, _) => resolve(. 2)) 47 | 48 | open Promise 49 | 50 | myPromise 51 | ->then(value => { 52 | Js.log(value) 53 | resolve(value) 54 | }) 55 | // we also offer a `map` function that spares as the extra `resolve` call 56 | ->then(value => { 57 | resolve(value + 1) 58 | }) 59 | ->then(value => { 60 | Js.log(value) // logs 3 61 | resolve() 62 | }) 63 | ->ignore 64 | ``` 65 | 66 | Please note how we changed the name from `Js.Promise.then_` to `Promise.then`. In ReScript, `then` is not a keyword, so it's perfectly fine to be used as a function name here. 67 | 68 | ### 2) Error Handling 69 | 70 | In the original `Js.Promise` binding, a promise error is encoded as an abstract type `Js.Promise.error`, with no further functionality of accessing the value. Users are supposed to know how to access and transform their value on their own. 71 | 72 | **Example:** 73 | 74 | ```rescript 75 | exception MyError(string) 76 | 77 | // Requires some type of unsafe coercion to be able to access the value 78 | external unsafeToExn: Js.Promise.error => exn = "%identity" 79 | 80 | Js.Promise.reject(MyError("test")) 81 | ->Js.Promise.catch(err => { 82 | switch err->unsafeToExn { 83 | | MyError(str) => Js.log2("My error occurred: ", str) 84 | | _ => Js.log("Some other error occurred") 85 | } 86 | }, _) 87 | ``` 88 | 89 | Now this solution is problematic in many different ways, because without knowing anything about the encoding of ReScript / JS exceptions, one needs to consider following cases: 90 | 91 | - What if `err` is a JS exception thrown through a JS related error? 92 | - What if `err` is actually no exception, but a non-error value? (it is perfectly viable to throw other primitive data types in JS as well) 93 | 94 | We think that this leaves too many decisions on correctly handling the type, so it might end up to different solutions in different codebases. We want to unify that process in the following manner: 95 | 96 | **Proposed API:** 97 | 98 | ```rescript 99 | open Promise 100 | exception MyError(string) 101 | 102 | Promise.reject(MyError("test")) 103 | ->Promise.catch(err => { 104 | switch err { 105 | | MyError(str) => Js.log2("My error occurred: ", str) 106 | | JsError(obj) => 107 | switch Js.Exn.message(obj) { 108 | | Some(msg) => Js.log2("JS error message:", msg) 109 | | None => Js.log("This might be a non-error JS value?") 110 | } 111 | | _ => Js.log("Some other (ReScript) error occurred") 112 | } 113 | }) 114 | ``` 115 | 116 | Starting from ReScript v9 and above, the `Promise.JsError` exception will be deprecated in favor of the builtin `Js.Exn.Error` exception: 117 | 118 | ```rescript 119 | // In this version, like in a synchronous try / catch block with mixed 120 | // ReScript / JS Errors, we use the `Js.Exn.Error` case to match on 121 | // JS errors 122 | Promise.reject(MyError("test")) 123 | ->Promise.catch(err => { 124 | // err is of type `exn` already - no need to classify it yourself! 125 | switch err { 126 | | MyError(str) => Js.log2("My error occurred: ", str) 127 | | Js.Exn.Error(obj) => 128 | switch Js.Exn.message(obj) { 129 | | Some(msg) => Js.log2("JS error message:", msg) 130 | | None => Js.log("This might be a non-error JS value?") 131 | } 132 | | _ => Js.log("Some other (ReScript) error occurred") 133 | } 134 | }) 135 | ``` 136 | 137 | The proposed solution takes the burden of classifying the `Js.Promise.error` value, and allows for a similar pattern match as in a normal try / catch block, as explained in our [exception docs](https://rescript-lang.org/docs/manual/latest/exception#catch-both-rescript-and-js-exceptions-in-the-same-catch-clause). 138 | 139 | ## Nested Promises Issue Trade-offs 140 | 141 | As previously mentioned, right now there is an edge-case in our proposed API that allow a potential runtime error, due to the way nested promises auto-collapse in the JS runtime (which is not correctly reflected by the type system). 142 | 143 | To get more into detail: In JS whenever you return a promise within a promise chain, `then(() => Promise.resolve(somePromise))` will actually pass down the value that’s inside `somePromise`, instead of passing the nested promise (`Promise.t>`). This causes the type system to report a different type than the runtime, ultimately causing runtime errors. 144 | 145 | **Here is a simple demonstration of our edge-case:** 146 | 147 | ```rescript 148 | open Promise 149 | 150 | // NOTE: This code will cause a runtime error that will be caught by catch 151 | 152 | // SCENARIO: resolve a nested promise within `then` 153 | resolve(1)->then((value: int) => { 154 | // BAD: This will cause a Promise.t> 155 | resolve(resolve(value)) 156 | }) 157 | ->then((p: Promise.t) => { 158 | // p is marked as a Promise, but it's actually an int 159 | // so this code will fail 160 | p->then((n) => Js.log(n)->resolve)->ignore 161 | resolve() 162 | }) 163 | ->catch((e) => { 164 | Js.log("luckily, our mistake will be caught here"); 165 | // e: p.then is not a function 166 | }) 167 | ->ignore 168 | ``` 169 | 170 | This topic is not new, and has been solved by different alternative libraries in the Reason ecosystem. For example, see [this discussion in the reason-promise](https://github.com/aantron/promise#discussion-why-js-promises-are-unsafe) repository. 171 | 172 | ### Why we think the "nested promises" problem is not worth solving 173 | 174 | The only way to solve this problem with relatively low effort is by introducing a small runtime layer on top of our Promise resolving mechanism. This runtime would detect resolved promises (nested promises), and put them in a opaque container, so that the JS runtime is not able to auto-collapse the value. Later on when we `then` chain on the resulting data, it will be unwrapped again. 175 | 176 | In our design process, we implemented both, a runtime version, and a non-runtime version. In the beginning the small runtime overhead didn't feel like such a burden, but after building some real-world examples, we realized that a common usage path seldomly triggers the edge-case. 177 | 178 | On the other hand, not using a runtime gives following (in our opinion) huge advantages: 179 | 180 | - Readable and predictable JS output 181 | - Less complexity due to the boxing / unboxing nature, that might collide with other existing JS libraries 182 | - Without the extra complexity, it gives us more room in our complexity budget to introduce other, more pressing features instead (e.g. emulated cancelation wrappers) 183 | 184 | Readable and predictable JS output is probably the most important one, because our goal is seamless interop and almost human-readable JS code after compilation. Also, in practical use-cases, even if we'd introduce said runtime code to prevent the unnesting problem, it wouldn't actually give us any guarantees that there won't be any error during runtime. 185 | 186 | The previously mentioned `reason-promise` tries to tackle all of this dirty edge-cases on multiple levels, but this comes with a complexity cost of introducing two different types to differentiate between `rejectable` and `non-rejectable` promises. This introduces a non-trivial amount of mental overhead, where users are forced to continously categorize between different promises, even if the underlying data structure is the same. 187 | 188 | We think it's more practical to just teach one simple `then`, `all`, `race`, `finally` API, and then tell our users to use a final `catch` on each promise chain, to always be on the runtime safe side even if they make mistakes with our aforementioned edge-cases. 189 | 190 | Also, it is pretty hard to get into the edge-case, since there are different warning flags that you are doing something wrong, e.g.: 191 | 192 | ```rescript 193 | @val external queryUsers: unit => Promise.t> = "queryUsers" 194 | 195 | open Promise 196 | resolve(1) 197 | ->then(value => { 198 | // Let's assume we return a promise here, even though we are not supposed to 199 | resolve(queryUsers()) 200 | }) 201 | // This will cause the next value to be a `Promise.t`, which is not true, because in the JS runtime, it's just an `int` 202 | ->then((value: Promise.t>) => { 203 | // Now the consumer would be forced to use a `then` within a `then`, which seems unintuitive. 204 | // The correct way would have been to not use a `resolve` on our `queryUsers` call in the `then` before 205 | value->then((v) => { 206 | Js.log(v) 207 | resolve() 208 | }) 209 | }) 210 | ->catch((e) => { 211 | // This catch all clause will luckily protect us from the edge-case above 212 | }) 213 | ``` 214 | 215 | **To sum it up:** We think the upsides of having zero-cost interop, while having familiar JS, outweights the benefits of allowing nested promises, which should hopefully not happen in real world scenarios anyways. 216 | 217 | ## Compatiblity 218 | 219 | Our proposed API exposes a `Promise.t` type that is fully compatible with the original `Js.Promise.t`, so it's easy to use the new bindings in existing codebases with `Js.Promise` code. 220 | 221 | ## Prior Art 222 | 223 | ### reason-promise 224 | 225 | The most obvious here is the already mentioned [reason-promise](https://github.com/aantron/promise), which was the most prominent inspiration for our proposal. 226 | 227 | **The good parts:** The interesting part are the boxing / unboxing mechanism, which allows us to automatically box any non-promise value in a `PromiseBox`, that gets wrapped depending on the value at hand. This code adds some additional runtime overhead on each `then` call, but is arguably small and most likely won't end up in any hot paths. It fixes the nested promises problem quite efficiently. 228 | 229 | A few things we also recognized when evaluating the library: 230 | 231 | **Non idiomatic APIs:** The APIs are harder to understand, and the library tries to tackle more problems than we care about. E.g. it tracks the `rejectable` state of a promise, which means we need to differentiate between two categories of promises. 232 | 233 | It also adds `uncaughtError` handlers, and `result` / `option` based apis, which are easy to build in userspace, and should probably not be part of a core module. 234 | 235 | **JS unfriendly APIs:** It has a preference for `map` and `flatMap` over the original `then` naming, probably to satisify its criteria for Reason / OCaml usage (let\* operator), it uses `list` instead of `Array`, which causes unnecessary convertion (e.g. in `Promise.all`. This causes too much overhead to be a low-level solution for Promises in ReScript. 236 | 237 | ### Other related libraries 238 | 239 | - [RationalJS/future](https://github.com/RationalJS/future) 240 | - [wokalski/vow](https://github.com/wokalski/vow) 241 | - [yawaramin/prometo](https://www.npmjs.com/package/@yawaramin/prometo) 242 | 243 | All those libraries gave us some good insights on different approaches of tackling promises during runtime, and they are probably a good solution for users who want to go the extra mile for extra type safety features. We wanted to keep it minimalistic though, so we generally went with a simpler approach. 244 | 245 | ## Conclusion 246 | 247 | We think that with the final design, as documented in the [README](./README.md), we evaluated all available options and settled with the most minimalistic version of a `Promise` binding, that allows us to fix up the most pressing problems, and postpone the other mentioned problems to a later point in time. It's easier to argue to add a runtime layer later on, if the edge-cases turned out to be regular cases. 248 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rescript-promise 2 | 3 | This is a proposal for replacing the original `Js.Promise` binding that is shipped within the ReScript compiler. It will be upstreamed as `Js.Promise2` soon. This binding was made to allow our users to try out the implementation in their codebases first. 4 | 5 | > See the [PROPOSAL.md](./PROPOSAL.md) for the rationale and design decisions. 6 | 7 | **Feature Overview:** 8 | 9 | - `t-first` bindings 10 | - Fully compatible with the builtin `Js.Promise.t` type 11 | - `make` for creating a new promise with a `(resolve, reject) => {}` callback 12 | - `resolve` for creating a resolved promise 13 | - `reject` for creating a rejected promise 14 | - `catch` for catching any JS or ReScript errors (all represented as an `exn` value) 15 | - `then` for chaining functions that return another promise 16 | - `thenResolve` for chaining functions that transform the value inside a promise 17 | - `all` and `race` for running promises concurrently 18 | - `finally` for arbitrary tasks after a promise has rejected / resolved 19 | - Globally accessible `Promise` module that doesn't collide with `Js.Promise` 20 | 21 | **Non-Goals of `rescript-promise`:** 22 | 23 | - No rejection tracking or other complex type hackery 24 | - No special utilities (we will add docs on how to implement common utils on your own) 25 | 26 | **Caveats:** 27 | 28 | - There are 2 edge-cases where returning a `Promise.t>` value within `then` / `thenResolve` is not runtime safe (but also quite rare in practise). Refer to the [Common Mistakes](#common-mistakes) section for details. 29 | - These edge-cases shouldn't happen in day to day use, also, for those with general concerns about runtime safetiness, it is recommended to use a `catch` call in the end of each promise chain to prevent runtime crashes anyways (just like in JS). 30 | 31 | ## Requirements 32 | 33 | `bs-platform@8.2` and above. 34 | 35 | ## Installation 36 | 37 | ``` 38 | npm install @ryyppy/rescript-promise --save 39 | ``` 40 | 41 | Add `@ryyppy/rescript-promise` as a dependency in your `bsconfig.json`: 42 | 43 | ```json 44 | { 45 | "bs-dependencies": ["@ryyppy/rescript-promise"] 46 | } 47 | ``` 48 | 49 | This will expose a global `Promise` module (don't worry, it will not mess with your existing `Js.Promise` code). 50 | 51 | ### In case you are using @aantron/promise 52 | 53 | Unfortunately I didn't consider the case of mixing `@aantron/promise` with this particular binding, and both libraries bind to the globally accessible `Promise` module, so you can't mix them. 54 | 55 | In this case, copy `src/Promise.res` and `src/Promise.resi` into your bindings folder as `Promise2.res` and `Promise2.resi` and use it that way. Sorry for the inconvenience. At some point it will be part of the compiler stdlib, so hopefully this will be easy to migrate to later on. 56 | 57 | ## Examples 58 | 59 | - [examples/FetchExample.res](examples/FetchExample.res): Using the `fetch` api to login / query some data with a full promise chain scenario 60 | 61 | ## Usage 62 | 63 | **Creating a Promise:** 64 | 65 | ```rescript 66 | let p1 = Promise.make((resolve, _reject) => { 67 | 68 | // We use uncurried functions for resolve / reject 69 | // for cleaner JS output without unintended curry calls 70 | resolve(. "hello world") 71 | }) 72 | 73 | let p2 = Promise.resolve("some value") 74 | 75 | // You can only reject `exn` values for streamlined catch handling 76 | exception MyOwnError(string) 77 | let p3 = Promise.reject(MyOwnError("some rejection")) 78 | ``` 79 | 80 | **Access and transform a promise value:** 81 | 82 | ```rescript 83 | open Promise 84 | Promise.resolve("hello world") 85 | ->then(msg => { 86 | // then callbacks require the result to be resolved explicitly 87 | resolve("Message: " ++ msg) 88 | }) 89 | ->then(msg => { 90 | Js.log(msg); 91 | 92 | // Even if there is no result, we need to use resolve() to return a promise 93 | resolve() 94 | }) 95 | ->ignore // Requires ignoring due to unhandled return value 96 | ``` 97 | 98 | **Chain promises:** 99 | 100 | ```rescript 101 | open Promise 102 | 103 | type user = {"name": string} 104 | type comment = string 105 | 106 | // mock function 107 | let queryComments = (username: string): Js.Promise.t> => { 108 | switch username { 109 | | "patrick" => ["comment 1", "comment 2"] 110 | | _ => [] 111 | }->resolve 112 | } 113 | 114 | // mock function 115 | let queryUser = (_: string): Js.Promise.t => { 116 | resolve({"name": "patrick"}) 117 | } 118 | 119 | let queryUser = queryUser("u1") 120 | ->then(user => { 121 | // We use `then` to automatically 122 | // unnest our queryComments promise 123 | queryComments(user["name"]) 124 | }) 125 | ->then(comments => { 126 | // comments is now an array 127 | Belt.Array.forEach(comments, comment => Js.log(comment)) 128 | 129 | // Output: 130 | // comment 1 131 | // comment 2 132 | 133 | resolve() 134 | }) 135 | ->ignore 136 | ``` 137 | 138 | You can also use `thenResolve` to chain a promise, and transform its nested value: 139 | 140 | ```rescript 141 | open Promise 142 | 143 | let createNumPromise = (n) => resolve(n) 144 | 145 | createNumPromise(5) 146 | ->thenResolve(num => { 147 | num + 1 148 | }) 149 | ->thenResolve(num => { 150 | Js.log(num) 151 | }) 152 | ->ignore 153 | ``` 154 | 155 | **Catch promise errors:** 156 | 157 | **Important:** `catch` needs to return the same return value as its previous `then` call (e.g. if you pass a `promise` of type `Promise.t`, you need to return an `int` in your `catch` callback). This usually implies that you'll need to use a `result` value to express successful / unsuccessful operations: 158 | 159 | ```rescript 160 | exception MyError(string) 161 | 162 | open Promise 163 | 164 | Promise.reject(MyError("test")) 165 | ->then(str => { 166 | Js.log("this should not be reached: " ++ str) 167 | 168 | // Here we use the builtin `result` constructor `Ok` 169 | Ok("successful")->resolve 170 | }) 171 | ->catch(e => { 172 | let err = switch e { 173 | | MyError(str) => "found MyError: " ++ str 174 | | _ => "Some unknown error" 175 | } 176 | 177 | // Here we are using the same type (`t`) as in the previous `then` call 178 | Error(err)->resolve 179 | }) 180 | ->then(result => { 181 | let msg = switch result { 182 | | Ok(str) => "Successful: " ++ str 183 | | Error(msg) => "Error: " ++ msg 184 | } 185 | Js.log(msg) 186 | resolve() 187 | }) 188 | ->ignore 189 | ``` 190 | 191 | **Catch promise errors caused by a thrown JS exception:** 192 | 193 | ```rescript 194 | open Promise 195 | 196 | let causeErr = () => { 197 | Js.Exn.raiseError("Some JS error")->resolve 198 | } 199 | 200 | Promise.resolve() 201 | ->then(_ => { 202 | causeErr() 203 | }) 204 | ->catch(e => { 205 | switch e { 206 | | JsError(obj) => 207 | switch Js.Exn.message(obj) { 208 | | Some(msg) => Js.log("Some JS error msg: " ++ msg) 209 | | None => Js.log("Must be some non-error value") 210 | } 211 | | _ => Js.log("Some unknown error") 212 | } 213 | resolve() 214 | // Outputs: Some JS error msg: Some JS error 215 | }) 216 | ->ignore 217 | ``` 218 | 219 | **Catch promise errors that can be caused by ReScript OR JS Errors (mixed error types):** 220 | 221 | Every value passed to `catch` are unified into an `exn` value, no matter if those errors were thrown in JS, or in ReScript. This is similar to how we [handle mixed JS / ReScript errors](https://rescript-lang.org/docs/manual/latest/exception#catch-both-rescript-and-js-exceptions-in-the-same-catch-clause) in synchronous try / catch blocks. 222 | 223 | ```rescript 224 | exception TestError(string) 225 | 226 | let causeJsErr = () => { 227 | Js.Exn.raiseError("Some JS error") 228 | } 229 | 230 | let causeReScriptErr = () => { 231 | raise(TestError("Some ReScript error")) 232 | } 233 | 234 | // imaginary randomizer function 235 | @bs.val external generateRandomInt: unit => int = "generateRandomInt" 236 | 237 | open Promise 238 | 239 | resolve() 240 | ->then(_ => { 241 | // We simulate a promise that either throws 242 | // a ReScript error, or JS error 243 | if generateRandomInt() > 5 { 244 | causeReScriptErr() 245 | } else { 246 | causeJsErr() 247 | }->resolve 248 | }) 249 | ->catch(e => { 250 | switch e { 251 | | TestError(msg) => Js.log("ReScript Error caught:" ++ msg) 252 | | JsError(obj) => 253 | switch Js.Exn.message(obj) { 254 | | Some(msg) => Js.log("Some JS error msg: " ++ msg) 255 | | None => Js.log("Must be some non-error value") 256 | } 257 | | _ => Js.log("Some unknown error") 258 | } 259 | resolve() 260 | }) 261 | ->ignore 262 | ``` 263 | 264 | **Using a promise from JS (interop):** 265 | 266 | ```rescript 267 | open Promise 268 | 269 | @val external someAsyncApi: unit => Js.Promise.t = "someAsyncApi" 270 | 271 | someAsyncApi()->Promise.then((str) => Js.log(str)->resolve)->ignore 272 | ``` 273 | 274 | **Running multiple Promises concurrently:** 275 | 276 | ```rescript 277 | open Promise 278 | 279 | let place = ref(0) 280 | 281 | let delayedMsg = (ms, msg) => { 282 | Promise.make((resolve, _) => { 283 | Js.Global.setTimeout(() => { 284 | place := place.contents + 1 285 | resolve(.(place.contents, msg)) 286 | }, ms)->ignore 287 | }) 288 | } 289 | 290 | let p1 = delayedMsg(1000, "is Anna") 291 | let p2 = delayedMsg(500, "myName") 292 | let p3 = delayedMsg(100, "Hi") 293 | 294 | all([p1, p2, p3])->then(arr => { 295 | // arr = [ [ 3, 'is Anna' ], [ 2, 'myName' ], [ 1, 'Hi' ] ] 296 | 297 | Belt.Array.forEach(arr, ((place, name)) => { 298 | Js.log(`Place ${Belt.Int.toString(place)} => ${name}`) 299 | }) 300 | // forEach output: 301 | // Place 3 => is Anna 302 | // Place 2 => myName 303 | // Place 1 => Hi 304 | 305 | resolve() 306 | }) 307 | ->ignore 308 | ``` 309 | 310 | **Race Promises:** 311 | 312 | ```rescript 313 | open Promise 314 | 315 | let racer = (ms, name) => { 316 | Promise.make((resolve, _) => { 317 | Js.Global.setTimeout(() => { 318 | resolve(. name) 319 | }, ms)->ignore 320 | }) 321 | } 322 | 323 | let promises = [racer(1000, "Turtle"), racer(500, "Hare"), racer(100, "Eagle")] 324 | 325 | race(promises) 326 | ->then(winner => { 327 | Js.log("Congrats: " ++ winner)->resolve 328 | // Congrats: Eagle 329 | }) 330 | ->ignore 331 | ``` 332 | 333 | ## Common Mistakes 334 | 335 | **Don't return a `Promise.t>` within a `then` callback:** 336 | 337 | ```rescript 338 | open Promise 339 | 340 | resolve(1) 341 | ->then((value: int) => { 342 | let someOtherPromise = resolve(value + 2) 343 | 344 | // BAD: this will cause a Promise.t> 345 | resolve(someOtherPromise) 346 | }) 347 | ->then((p: Promise.t) => { 348 | // p is marked as a Promise, but it's actually an int 349 | // so this code will fail 350 | p->then((n) => Js.log(n)->resolve) 351 | }) 352 | ->catch((e) => { 353 | Js.log("luckily, our mistake will be caught here"); 354 | Js.log(e) 355 | // p.then is not a function 356 | resolve() 357 | }) 358 | ->ignore 359 | ``` 360 | 361 | **Don't return a `Promise.t<'a>` within a `thenResolve` callback:** 362 | 363 | ```rescript 364 | open Promise 365 | resolve(1) 366 | ->thenResolve((value: int) => { 367 | // BAD: This will cause a Promise.t> 368 | resolve(value) 369 | }) 370 | ->thenResolve((p: Promise.t) => { 371 | // p is marked as a Promise, but it's actually an int 372 | // so this code will fail 373 | p->thenResolve((n) => Js.log(n))->ignore 374 | }) 375 | ->catch((e) => { 376 | Js.log("luckily, our mistake will be caught here"); 377 | // e: p.then is not a function 378 | resolve() 379 | }) 380 | ->ignore 381 | ``` 382 | 383 | ## Development 384 | 385 | ``` 386 | # Building 387 | npm run build 388 | 389 | # Watching 390 | npm run dev 391 | ``` 392 | 393 | ## Run Test 394 | 395 | Runs all tests 396 | 397 | ``` 398 | node tests/PromiseTest.js 399 | ``` 400 | 401 | ## Run Examples 402 | 403 | Examples are runnable on node, and require an active internet connection to be able to access external mockup apis. 404 | 405 | ``` 406 | node examples/FetchExample.js 407 | ``` 408 | -------------------------------------------------------------------------------- /bsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ryyppy/rescript-promise", 3 | "sources": [ 4 | { 5 | "dir" : "src", 6 | "subdirs" : true 7 | }, 8 | { 9 | "dir" : "tests", 10 | "type": "dev" 11 | }, 12 | { 13 | "dir" : "examples", 14 | "type": "dev" 15 | } 16 | ], 17 | "package-specs": { 18 | "module": "commonjs", 19 | "in-source": true 20 | }, 21 | "suffix": ".js", 22 | "warnings": { 23 | "error" : "+101" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/FetchExample.js: -------------------------------------------------------------------------------- 1 | // Generated by ReScript, PLEASE EDIT WITH CARE 2 | 'use strict'; 3 | 4 | var $$Promise = require("../src/Promise.js"); 5 | var Belt_Array = require("bs-platform/lib/js/belt_Array.js"); 6 | var NodeFetch = require("node-fetch"); 7 | var Caml_exceptions = require("bs-platform/lib/js/caml_exceptions.js"); 8 | 9 | globalThis.fetch = NodeFetch; 10 | 11 | var NodeFetchPolyfill = {}; 12 | 13 | var $$Response = {}; 14 | 15 | function login(email, password) { 16 | var body = { 17 | email: email, 18 | password: password 19 | }; 20 | var params = { 21 | method: "POST", 22 | headers: { 23 | "Content-Type": "application/json" 24 | }, 25 | body: JSON.stringify(body) 26 | }; 27 | return $$Promise.$$catch(globalThis.fetch("https://reqres.in/api/login", params).then(function (res) { 28 | return res.json(); 29 | }).then(function (data) { 30 | var msg = data.error; 31 | var tmp; 32 | if (msg == null) { 33 | var token = data.token; 34 | tmp = (token == null) ? ({ 35 | TAG: /* Error */1, 36 | _0: "Didn't return a token" 37 | }) : ({ 38 | TAG: /* Ok */0, 39 | _0: token 40 | }); 41 | } else { 42 | tmp = { 43 | TAG: /* Error */1, 44 | _0: msg 45 | }; 46 | } 47 | return Promise.resolve(tmp); 48 | }), (function (e) { 49 | var msg; 50 | if (e.RE_EXN_ID === $$Promise.JsError) { 51 | var msg$1 = e._1.message; 52 | msg = msg$1 !== undefined ? msg$1 : ""; 53 | } else { 54 | msg = "Unexpected error occurred"; 55 | } 56 | return Promise.resolve({ 57 | TAG: /* Error */1, 58 | _0: msg 59 | }); 60 | })); 61 | } 62 | 63 | var Login = { 64 | login: login 65 | }; 66 | 67 | function getProducts(token, param) { 68 | var params = { 69 | Authorization: "Bearer " + token 70 | }; 71 | return $$Promise.$$catch(globalThis.fetch("https://reqres.in/api/products", params).then(function (res) { 72 | return res.json(); 73 | }).then(function (data) { 74 | var data$1 = data.data; 75 | var ret = (data$1 == null) ? [] : data$1; 76 | return Promise.resolve({ 77 | TAG: /* Ok */0, 78 | _0: ret 79 | }); 80 | }), (function (e) { 81 | var msg; 82 | if (e.RE_EXN_ID === $$Promise.JsError) { 83 | var msg$1 = e._1.message; 84 | msg = msg$1 !== undefined ? msg$1 : ""; 85 | } else { 86 | msg = "Unexpected error occurred"; 87 | } 88 | return Promise.resolve({ 89 | TAG: /* Error */1, 90 | _0: msg 91 | }); 92 | })); 93 | } 94 | 95 | var Product = { 96 | getProducts: getProducts 97 | }; 98 | 99 | var FailedRequest = Caml_exceptions.create("FetchExample.FailedRequest"); 100 | 101 | $$Promise.$$catch(login("emma.wong@reqres.in", "pw").then(function (ret) { 102 | if (ret.TAG !== /* Ok */0) { 103 | return Promise.reject({ 104 | RE_EXN_ID: FailedRequest, 105 | _1: "Login error - " + ret._0 106 | }); 107 | } 108 | console.log("Login successful! Querying data..."); 109 | return getProducts(ret._0, undefined); 110 | }).then(function (result) { 111 | var tmp; 112 | if (result.TAG === /* Ok */0) { 113 | console.log("\nAvailable Products:\n---"); 114 | tmp = Belt_Array.forEach(result._0, (function (p) { 115 | console.log(String(p.id) + " - " + p.name); 116 | 117 | })); 118 | } else { 119 | console.log("Could not query products: " + result._0); 120 | tmp = undefined; 121 | } 122 | return Promise.resolve(tmp); 123 | }), (function (e) { 124 | if (e.RE_EXN_ID === FailedRequest) { 125 | console.log("Operation failed! " + e._1); 126 | } else { 127 | console.log("Unknown error"); 128 | } 129 | return Promise.resolve(undefined); 130 | })); 131 | 132 | exports.NodeFetchPolyfill = NodeFetchPolyfill; 133 | exports.$$Response = $$Response; 134 | exports.Login = Login; 135 | exports.Product = Product; 136 | exports.FailedRequest = FailedRequest; 137 | /* Not a pure module */ 138 | -------------------------------------------------------------------------------- /examples/FetchExample.res: -------------------------------------------------------------------------------- 1 | // This is only needed for polyfilling the `fetch` API in 2 | // node, so we can run this example on the commandline 3 | module NodeFetchPolyfill = { 4 | type t 5 | @module external fetch: t = "node-fetch" 6 | @val external globalThis: 'a = "globalThis" 7 | globalThis["fetch"] = fetch 8 | } 9 | 10 | /* 11 | 12 | In this example, we are accessing a REST endpoint by doing two async operations: 13 | - Login with a valid user and retrieve a Bearer token 14 | - Use the token in our next call to retrieve a list of products 15 | 16 | We factor our code in two submodules: Login and Product. 17 | 18 | Both modules bind to their own specialized version of `fetch` in the global scope, 19 | and specify the return type to their resulting data structures. 20 | 21 | Results are not formally verified (decoded), so we made type assumptions on our 22 | incoming data, and depending on its results, return a `result` value to signal 23 | error or success cases. 24 | 25 | We also use some `catch` calls to either short-circuit operations that have failed, 26 | or to catch failed operations to unify into a `result` value. 27 | */ 28 | 29 | // Fetch uses a `Response` object that offers a `res.json()` function to retrieve 30 | // a json result. We use a json based api, so we create a binding to access this feature. 31 | module Response = { 32 | type t<'data> 33 | @send external json: t<'data> => Promise.t<'data> = "json" 34 | } 35 | 36 | module Login = { 37 | // This is our type assumption for a /login query return value 38 | // In case the operation was successful, the response will contain a `token` field, 39 | // otherwise it will return an `{"error": "msg"}` value that signals an unsuccessful login 40 | type response = {"token": Js.Nullable.t, "error": Js.Nullable.t} 41 | 42 | @val @scope("globalThis") 43 | external fetch: ( 44 | string, 45 | 'params, 46 | ) => Promise.t, "error": Js.Nullable.t}>> = 47 | "fetch" 48 | 49 | let login = (email: string, password: string) => { 50 | open Promise 51 | 52 | let body = { 53 | "email": email, 54 | "password": password, 55 | } 56 | 57 | let params = { 58 | "method": "POST", 59 | "headers": { 60 | "Content-Type": "application/json", 61 | }, 62 | "body": Js.Json.stringifyAny(body), 63 | } 64 | 65 | fetch("https://reqres.in/api/login", params) 66 | ->then(res => { 67 | Response.json(res) 68 | }) 69 | ->then(data => { 70 | // Notice our pattern match on the "error" / "token" fields 71 | // to determine the final result. Be aware that this logic highly 72 | // depends on the backend specificiation. 73 | switch Js.Nullable.toOption(data["error"]) { 74 | | Some(msg) => Error(msg) 75 | | None => 76 | switch Js.Nullable.toOption(data["token"]) { 77 | | Some(token) => Ok(token) 78 | | None => Error("Didn't return a token") 79 | } 80 | }->resolve 81 | }) 82 | ->catch(e => { 83 | let msg = switch e { 84 | | JsError(err) => 85 | switch Js.Exn.message(err) { 86 | | Some(msg) => msg 87 | | None => "" 88 | } 89 | | _ => "Unexpected error occurred" 90 | } 91 | Error(msg)->resolve 92 | }) 93 | } 94 | } 95 | 96 | module Product = { 97 | type t = {id: int, name: string} 98 | 99 | @val @scope("globalThis") 100 | external fetch: (string, 'params) => Promise.t>}>> = 101 | "fetch" 102 | 103 | let getProducts = (~token: string, ()) => { 104 | open Promise 105 | 106 | let params = { 107 | "Authorization": `Bearer ${token}`, 108 | } 109 | 110 | fetch("https://reqres.in/api/products", params) 111 | ->then(res => { 112 | res->Response.json 113 | }) 114 | ->then(data => { 115 | let ret = switch Js.Nullable.toOption(data["data"]) { 116 | | Some(data) => data 117 | | None => [] 118 | } 119 | Ok(ret)->resolve 120 | }) 121 | ->catch(e => { 122 | let msg = switch e { 123 | | JsError(err) => 124 | switch Js.Exn.message(err) { 125 | | Some(msg) => msg 126 | | None => "" 127 | } 128 | | _ => "Unexpected error occurred" 129 | } 130 | Error(msg)->resolve 131 | }) 132 | } 133 | } 134 | 135 | exception FailedRequest(string) 136 | 137 | let _ = { 138 | open Promise 139 | Login.login("emma.wong@reqres.in", "pw") 140 | ->Promise.then(ret => { 141 | switch ret { 142 | | Ok(token) => 143 | Js.log("Login successful! Querying data...") 144 | Product.getProducts(~token, ()) 145 | | Error(msg) => reject(FailedRequest("Login error - " ++ msg)) 146 | } 147 | }) 148 | ->then(result => { 149 | switch result { 150 | | Ok(products) => 151 | Js.log("\nAvailable Products:\n---") 152 | Belt.Array.forEach(products, p => { 153 | Js.log(`${Belt.Int.toString(p.id)} - ${p.name}`) 154 | }) 155 | | Error(msg) => Js.log("Could not query products: " ++ msg) 156 | }->resolve 157 | }) 158 | ->catch(e => { 159 | switch e { 160 | | FailedRequest(msg) => Js.log("Operation failed! " ++ msg) 161 | | _ => Js.log("Unknown error") 162 | } 163 | resolve() 164 | }) 165 | } 166 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rescript-promise", 3 | "version": "2.1.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@babel/code-frame": { 8 | "version": "7.12.11", 9 | "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", 10 | "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", 11 | "dev": true, 12 | "requires": { 13 | "@babel/highlight": "^7.10.4" 14 | } 15 | }, 16 | "@babel/helper-validator-identifier": { 17 | "version": "7.12.11", 18 | "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", 19 | "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", 20 | "dev": true 21 | }, 22 | "@babel/highlight": { 23 | "version": "7.10.4", 24 | "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", 25 | "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", 26 | "dev": true, 27 | "requires": { 28 | "@babel/helper-validator-identifier": "^7.10.4", 29 | "chalk": "^2.0.0", 30 | "js-tokens": "^4.0.0" 31 | } 32 | }, 33 | "ansi-styles": { 34 | "version": "3.2.1", 35 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 36 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 37 | "dev": true, 38 | "requires": { 39 | "color-convert": "^1.9.0" 40 | } 41 | }, 42 | "bs-platform": { 43 | "version": "8.4.2", 44 | "resolved": "https://registry.npmjs.org/bs-platform/-/bs-platform-8.4.2.tgz", 45 | "integrity": "sha512-9q7S4/LLV/a68CweN382NJdCCr/lOSsJR3oQYnmPK98ChfO/AdiA3lYQkQTp6T+U0I5Z5RypUAUprNstwDtMDQ==", 46 | "dev": true 47 | }, 48 | "chalk": { 49 | "version": "2.4.2", 50 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 51 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 52 | "dev": true, 53 | "requires": { 54 | "ansi-styles": "^3.2.1", 55 | "escape-string-regexp": "^1.0.5", 56 | "supports-color": "^5.3.0" 57 | } 58 | }, 59 | "color-convert": { 60 | "version": "1.9.3", 61 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 62 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 63 | "dev": true, 64 | "requires": { 65 | "color-name": "1.1.3" 66 | } 67 | }, 68 | "color-name": { 69 | "version": "1.1.3", 70 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 71 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", 72 | "dev": true 73 | }, 74 | "escape-string-regexp": { 75 | "version": "1.0.5", 76 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 77 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 78 | "dev": true 79 | }, 80 | "has-flag": { 81 | "version": "3.0.0", 82 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 83 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 84 | "dev": true 85 | }, 86 | "js-tokens": { 87 | "version": "4.0.0", 88 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 89 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 90 | "dev": true 91 | }, 92 | "node-fetch": { 93 | "version": "2.6.1", 94 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", 95 | "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", 96 | "dev": true 97 | }, 98 | "supports-color": { 99 | "version": "5.5.0", 100 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 101 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 102 | "dev": true, 103 | "requires": { 104 | "has-flag": "^3.0.0" 105 | } 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ryyppy/rescript-promise", 3 | "version": "2.1.0", 4 | "description": "2020 proposal for new ReScript promise bindings", 5 | "homepage": "https://github.com/ryyppy/rescript-promise", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "node tests/PromiseTest.js", 9 | "build": "bsb -make-world", 10 | "watch": "bsb -make-world -w" 11 | }, 12 | "files": [ 13 | "src/Promise.res", 14 | "src/Promise.resi", 15 | "bsconfig.json" 16 | ], 17 | "keywords": [ 18 | "rescript", 19 | "promise", 20 | "bindings" 21 | ], 22 | "author": "Patrick Ecker", 23 | "license": "MIT", 24 | "devDependencies": { 25 | "@babel/code-frame": "^7.12.11", 26 | "bs-platform": "8.4.2", 27 | "node-fetch": "^2.6.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Promise.js: -------------------------------------------------------------------------------- 1 | // Generated by ReScript, PLEASE EDIT WITH CARE 2 | 'use strict'; 3 | 4 | var Curry = require("bs-platform/lib/js/curry.js"); 5 | var Caml_exceptions = require("bs-platform/lib/js/caml_exceptions.js"); 6 | 7 | var JsError = Caml_exceptions.create("Promise.JsError"); 8 | 9 | function $$catch(promise, callback) { 10 | return promise.catch(function (err) { 11 | return Curry._1(callback, Caml_exceptions.caml_is_extension(err) ? err : ({ 12 | RE_EXN_ID: JsError, 13 | _1: err 14 | })); 15 | }); 16 | } 17 | 18 | exports.JsError = JsError; 19 | exports.$$catch = $$catch; 20 | /* No side effect */ 21 | -------------------------------------------------------------------------------- /src/Promise.res: -------------------------------------------------------------------------------- 1 | type t<+'a> = Js.Promise.t<'a> 2 | 3 | exception JsError(Js.Exn.t) 4 | external unsafeToJsExn: exn => Js.Exn.t = "%identity" 5 | 6 | @bs.new 7 | external make: ((@bs.uncurry (. 'a) => unit, (. 'e) => unit) => unit) => t<'a> = "Promise" 8 | 9 | @bs.val @bs.scope("Promise") 10 | external resolve: 'a => t<'a> = "resolve" 11 | 12 | @bs.send external then: (t<'a>, @uncurry ('a => t<'b>)) => t<'b> = "then" 13 | 14 | @bs.send 15 | external thenResolve: (t<'a>, @uncurry ('a => 'b)) => t<'b> = "then" 16 | 17 | @bs.send external finally: (t<'a>, unit => unit) => t<'a> = "finally" 18 | 19 | @bs.scope("Promise") @bs.val 20 | external reject: exn => t<_> = "reject" 21 | 22 | @bs.scope("Promise") @bs.val 23 | external all: array> => t> = "all" 24 | 25 | @bs.scope("Promise") @bs.val 26 | external all2: ((t<'a>, t<'b>)) => t<('a, 'b)> = "all" 27 | 28 | @bs.scope("Promise") @bs.val 29 | external all3: ((t<'a>, t<'b>, t<'c>)) => t<('a, 'b, 'c)> = "all" 30 | 31 | @bs.scope("Promise") @bs.val 32 | external all4: ((t<'a>, t<'b>, t<'c>, t<'d>)) => t<('a, 'b, 'c, 'd)> = "all" 33 | 34 | @bs.scope("Promise") @bs.val 35 | external all5: ((t<'a>, t<'b>, t<'c>, t<'d>, t<'e>)) => t<('a, 'b, 'c, 'd, 'e)> = "all" 36 | 37 | @bs.scope("Promise") @bs.val 38 | external all6: ((t<'a>, t<'b>, t<'c>, t<'d>, t<'e>, t<'f>)) => t<('a, 'b, 'c, 'd, 'e, 'f)> = "all" 39 | 40 | @bs.send 41 | external _catch: (t<'a>, @bs.uncurry (exn => t<'a>)) => t<'a> = "catch" 42 | 43 | let catch = (promise, callback) => { 44 | _catch(promise, err => { 45 | // In future versions, we could use the better version: 46 | /* callback(Js.Exn.anyToExnInternal(e)) */ 47 | 48 | // for now we need to bring our own JsError type 49 | let v = if Js.Exn.isCamlExceptionOrOpenVariant(err) { 50 | err 51 | } else { 52 | JsError(unsafeToJsExn(err)) 53 | } 54 | callback(v) 55 | }) 56 | } 57 | 58 | @bs.scope("Promise") @bs.val 59 | external race: array> => t<'a> = "race" 60 | -------------------------------------------------------------------------------- /src/Promise.resi: -------------------------------------------------------------------------------- 1 | // The +'a marks the abstract type parameter as covariant, which essentially means that 2 | // a value of type 'a is immutable and may not be used in some mutable context. 3 | // 4 | // This makes sense for promises, since according to their specification, once a promise has 5 | // been resolved (with a specific value), it will never change its resolved value. 6 | // 7 | // More details about polymorphism / invariance / covariance,... can be found here: 8 | // https://caml.inria.fr/pub/docs/manual-ocaml/polymorphism.html#ss:variance:abstract-data-types 9 | type t<+'a> = Js.Promise.t<'a> 10 | 11 | // JsError is currently a shim exception and will be deprecated for 12 | // ReScript version >= 9.0. 13 | // 14 | // See following merged compiler PR for more context: 15 | // https://github.com/rescript-lang/rescript-compiler/pull/4905 16 | exception JsError(Js.Exn.t) 17 | 18 | @ocaml.doc(" 19 | [resolve(value)] creates a resolved Promise with a given `value` 20 | 21 | ```rescript 22 | let p = Promise.resolve(5) // Promise.t 23 | ``` 24 | ") 25 | @bs.val 26 | @bs.scope("Promise") 27 | external resolve: 'a => t<'a> = "resolve" 28 | 29 | @bs.scope("Promise") @bs.val 30 | external reject: exn => t<_> = "reject" 31 | 32 | @ocaml.doc(" 33 | `make(callback)` creates a new Promise based on a `callback` that receives two 34 | uncurried functions `resolve` and `reject` for defining the Promise's result. 35 | 36 | ```rescript 37 | open Promise 38 | 39 | let n = 4 40 | Promise.make((resolve, reject) => { 41 | if(n < 5) { 42 | resolve(. \"success\") 43 | } 44 | else { 45 | reject(. \"failed\") 46 | } 47 | }) 48 | ->then(str => { 49 | Js.log(str)->resolve 50 | }) 51 | ->catch(e => { 52 | Js.log(\"Error occurred\") 53 | resolve() 54 | }) 55 | ->ignore 56 | ``` 57 | ") 58 | @bs.new 59 | external make: ((@bs.uncurry (. 'a) => unit, (. 'e) => unit) => unit) => t<'a> = "Promise" 60 | 61 | @ocaml.doc(" 62 | `catch(promise, errorCallback)` registers an exception handler in a promise chain. 63 | The `errorCallback` receives an `exn` value that can later be refined into a JS error or ReScript 64 | error. The `errorCallback` needs to return a promise with the same type as the consumed promise. 65 | 66 | ```rescript 67 | open Promise 68 | 69 | exception SomeError(string) 70 | 71 | reject(SomeError(\"this is an error\")) 72 | ->then(_ => { 73 | Ok(\"This result will never be returned\")->resolve 74 | }) 75 | ->catch(e => { 76 | let msg = switch(e) { 77 | | SomeError(msg) => \"ReScript error occurred: \" ++ msg 78 | | JsError(obj) => 79 | switch Js.Exn.message(obj) { 80 | | Some(msg) => \"JS exception occurred: \" ++ msg 81 | | None => \"Some other JS value has been thrown\" 82 | } 83 | | _ => \"Unexpected error occurred\" 84 | } 85 | 86 | Error(msg)->resolve 87 | }) 88 | ->then(result => { 89 | switch result { 90 | | Ok(r) => Js.log2(\"Operation successful: \", r) 91 | | Error(msg) => Js.log2(\"Operation failed: \", msg) 92 | }->resolve 93 | }) 94 | ->ignore // Ignore needed for side-effects 95 | ``` 96 | 97 | In case you want to return another promise in your `callback`, consider using \`then\` instead. 98 | ") 99 | let catch: (t<'a>, exn => t<'a>) => t<'a> 100 | 101 | @ocaml.doc(" 102 | `then(promise, callback)` returns a new promise based on the result of `promise`'s value. 103 | The `callback` needs to explicitly return a new promise via `resolve`. 104 | 105 | It is **not allowed** to resolve a nested promise (like `resolve(resolve(1))`). 106 | 107 | ```rescript 108 | Promise.resolve(5) 109 | ->then(num => { 110 | resolve(num + 5) 111 | }) 112 | ->then(num => { 113 | Js.log2(\"Your lucky number is: \", num) 114 | resolve() 115 | }) 116 | ->ignore 117 | ``` 118 | ") 119 | @bs.send 120 | external then: (t<'a>, @uncurry ('a => t<'b>)) => t<'b> = "then" 121 | 122 | @ocaml.doc(" 123 | `thenResolve(promise, callback)` converts an encapsulated value of a promise into another promise wrapped value. 124 | 125 | It is **not allowed** to return a promise within the provided callback (e.g. `thenResolve(value => resolve(value))`). 126 | 127 | ```rescript 128 | resolve(\"Anna\") 129 | ->thenResolve(str => { 130 | \"Hello \" ++ str 131 | }) 132 | ->thenResolve(str => { 133 | Js.log(str) 134 | }) 135 | ->ignore // Ignore needed for side-effects 136 | ``` 137 | 138 | In case you want to return another promise in your `callback`, consider using \`then\` instead. 139 | ") 140 | @bs.send 141 | external thenResolve: (t<'a>, @uncurry ('a => 'b)) => t<'b> = "then" 142 | 143 | @ocaml.doc(" 144 | [finally(promise, callback)] is used to execute a function that is called no matter if a promise 145 | was resolved or rejected. It will return the same `promise` it originally received. 146 | 147 | ```rescript 148 | exception SomeError(string) 149 | let isDone = ref(false) 150 | 151 | resolve(5) 152 | ->then(_ => { 153 | reject(TestError(\"test\")) 154 | }) 155 | ->then(v => { 156 | Js.log2(\"final result\", v) 157 | resolve() 158 | }) 159 | ->catch(_ => { 160 | Js.log(\"Error handled\") 161 | resolve() 162 | }) 163 | ->finally(() => { 164 | Js.log(\"finally\") 165 | isDone := true 166 | }) 167 | ->then(() => { 168 | Js.log2(\"isDone:\", isDone.contents) 169 | resolve() 170 | }) 171 | ->ignore 172 | ``` 173 | ") 174 | @bs.send 175 | external finally: (t<'a>, unit => unit) => t<'a> = "finally" 176 | 177 | /* Combining promises. */ 178 | @bs.scope("Promise") @bs.val 179 | external race: array> => t<'a> = "race" 180 | 181 | @ocaml.doc(" 182 | [all(promises)] runs all promises in parallel and returns a new promise resolving all gathered results in a unified array. 183 | 184 | ```rescript 185 | open Promise 186 | let promises = [resolve(1), resolve(2), resolve(3)] 187 | 188 | all(promises) 189 | ->then((results) => { 190 | Belt.Array.forEach(results, (num) => { 191 | Js.log2(\"Number: \", num) 192 | }) 193 | 194 | resolve() 195 | }) 196 | ->ignore 197 | ``` 198 | ") 199 | @bs.scope("Promise") 200 | @bs.val 201 | external all: array> => t> = "all" 202 | 203 | @ocaml.doc(" 204 | [all2((p1, p2))]. Like `all()`, but with a fixed size tuple of 2 205 | ") 206 | @bs.scope("Promise") 207 | @bs.val 208 | external all2: ((t<'a>, t<'b>)) => t<('a, 'b)> = "all" 209 | 210 | @ocaml.doc(" 211 | [all3((p1, p2, p3))]. Like `all()`, but with a fixed size tuple of 3 212 | ") 213 | @bs.scope("Promise") 214 | @bs.val 215 | external all3: ((t<'a>, t<'b>, t<'c>)) => t<('a, 'b, 'c)> = "all" 216 | 217 | @ocaml.doc(" 218 | [all4((p1, p2, p3, p4))]. Like `all()`, but with a fixed size tuple of 4 219 | ") 220 | @bs.scope("Promise") 221 | @bs.val 222 | external all4: ((t<'a>, t<'b>, t<'c>, t<'d>)) => t<('a, 'b, 'c, 'd)> = "all" 223 | 224 | @ocaml.doc(" 225 | [all5((p1, p2, p3, p4, p5))]. Like `all()`, but with a fixed size tuple of 5 226 | ") 227 | @bs.scope("Promise") 228 | @bs.val 229 | external all5: ((t<'a>, t<'b>, t<'c>, t<'d>, t<'e>)) => t<('a, 'b, 'c, 'd, 'e)> = "all" 230 | 231 | @ocaml.doc(" 232 | [all6((p1, p2, p4, p5, p6))]. Like `all()`, but with a fixed size tuple of 6 233 | ") 234 | @bs.scope("Promise") 235 | @bs.val 236 | external all6: ((t<'a>, t<'b>, t<'c>, t<'d>, t<'e>, t<'f>)) => t<('a, 'b, 'c, 'd, 'e, 'f)> = "all" 237 | -------------------------------------------------------------------------------- /tests/PromiseTest.js: -------------------------------------------------------------------------------- 1 | // Generated by ReScript, PLEASE EDIT WITH CARE 2 | 'use strict'; 3 | 4 | var Test = require("./Test.js"); 5 | var Curry = require("bs-platform/lib/js/curry.js"); 6 | var Js_exn = require("bs-platform/lib/js/js_exn.js"); 7 | var $$Promise = require("../src/Promise.js"); 8 | var Caml_obj = require("bs-platform/lib/js/caml_obj.js"); 9 | var Caml_exceptions = require("bs-platform/lib/js/caml_exceptions.js"); 10 | 11 | var TestError = Caml_exceptions.create("PromiseTest.TestError"); 12 | 13 | var fail = Js_exn.raiseError; 14 | 15 | var equal = Caml_obj.caml_equal; 16 | 17 | function resolveTest(param) { 18 | Promise.resolve("test").then(function (str) { 19 | Test.run([ 20 | [ 21 | "PromiseTest.res", 22 | 17, 23 | 26, 24 | 47 25 | ], 26 | "Should resolve test" 27 | ], str, equal, "test"); 28 | return Promise.resolve(undefined); 29 | }); 30 | 31 | } 32 | 33 | function runTests(param) { 34 | return resolveTest(undefined); 35 | } 36 | 37 | var Creation = { 38 | resolveTest: resolveTest, 39 | runTests: runTests 40 | }; 41 | 42 | function testThen(param) { 43 | return Promise.resolve(1).then(function (first) { 44 | return Promise.resolve(first + 1 | 0); 45 | }).then(function (value) { 46 | Test.run([ 47 | [ 48 | "PromiseTest.res", 49 | 39, 50 | 26, 51 | 39 52 | ], 53 | "Should be 2" 54 | ], value, equal, 2); 55 | return Promise.resolve(undefined); 56 | }); 57 | } 58 | 59 | function testInvalidThen(param) { 60 | return $$Promise.$$catch(Promise.resolve(1).then(function (first) { 61 | return Promise.resolve(Promise.resolve(first + 1 | 0)); 62 | }).then(function (p) { 63 | p.then(function (value) { 64 | Test.run([ 65 | [ 66 | "PromiseTest.res", 67 | 55, 68 | 28, 69 | 41 70 | ], 71 | "Should be 2" 72 | ], value, equal, 2); 73 | return Promise.resolve(undefined); 74 | }); 75 | return Promise.resolve(undefined); 76 | }), (function (e) { 77 | var ret = e.RE_EXN_ID === $$Promise.JsError ? e._1.message === "p.then is not a function" : false; 78 | Test.run([ 79 | [ 80 | "PromiseTest.res", 81 | 66, 82 | 26, 83 | 60 84 | ], 85 | "then should have thrown an error" 86 | ], ret, equal, true); 87 | return Promise.resolve(undefined); 88 | })); 89 | } 90 | 91 | function testThenResolve(param) { 92 | return Promise.resolve(1).then(function (num) { 93 | return num + 1 | 0; 94 | }).then(function (ret) { 95 | return Test.run([ 96 | [ 97 | "PromiseTest.res", 98 | 79, 99 | 26, 100 | 39 101 | ], 102 | "Should be 2" 103 | ], ret, equal, 2); 104 | }); 105 | } 106 | 107 | function testInvalidThenResolve(param) { 108 | return $$Promise.$$catch(Promise.resolve(1).then(function (num) { 109 | return Promise.resolve(num); 110 | }).then(function (p) { 111 | p.then(function (num) { 112 | return num + 1 | 0; 113 | }); 114 | return Promise.resolve(undefined); 115 | }), (function (e) { 116 | var ret = e.RE_EXN_ID === $$Promise.JsError ? e._1.message === "p.then is not a function" : false; 117 | Test.run([ 118 | [ 119 | "PromiseTest.res", 120 | 105, 121 | 26, 122 | 60 123 | ], 124 | "then should have thrown an error" 125 | ], ret, equal, true); 126 | return Promise.resolve(undefined); 127 | })); 128 | } 129 | 130 | function runTests$1(param) { 131 | testThen(undefined); 132 | testInvalidThen(undefined); 133 | testThenResolve(undefined); 134 | testInvalidThenResolve(undefined); 135 | 136 | } 137 | 138 | var ThenChaining = { 139 | testThen: testThen, 140 | testInvalidThen: testInvalidThen, 141 | testThenResolve: testThenResolve, 142 | testInvalidThenResolve: testInvalidThenResolve, 143 | runTests: runTests$1 144 | }; 145 | 146 | function testExnRejection(param) { 147 | $$Promise.$$catch(Promise.reject({ 148 | RE_EXN_ID: TestError, 149 | _1: "oops" 150 | }), (function (e) { 151 | Test.run([ 152 | [ 153 | "PromiseTest.res", 154 | 127, 155 | 26, 156 | 30 157 | ], 158 | "Expect rejection to contain a TestError" 159 | ], e, equal, { 160 | RE_EXN_ID: TestError, 161 | _1: "oops" 162 | }); 163 | return Promise.resolve(undefined); 164 | })); 165 | 166 | } 167 | 168 | function runTests$2(param) { 169 | testExnRejection(undefined); 170 | 171 | } 172 | 173 | var Rejection = { 174 | testExnRejection: testExnRejection, 175 | runTests: runTests$2 176 | }; 177 | 178 | var asyncParseFail = (function() { 179 | return new Promise((resolve) => { 180 | var result = JSON.parse("{.."); 181 | return resolve(result); 182 | }) 183 | }); 184 | 185 | function testExternalPromiseThrow(param) { 186 | return $$Promise.$$catch(Curry._1(asyncParseFail, undefined).then(function (param) { 187 | return Promise.resolve(undefined); 188 | }), (function (e) { 189 | var success = e.RE_EXN_ID === $$Promise.JsError ? Caml_obj.caml_equal(e._1.message, "Unexpected token . in JSON at position 1") : false; 190 | Test.run([ 191 | [ 192 | "PromiseTest.res", 193 | 161, 194 | 26, 195 | 76 196 | ], 197 | "Should be a parser error with Unexpected token ." 198 | ], success, equal, true); 199 | return Promise.resolve(undefined); 200 | })); 201 | } 202 | 203 | function testExnThrow(param) { 204 | return $$Promise.$$catch(Promise.resolve(undefined).then(function (param) { 205 | throw { 206 | RE_EXN_ID: TestError, 207 | _1: "Thrown exn", 208 | Error: new Error() 209 | }; 210 | }), (function (e) { 211 | var isTestErr = e.RE_EXN_ID === TestError && e._1 === "Thrown exn" ? true : false; 212 | Test.run([ 213 | [ 214 | "PromiseTest.res", 215 | 180, 216 | 26, 217 | 49 218 | ], 219 | "Should be a TestError" 220 | ], isTestErr, equal, true); 221 | return Promise.resolve(undefined); 222 | })); 223 | } 224 | 225 | function testRaiseErrorThrow(param) { 226 | return $$Promise.$$catch(Promise.resolve(undefined).then(function (param) { 227 | return Js_exn.raiseError("Some JS error"); 228 | }), (function (e) { 229 | var isTestErr = e.RE_EXN_ID === $$Promise.JsError ? Caml_obj.caml_equal(e._1.message, "Some JS error") : false; 230 | Test.run([ 231 | [ 232 | "PromiseTest.res", 233 | 203, 234 | 26, 235 | 51 236 | ], 237 | "Should be some JS error" 238 | ], isTestErr, equal, true); 239 | return Promise.resolve(undefined); 240 | })); 241 | } 242 | 243 | function thenAfterCatch(param) { 244 | return $$Promise.$$catch(Promise.resolve(undefined).then(function (param) { 245 | return Promise.reject({ 246 | RE_EXN_ID: TestError, 247 | _1: "some rejected value" 248 | }); 249 | }), (function (e) { 250 | var tmp; 251 | tmp = e.RE_EXN_ID === TestError && e._1 === "some rejected value" ? "success" : "not a test error"; 252 | return Promise.resolve(tmp); 253 | })).then(function (msg) { 254 | Test.run([ 255 | [ 256 | "PromiseTest.res", 257 | 226, 258 | 26, 259 | 45 260 | ], 261 | "Should be success" 262 | ], msg, equal, "success"); 263 | return Promise.resolve(undefined); 264 | }); 265 | } 266 | 267 | function testCatchFinally(param) { 268 | var wasCalled = { 269 | contents: false 270 | }; 271 | $$Promise.$$catch(Promise.resolve(5).then(function (param) { 272 | return Promise.reject({ 273 | RE_EXN_ID: TestError, 274 | _1: "test" 275 | }); 276 | }).then(function (v) { 277 | return Promise.resolve(v); 278 | }), (function (param) { 279 | return Promise.resolve(undefined); 280 | })).finally(function (param) { 281 | wasCalled.contents = true; 282 | 283 | }).then(function (v) { 284 | Test.run([ 285 | [ 286 | "PromiseTest.res", 287 | 248, 288 | 26, 289 | 48 290 | ], 291 | "value should be unit" 292 | ], v, equal, undefined); 293 | Test.run([ 294 | [ 295 | "PromiseTest.res", 296 | 249, 297 | 26, 298 | 59 299 | ], 300 | "finally should have been called" 301 | ], wasCalled.contents, equal, true); 302 | return Promise.resolve(undefined); 303 | }); 304 | 305 | } 306 | 307 | function testResolveFinally(param) { 308 | var wasCalled = { 309 | contents: false 310 | }; 311 | Promise.resolve(5).then(function (v) { 312 | return Promise.resolve(v + 5 | 0); 313 | }).finally(function (param) { 314 | wasCalled.contents = true; 315 | 316 | }).then(function (v) { 317 | Test.run([ 318 | [ 319 | "PromiseTest.res", 320 | 266, 321 | 26, 322 | 45 323 | ], 324 | "value should be 5" 325 | ], v, equal, 10); 326 | Test.run([ 327 | [ 328 | "PromiseTest.res", 329 | 267, 330 | 26, 331 | 59 332 | ], 333 | "finally should have been called" 334 | ], wasCalled.contents, equal, true); 335 | return Promise.resolve(undefined); 336 | }); 337 | 338 | } 339 | 340 | function runTests$3(param) { 341 | testExternalPromiseThrow(undefined); 342 | testExnThrow(undefined); 343 | testRaiseErrorThrow(undefined); 344 | thenAfterCatch(undefined); 345 | testCatchFinally(undefined); 346 | testResolveFinally(undefined); 347 | 348 | } 349 | 350 | var Catching = { 351 | asyncParseFail: asyncParseFail, 352 | testExternalPromiseThrow: testExternalPromiseThrow, 353 | testExnThrow: testExnThrow, 354 | testRaiseErrorThrow: testRaiseErrorThrow, 355 | thenAfterCatch: thenAfterCatch, 356 | testCatchFinally: testCatchFinally, 357 | testResolveFinally: testResolveFinally, 358 | runTests: runTests$3 359 | }; 360 | 361 | function testParallel(param) { 362 | var place = { 363 | contents: 0 364 | }; 365 | var delayedMsg = function (ms, msg) { 366 | return new Promise((function (resolve, param) { 367 | setTimeout((function (param) { 368 | place.contents = place.contents + 1 | 0; 369 | return resolve([ 370 | place.contents, 371 | msg 372 | ]); 373 | }), ms); 374 | 375 | })); 376 | }; 377 | var p1 = delayedMsg(1000, "is Anna"); 378 | var p2 = delayedMsg(500, "myName"); 379 | var p3 = delayedMsg(100, "Hi"); 380 | return Promise.all([ 381 | p1, 382 | p2, 383 | p3 384 | ]).then(function (arr) { 385 | var exp = [ 386 | [ 387 | 3, 388 | "is Anna" 389 | ], 390 | [ 391 | 2, 392 | "myName" 393 | ], 394 | [ 395 | 1, 396 | "Hi" 397 | ] 398 | ]; 399 | Test.run([ 400 | [ 401 | "PromiseTest.res", 402 | 304, 403 | 26, 404 | 55 405 | ], 406 | "Should have correct placing" 407 | ], arr, equal, exp); 408 | return Promise.resolve(undefined); 409 | }); 410 | } 411 | 412 | function testRace(param) { 413 | var racer = function (ms, name) { 414 | return new Promise((function (resolve, param) { 415 | setTimeout((function (param) { 416 | return resolve(name); 417 | }), ms); 418 | 419 | })); 420 | }; 421 | var promises = [ 422 | racer(1000, "Turtle"), 423 | racer(500, "Hare"), 424 | racer(100, "Eagle") 425 | ]; 426 | return Promise.race(promises).then(function (winner) { 427 | Test.run([ 428 | [ 429 | "PromiseTest.res", 430 | 323, 431 | 26, 432 | 44 433 | ], 434 | "Eagle should win" 435 | ], winner, equal, "Eagle"); 436 | return Promise.resolve(undefined); 437 | }); 438 | } 439 | 440 | function testParallel2(param) { 441 | var place = { 442 | contents: 0 443 | }; 444 | var delayedMsg = function (ms, msg) { 445 | return new Promise((function (resolve, param) { 446 | setTimeout((function (param) { 447 | place.contents = place.contents + 1 | 0; 448 | return resolve([ 449 | place.contents, 450 | msg 451 | ]); 452 | }), ms); 453 | 454 | })); 455 | }; 456 | var p1 = delayedMsg(1000, "is Anna"); 457 | var p2 = delayedMsg(500, "myName"); 458 | return Promise.all([ 459 | p1, 460 | p2 461 | ]).then(function (arr) { 462 | Test.run([ 463 | [ 464 | "PromiseTest.res", 465 | 347, 466 | 26, 467 | 55 468 | ], 469 | "Should have correct placing" 470 | ], arr, equal, [ 471 | [ 472 | 2, 473 | "is Anna" 474 | ], 475 | [ 476 | 1, 477 | "myName" 478 | ] 479 | ]); 480 | return Promise.resolve(undefined); 481 | }); 482 | } 483 | 484 | function testParallel3(param) { 485 | var place = { 486 | contents: 0 487 | }; 488 | var delayedMsg = function (ms, msg) { 489 | return new Promise((function (resolve, param) { 490 | setTimeout((function (param) { 491 | place.contents = place.contents + 1 | 0; 492 | return resolve([ 493 | place.contents, 494 | msg 495 | ]); 496 | }), ms); 497 | 498 | })); 499 | }; 500 | var p1 = delayedMsg(1000, "is Anna"); 501 | var p2 = delayedMsg(500, "myName"); 502 | var p3 = delayedMsg(100, "Hi"); 503 | return Promise.all([ 504 | p1, 505 | p2, 506 | p3 507 | ]).then(function (arr) { 508 | Test.run([ 509 | [ 510 | "PromiseTest.res", 511 | 372, 512 | 26, 513 | 55 514 | ], 515 | "Should have correct placing" 516 | ], arr, equal, [ 517 | [ 518 | 3, 519 | "is Anna" 520 | ], 521 | [ 522 | 2, 523 | "myName" 524 | ], 525 | [ 526 | 1, 527 | "Hi" 528 | ] 529 | ]); 530 | return Promise.resolve(undefined); 531 | }); 532 | } 533 | 534 | function testParallel4(param) { 535 | var place = { 536 | contents: 0 537 | }; 538 | var delayedMsg = function (ms, msg) { 539 | return new Promise((function (resolve, param) { 540 | setTimeout((function (param) { 541 | place.contents = place.contents + 1 | 0; 542 | return resolve([ 543 | place.contents, 544 | msg 545 | ]); 546 | }), ms); 547 | 548 | })); 549 | }; 550 | var p1 = delayedMsg(1500, "Anna"); 551 | var p2 = delayedMsg(1000, "is"); 552 | var p3 = delayedMsg(500, "my name"); 553 | var p4 = delayedMsg(100, "Hi"); 554 | return Promise.all([ 555 | p1, 556 | p2, 557 | p3, 558 | p4 559 | ]).then(function (arr) { 560 | Test.run([ 561 | [ 562 | "PromiseTest.res", 563 | 398, 564 | 26, 565 | 55 566 | ], 567 | "Should have correct placing" 568 | ], arr, equal, [ 569 | [ 570 | 4, 571 | "Anna" 572 | ], 573 | [ 574 | 3, 575 | "is" 576 | ], 577 | [ 578 | 2, 579 | "my name" 580 | ], 581 | [ 582 | 1, 583 | "Hi" 584 | ] 585 | ]); 586 | return Promise.resolve(undefined); 587 | }); 588 | } 589 | 590 | function testParallel5(param) { 591 | var place = { 592 | contents: 0 593 | }; 594 | var delayedMsg = function (ms, msg) { 595 | return new Promise((function (resolve, param) { 596 | setTimeout((function (param) { 597 | place.contents = place.contents + 1 | 0; 598 | return resolve([ 599 | place.contents, 600 | msg 601 | ]); 602 | }), ms); 603 | 604 | })); 605 | }; 606 | var p1 = delayedMsg(1500, "Anna"); 607 | var p2 = delayedMsg(1000, "is"); 608 | var p3 = delayedMsg(500, "name"); 609 | var p4 = delayedMsg(100, "my"); 610 | var p5 = delayedMsg(50, "Hi"); 611 | return Promise.all([ 612 | p1, 613 | p2, 614 | p3, 615 | p4, 616 | p5 617 | ]).then(function (arr) { 618 | Test.run([ 619 | [ 620 | "PromiseTest.res", 621 | 425, 622 | 26, 623 | 55 624 | ], 625 | "Should have correct placing" 626 | ], arr, equal, [ 627 | [ 628 | 5, 629 | "Anna" 630 | ], 631 | [ 632 | 4, 633 | "is" 634 | ], 635 | [ 636 | 3, 637 | "name" 638 | ], 639 | [ 640 | 2, 641 | "my" 642 | ], 643 | [ 644 | 1, 645 | "Hi" 646 | ] 647 | ]); 648 | return Promise.resolve(undefined); 649 | }); 650 | } 651 | 652 | function testParallel6(param) { 653 | var place = { 654 | contents: 0 655 | }; 656 | var delayedMsg = function (ms, msg) { 657 | return new Promise((function (resolve, param) { 658 | setTimeout((function (param) { 659 | place.contents = place.contents + 1 | 0; 660 | return resolve([ 661 | place.contents, 662 | msg 663 | ]); 664 | }), ms); 665 | 666 | })); 667 | }; 668 | var p1 = delayedMsg(1500, "Anna"); 669 | var p2 = delayedMsg(1000, "is"); 670 | var p3 = delayedMsg(500, "name"); 671 | var p4 = delayedMsg(100, "my"); 672 | var p5 = delayedMsg(50, ", "); 673 | var p6 = delayedMsg(10, "Hi"); 674 | return Promise.all([ 675 | p1, 676 | p2, 677 | p3, 678 | p4, 679 | p5, 680 | p6 681 | ]).then(function (arr) { 682 | Test.run([ 683 | [ 684 | "PromiseTest.res", 685 | 453, 686 | 26, 687 | 55 688 | ], 689 | "Should have correct placing" 690 | ], arr, equal, [ 691 | [ 692 | 6, 693 | "Anna" 694 | ], 695 | [ 696 | 5, 697 | "is" 698 | ], 699 | [ 700 | 4, 701 | "name" 702 | ], 703 | [ 704 | 3, 705 | "my" 706 | ], 707 | [ 708 | 2, 709 | ", " 710 | ], 711 | [ 712 | 1, 713 | "Hi" 714 | ] 715 | ]); 716 | return Promise.resolve(undefined); 717 | }); 718 | } 719 | 720 | function runTests$4(param) { 721 | testParallel(undefined); 722 | testRace(undefined); 723 | testParallel2(undefined); 724 | testParallel3(undefined); 725 | testParallel4(undefined); 726 | testParallel5(undefined); 727 | testParallel6(undefined); 728 | 729 | } 730 | 731 | var Concurrently = { 732 | testParallel: testParallel, 733 | testRace: testRace, 734 | testParallel2: testParallel2, 735 | testParallel3: testParallel3, 736 | testParallel4: testParallel4, 737 | testParallel5: testParallel5, 738 | testParallel6: testParallel6, 739 | runTests: runTests$4 740 | }; 741 | 742 | resolveTest(undefined); 743 | 744 | runTests$1(undefined); 745 | 746 | testExnRejection(undefined); 747 | 748 | runTests$3(undefined); 749 | 750 | runTests$4(undefined); 751 | 752 | exports.TestError = TestError; 753 | exports.fail = fail; 754 | exports.equal = equal; 755 | exports.Creation = Creation; 756 | exports.ThenChaining = ThenChaining; 757 | exports.Rejection = Rejection; 758 | exports.Catching = Catching; 759 | exports.Concurrently = Concurrently; 760 | /* Not a pure module */ 761 | -------------------------------------------------------------------------------- /tests/PromiseTest.res: -------------------------------------------------------------------------------- 1 | exception TestError(string) 2 | 3 | let fail = msg => { 4 | Js.Exn.raiseError(msg) 5 | } 6 | 7 | let equal = (a, b) => { 8 | a == b 9 | } 10 | 11 | module Creation = { 12 | let resolveTest = () => { 13 | open Promise 14 | 15 | Promise.resolve("test") 16 | ->then(str => { 17 | Test.run(__POS_OF__("Should resolve test"), str, equal, "test") 18 | resolve() 19 | }) 20 | ->ignore 21 | } 22 | 23 | let runTests = () => { 24 | resolveTest() 25 | } 26 | } 27 | 28 | module ThenChaining = { 29 | // A promise should be able to return a nested 30 | // Promise and also flatten it for another then call 31 | // to the actual value 32 | let testThen = () => { 33 | open Promise 34 | resolve(1) 35 | ->then(first => { 36 | resolve(first + 1) 37 | }) 38 | ->then(value => { 39 | Test.run(__POS_OF__("Should be 2"), value, equal, 2) 40 | resolve() 41 | }) 42 | } 43 | 44 | // It's not allowed to return a Promise.t> value 45 | // within a then. This operation will throw an error 46 | let testInvalidThen = () => { 47 | open Promise 48 | resolve(1) 49 | ->then(first => { 50 | resolve(resolve(first + 1)) 51 | }) 52 | ->then(p => { 53 | p 54 | ->then(value => { 55 | Test.run(__POS_OF__("Should be 2"), value, equal, 2) 56 | resolve() 57 | }) 58 | ->ignore 59 | resolve() 60 | }) 61 | ->catch(e => { 62 | let ret = switch e { 63 | | JsError(m) => Js.Exn.message(m) === Some("p.then is not a function") 64 | | _ => false 65 | } 66 | Test.run(__POS_OF__("then should have thrown an error"), ret, equal, true) 67 | resolve() 68 | }) 69 | } 70 | 71 | let testThenResolve = () => { 72 | open Promise 73 | 74 | resolve(1) 75 | ->thenResolve(num => { 76 | num + 1 77 | }) 78 | ->thenResolve(ret => { 79 | Test.run(__POS_OF__("Should be 2"), ret, equal, 2) 80 | }) 81 | } 82 | 83 | let testInvalidThenResolve = () => { 84 | open Promise 85 | 86 | resolve(1) 87 | ->thenResolve(num => { 88 | // This is against the law 89 | resolve(num) 90 | }) 91 | ->then(p => { 92 | // This will throw because of the auto-collapsing of promises 93 | p 94 | ->thenResolve(num => { 95 | num + 1 96 | }) 97 | ->ignore 98 | resolve() 99 | }) 100 | ->catch(e => { 101 | let ret = switch e { 102 | | JsError(m) => Js.Exn.message(m) === Some("p.then is not a function") 103 | | _ => false 104 | } 105 | Test.run(__POS_OF__("then should have thrown an error"), ret, equal, true) 106 | resolve() 107 | }) 108 | } 109 | 110 | let runTests = () => { 111 | testThen()->ignore 112 | testInvalidThen()->ignore 113 | testThenResolve()->ignore 114 | testInvalidThenResolve()->ignore 115 | } 116 | } 117 | 118 | module Rejection = { 119 | // Should gracefully handle a exn passed via reject() 120 | let testExnRejection = () => { 121 | let cond = "Expect rejection to contain a TestError" 122 | open Promise 123 | 124 | TestError("oops") 125 | ->reject 126 | ->catch(e => { 127 | Test.run(__POS_OF__(cond), e, equal, TestError("oops")) 128 | resolve() 129 | }) 130 | ->ignore 131 | } 132 | 133 | let runTests = () => { 134 | testExnRejection()->ignore 135 | } 136 | } 137 | 138 | module Catching = { 139 | let asyncParseFail: unit => Js.Promise.t = %raw(` 140 | function() { 141 | return new Promise((resolve) => { 142 | var result = JSON.parse("{.."); 143 | return resolve(result); 144 | }) 145 | } 146 | `) 147 | 148 | // Should correctly capture an JS error thrown within 149 | // a Promise `then` function 150 | let testExternalPromiseThrow = () => { 151 | open Promise 152 | 153 | asyncParseFail() 154 | ->then(_ => resolve()) // Since our asyncParse will fail anyways, we convert to Promise.t for our catch later 155 | ->catch(e => { 156 | let success = switch e { 157 | | JsError(err) => Js.Exn.message(err) == Some("Unexpected token . in JSON at position 1") 158 | | _ => false 159 | } 160 | 161 | Test.run(__POS_OF__("Should be a parser error with Unexpected token ."), success, equal, true) 162 | resolve() 163 | }) 164 | } 165 | 166 | // Should correctly capture an exn thrown in a Promise 167 | // `then` function 168 | let testExnThrow = () => { 169 | open Promise 170 | 171 | resolve() 172 | ->then(_ => { 173 | raise(TestError("Thrown exn")) 174 | }) 175 | ->catch(e => { 176 | let isTestErr = switch e { 177 | | TestError("Thrown exn") => true 178 | | _ => false 179 | } 180 | Test.run(__POS_OF__("Should be a TestError"), isTestErr, equal, true) 181 | resolve() 182 | }) 183 | } 184 | 185 | // Should correctly capture a JS error raised with Js.Exn.raiseError 186 | // within a Promise then function 187 | let testRaiseErrorThrow = () => { 188 | open Promise 189 | 190 | let causeErr = () => { 191 | Js.Exn.raiseError("Some JS error") 192 | } 193 | 194 | resolve() 195 | ->then(_ => { 196 | causeErr() 197 | }) 198 | ->catch(e => { 199 | let isTestErr = switch e { 200 | | JsError(err) => Js.Exn.message(err) == Some("Some JS error") 201 | | _ => false 202 | } 203 | Test.run(__POS_OF__("Should be some JS error"), isTestErr, equal, true) 204 | resolve() 205 | }) 206 | } 207 | 208 | // Should recover a rejection and use then to 209 | // access the value 210 | let thenAfterCatch = () => { 211 | open Promise 212 | resolve() 213 | ->then(_ => { 214 | // NOTE: if then is used, there will be an uncaught 215 | // error 216 | reject(TestError("some rejected value")) 217 | }) 218 | ->catch(e => { 219 | let s = switch e { 220 | | TestError("some rejected value") => "success" 221 | | _ => "not a test error" 222 | } 223 | resolve(s) 224 | }) 225 | ->then(msg => { 226 | Test.run(__POS_OF__("Should be success"), msg, equal, "success") 227 | resolve() 228 | }) 229 | } 230 | 231 | let testCatchFinally = () => { 232 | open Promise 233 | let wasCalled = ref(false) 234 | resolve(5) 235 | ->then(_ => { 236 | reject(TestError("test")) 237 | }) 238 | ->then(v => { 239 | v->resolve 240 | }) 241 | ->catch(_ => { 242 | resolve() 243 | }) 244 | ->finally(() => { 245 | wasCalled := true 246 | }) 247 | ->then(v => { 248 | Test.run(__POS_OF__("value should be unit"), v, equal, ()) 249 | Test.run(__POS_OF__("finally should have been called"), wasCalled.contents, equal, true) 250 | resolve() 251 | }) 252 | ->ignore 253 | } 254 | 255 | let testResolveFinally = () => { 256 | open Promise 257 | let wasCalled = ref(false) 258 | resolve(5) 259 | ->then(v => { 260 | resolve(v + 5) 261 | }) 262 | ->finally(() => { 263 | wasCalled := true 264 | }) 265 | ->then(v => { 266 | Test.run(__POS_OF__("value should be 5"), v, equal, 10) 267 | Test.run(__POS_OF__("finally should have been called"), wasCalled.contents, equal, true) 268 | resolve() 269 | }) 270 | ->ignore 271 | } 272 | 273 | let runTests = () => { 274 | testExternalPromiseThrow()->ignore 275 | testExnThrow()->ignore 276 | testRaiseErrorThrow()->ignore 277 | thenAfterCatch()->ignore 278 | testCatchFinally()->ignore 279 | testResolveFinally()->ignore 280 | } 281 | } 282 | 283 | module Concurrently = { 284 | let testParallel = () => { 285 | open Promise 286 | 287 | let place = ref(0) 288 | 289 | let delayedMsg = (ms, msg) => { 290 | Promise.make((resolve, _) => { 291 | Js.Global.setTimeout(() => { 292 | place := place.contents + 1 293 | resolve(.(place.contents, msg)) 294 | }, ms)->ignore 295 | }) 296 | } 297 | 298 | let p1 = delayedMsg(1000, "is Anna") 299 | let p2 = delayedMsg(500, "myName") 300 | let p3 = delayedMsg(100, "Hi") 301 | 302 | all([p1, p2, p3])->then(arr => { 303 | let exp = [(3, "is Anna"), (2, "myName"), (1, "Hi")] 304 | Test.run(__POS_OF__("Should have correct placing"), arr, equal, exp) 305 | resolve() 306 | }) 307 | } 308 | 309 | let testRace = () => { 310 | open Promise 311 | 312 | let racer = (ms, name) => { 313 | Promise.make((resolve, _) => { 314 | Js.Global.setTimeout(() => { 315 | resolve(. name) 316 | }, ms)->ignore 317 | }) 318 | } 319 | 320 | let promises = [racer(1000, "Turtle"), racer(500, "Hare"), racer(100, "Eagle")] 321 | 322 | race(promises)->then(winner => { 323 | Test.run(__POS_OF__("Eagle should win"), winner, equal, "Eagle") 324 | resolve() 325 | }) 326 | } 327 | 328 | let testParallel2 = () => { 329 | open Promise 330 | 331 | let place = ref(0) 332 | 333 | let delayedMsg = (ms, msg) => { 334 | Promise.make((resolve, _) => { 335 | Js.Global.setTimeout(() => { 336 | place := place.contents + 1 337 | resolve(.(place.contents, msg)) 338 | }, ms)->ignore 339 | }) 340 | } 341 | 342 | let p1 = delayedMsg(1000, "is Anna") 343 | let p2 = delayedMsg(500, "myName") 344 | 345 | all2((p1, p2))->then(arr => { 346 | let exp = ((2, "is Anna"), (1, "myName")) 347 | Test.run(__POS_OF__("Should have correct placing"), arr, equal, exp) 348 | resolve() 349 | }) 350 | } 351 | 352 | let testParallel3 = () => { 353 | open Promise 354 | 355 | let place = ref(0) 356 | 357 | let delayedMsg = (ms, msg) => { 358 | Promise.make((resolve, _) => { 359 | Js.Global.setTimeout(() => { 360 | place := place.contents + 1 361 | resolve(.(place.contents, msg)) 362 | }, ms)->ignore 363 | }) 364 | } 365 | 366 | let p1 = delayedMsg(1000, "is Anna") 367 | let p2 = delayedMsg(500, "myName") 368 | let p3 = delayedMsg(100, "Hi") 369 | 370 | all3((p1, p2, p3))->then(arr => { 371 | let exp = ((3, "is Anna"), (2, "myName"), (1, "Hi")) 372 | Test.run(__POS_OF__("Should have correct placing"), arr, equal, exp) 373 | resolve() 374 | }) 375 | } 376 | 377 | let testParallel4 = () => { 378 | open Promise 379 | 380 | let place = ref(0) 381 | 382 | let delayedMsg = (ms, msg) => { 383 | Promise.make((resolve, _) => { 384 | Js.Global.setTimeout(() => { 385 | place := place.contents + 1 386 | resolve(.(place.contents, msg)) 387 | }, ms)->ignore 388 | }) 389 | } 390 | 391 | let p1 = delayedMsg(1500, "Anna") 392 | let p2 = delayedMsg(1000, "is") 393 | let p3 = delayedMsg(500, "my name") 394 | let p4 = delayedMsg(100, "Hi") 395 | 396 | all4((p1, p2, p3, p4))->then(arr => { 397 | let exp = ((4, "Anna"), (3, "is"), (2, "my name"), (1, "Hi")) 398 | Test.run(__POS_OF__("Should have correct placing"), arr, equal, exp) 399 | resolve() 400 | }) 401 | } 402 | 403 | let testParallel5 = () => { 404 | open Promise 405 | 406 | let place = ref(0) 407 | 408 | let delayedMsg = (ms, msg) => { 409 | Promise.make((resolve, _) => { 410 | Js.Global.setTimeout(() => { 411 | place := place.contents + 1 412 | resolve(.(place.contents, msg)) 413 | }, ms)->ignore 414 | }) 415 | } 416 | 417 | let p1 = delayedMsg(1500, "Anna") 418 | let p2 = delayedMsg(1000, "is") 419 | let p3 = delayedMsg(500, "name") 420 | let p4 = delayedMsg(100, "my") 421 | let p5 = delayedMsg(50, "Hi") 422 | 423 | all5((p1, p2, p3, p4, p5))->then(arr => { 424 | let exp = ((5, "Anna"), (4, "is"), (3, "name"), (2, "my"), (1, "Hi")) 425 | Test.run(__POS_OF__("Should have correct placing"), arr, equal, exp) 426 | resolve() 427 | }) 428 | } 429 | 430 | let testParallel6 = () => { 431 | open Promise 432 | 433 | let place = ref(0) 434 | 435 | let delayedMsg = (ms, msg) => { 436 | Promise.make((resolve, _) => { 437 | Js.Global.setTimeout(() => { 438 | place := place.contents + 1 439 | resolve(.(place.contents, msg)) 440 | }, ms)->ignore 441 | }) 442 | } 443 | 444 | let p1 = delayedMsg(1500, "Anna") 445 | let p2 = delayedMsg(1000, "is") 446 | let p3 = delayedMsg(500, "name") 447 | let p4 = delayedMsg(100, "my") 448 | let p5 = delayedMsg(50, ", ") 449 | let p6 = delayedMsg(10, "Hi") 450 | 451 | all6((p1, p2, p3, p4, p5, p6))->then(arr => { 452 | let exp = ((6, "Anna"), (5, "is"), (4, "name"), (3, "my"), (2, ", "), (1, "Hi")) 453 | Test.run(__POS_OF__("Should have correct placing"), arr, equal, exp) 454 | resolve() 455 | }) 456 | } 457 | 458 | let runTests = () => { 459 | testParallel()->ignore 460 | testRace()->ignore 461 | testParallel2()->ignore 462 | testParallel3()->ignore 463 | testParallel4()->ignore 464 | testParallel5()->ignore 465 | testParallel6()->ignore 466 | } 467 | } 468 | 469 | Creation.runTests() 470 | ThenChaining.runTests() 471 | Rejection.runTests() 472 | Catching.runTests() 473 | Concurrently.runTests() 474 | -------------------------------------------------------------------------------- /tests/Test.js: -------------------------------------------------------------------------------- 1 | // Generated by ReScript, PLEASE EDIT WITH CARE 2 | 'use strict'; 3 | 4 | var Fs = require("fs"); 5 | var Path = require("path"); 6 | var Curry = require("bs-platform/lib/js/curry.js"); 7 | var CodeFrame = require("@babel/code-frame"); 8 | 9 | var dirname = typeof __dirname === "undefined" ? undefined : __dirname; 10 | 11 | var dirname$1 = dirname !== undefined ? dirname : ""; 12 | 13 | function cleanUpStackTrace(stack) { 14 | var removeInternalLines = function (lines, _i) { 15 | while(true) { 16 | var i = _i; 17 | if (i >= lines.length) { 18 | return lines; 19 | } 20 | if (lines[i].indexOf(" (internal/") >= 0) { 21 | return lines.slice(0, i); 22 | } 23 | _i = i + 1 | 0; 24 | continue ; 25 | }; 26 | }; 27 | return removeInternalLines(stack.split("\n").slice(2), 0).map(function (line) { 28 | return line.slice(2); 29 | }).join("\n"); 30 | } 31 | 32 | function run(loc, left, comparator, right) { 33 | if (Curry._2(comparator, left, right)) { 34 | return ; 35 | } 36 | var match = loc[0]; 37 | var line = match[1]; 38 | var file = match[0]; 39 | var fileContent = Fs.readFileSync(Path.join(dirname$1, file), { 40 | encoding: "utf-8" 41 | }); 42 | var left$1 = JSON.stringify(left); 43 | var right$1 = JSON.stringify(right); 44 | var codeFrame = CodeFrame.codeFrameColumns(fileContent, { 45 | start: { 46 | line: line 47 | } 48 | }, { 49 | highlightCode: true 50 | }); 51 | var errorMessage = "\n \u001b[31mTest Failure!\n \u001b[36m" + file + "\u001b[0m:\u001b[2m" + line + "\n" + codeFrame + "\n \u001b[39mLeft: \u001b[31m" + left$1 + "\n \u001b[39mRight: \u001b[31m" + right$1 + "\u001b[0m\n"; 52 | console.log(errorMessage); 53 | var obj = {}; 54 | Error.captureStackTrace(obj); 55 | console.log(cleanUpStackTrace(obj.stack)); 56 | 57 | } 58 | 59 | exports.dirname = dirname$1; 60 | exports.cleanUpStackTrace = cleanUpStackTrace; 61 | exports.run = run; 62 | /* dirname Not a pure module */ 63 | -------------------------------------------------------------------------------- /tests/Test.res: -------------------------------------------------------------------------------- 1 | // Test "framework" 2 | 3 | @bs.scope("process") @bs.val external exit: int => unit = "exit" 4 | @bs.scope("Error") @bs.val external captureStackTrace: {..} => unit = "captureStackTrace" 5 | @bs.module("@babel/code-frame") @bs.val external codeFrameColumns: string => {..} => {..} => string = "codeFrameColumns" 6 | @bs.module("fs") @bs.val external readFileSync: string => {..} => string = "readFileSync" 7 | @bs.module("path") @bs.val external join: string => string => string = "join" 8 | 9 | let dirname = switch %external(__dirname) { 10 | | None => "" 11 | | Some(dirname) => dirname 12 | } 13 | 14 | let cleanUpStackTrace = stack => { 15 | // Stack format: https://nodejs.org/api/errors.html#errors_error_stack 16 | // Remove the node loader and other lines. No point in showing them 17 | let rec removeInternalLines = (lines, i) => { 18 | if (i >= Js.Array2.length(lines)) { 19 | lines 20 | } else if (Js.Array2.unsafe_get(lines, i)->Js.String2.indexOf(" (internal/") >= 0) { 21 | lines->Js.Array2.slice(~start=0, ~end_=i) 22 | } else { 23 | removeInternalLines(lines, i + 1) 24 | } 25 | } 26 | 27 | stack 28 | ->Js.String2.split("\n") 29 | // first line is "Error ...". Second line is this frame's stack trace. Ignore the 2 30 | ->Js.Array2.sliceFrom(2) 31 | ->removeInternalLines(0) 32 | // stack is indented 4 spaces. Remove 2 33 | ->Js.Array2.map(line => line->Js.String2.sliceToEnd(~from=2)) 34 | ->Js.Array2.joinWith("\n") 35 | } 36 | 37 | let run = (loc, left, comparator, right) => { 38 | if (!comparator(left, right)) { 39 | let ((file, line, _, _), _ ) = loc 40 | let fileContent = readFileSync(join(dirname, file), {"encoding": "utf-8"}) 41 | let left = Js.Json.stringifyAny(left) 42 | let right = Js.Json.stringifyAny(right) 43 | let codeFrame = codeFrameColumns(fileContent, {"start": {"line": line}}, {"highlightCode": true}) 44 | let errorMessage = j` 45 | \u001b[31mTest Failure! 46 | \u001b[36m$file\u001b[0m:\u001b[2m$line 47 | $codeFrame 48 | \u001b[39mLeft: \u001b[31m$left 49 | \u001b[39mRight: \u001b[31m$right\u001b[0m 50 | ` 51 | Js.log(errorMessage) 52 | // API: https://nodejs.org/api/errors.html#errors_error_capturestacktrace_targetobject_constructoropt 53 | let obj = Js.Obj.empty() 54 | captureStackTrace(obj) 55 | Js.log(obj["stack"]->cleanUpStackTrace) 56 | } 57 | } 58 | --------------------------------------------------------------------------------