├── .ci ├── build-docs.yml ├── build-platform.yml ├── publish-api-docs.yml └── utils │ ├── cache.yml │ ├── prepare-cache.yml │ └── use-node.yml ├── .gitignore ├── .npmignore ├── README.md ├── azure-pipelines.yml ├── bsconfig.json ├── package.json ├── pnpm-lock.yaml └── src ├── Yawaramin__Prometo.re └── Yawaramin__Prometo.rei /.ci/build-docs.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - script: "npx bsdoc build Yawaramin__Prometo" 3 | displayName: "[docs] Build" 4 | 5 | - script: "npx bsdoc support-files" 6 | displayName: "[docs] Copy support files" 7 | 8 | - publish: docs 9 | displayName: "[docs] Publish Artifact: docs" 10 | artifact: docs 11 | -------------------------------------------------------------------------------- /.ci/build-platform.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - template: utils/use-node.yml 3 | - template: utils/cache.yml 4 | - script: "npm install" 5 | displayName: "Install project" 6 | - script: "npm run build" 7 | displayName: "Run build" 8 | -------------------------------------------------------------------------------- /.ci/publish-api-docs.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - script: | 3 | git clone https://github.com/yawaramin/prometo.git . 4 | git checkout gh-pages 5 | workingDirectory: $(Build.StagingDirectory) 6 | displayName: "Clone GitHub pages repo" 7 | 8 | - script: | 9 | rm -rf $(Build.StagingDirectory)/Yawaramin__Prometo || true 10 | rm $(Build.StagingDirectory)/odoc.css || true 11 | rm $(Build.StagingDirectory)/highlight.pack.js || true 12 | displayName: Remove last version of the docs 13 | 14 | - task: DownloadPipelineArtifact@2 15 | displayName: Download docs 16 | inputs: 17 | artifactName: docs 18 | targetPath: $(Build.StagingDirectory) 19 | 20 | - task: DownloadSecureFile@1 21 | inputs: 22 | secureFile: deploy_key 23 | displayName: 'Get the deploy key' 24 | 25 | - script: | 26 | mkdir ~/.ssh 27 | mv $DOWNLOADSECUREFILE_SECUREFILEPATH ~/.ssh/id_rsa 28 | chmod 700 ~/.ssh 29 | chmod 600 ~/.ssh/id_rsa 30 | ssh-keyscan -t rsa github.com >>~/.ssh/known_hosts 31 | git remote set-url --push origin git@github.com:yawaramin/prometo.git 32 | git config user.name "Prometo Docs Bot" 33 | git add --all 34 | git commit --amend --message "Generate docs" 35 | git push --force origin gh-pages 36 | workingDirectory: $(Build.StagingDirectory) 37 | displayName: "Push docs to Github pages" 38 | -------------------------------------------------------------------------------- /.ci/utils/cache.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | cache_key: pnpm-lock.yaml 3 | 4 | # The cache key is built up of the following: 5 | # We use a string that we can change to bust the cache 6 | # The string for the OS 7 | # The hash of the lock file 8 | steps: 9 | - task: CacheBeta@1 10 | inputs: 11 | key: key1 | npm | $(Agent.OS) | ${{ parameters.cache_key }} 12 | path: $(CACHE_FOLDER) 13 | cacheHitVar: CACHE_RESTORED 14 | displayName: '[Cache] npm packages' 15 | 16 | - script: 'mv $(CACHE_FOLDER) node_modules' 17 | condition: eq(variables.CACHE_RESTORED, 'true') 18 | displayName: '[Cache][Restore] Restore cached node_modules directory' 19 | -------------------------------------------------------------------------------- /.ci/utils/prepare-cache.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - script: 'mv node_modules $(CACHE_FOLDER)' 3 | displayName: '[Cache][Publish] Move node_modules to be cached' 4 | -------------------------------------------------------------------------------- /.ci/utils/use-node.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - task: NodeTool@0 3 | displayName: "Use Node 8.x" 4 | inputs: 5 | versionSpec: 8.x 6 | -------------------------------------------------------------------------------- /.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/bs 22 | *.mlast 23 | *.mliast 24 | .vscode 25 | .merlin 26 | .bsb.lock 27 | node_modules 28 | *.bs.js 29 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.bs.js 2 | .ci 3 | .gitignore 4 | .merlin 5 | .vscode 6 | azure-pipelines.yml 7 | docs 8 | lib 9 | node_modules 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Prometo 2 | 3 | [![npm](https://img.shields.io/npm/v/@yawaramin/prometo.svg)](https://npmjs.org/package/@yawaramin/prometo) 4 | [![Build Status](https://dev.azure.com/yawaramin/prometo/_apis/build/status/yawaramin.prometo?branchName=main)](https://dev.azure.com/yawaramin/prometo/_build/latest?definitionId=1&branchName=main) 5 | 6 | A type-safe promise type for ReasonML, built directly on top of 7 | JavaScript but adding fine-grained error control and promise 8 | cancellation. 9 | 10 | ## How it works 11 | 12 | Prometo is type-safe because of the following: 13 | 14 | - It's not just a 'promise of your data', it's a 'promise of result of 15 | your data'. In other words, it can't be affected by JavaScript 16 | promises' well-known unsoundness issue where a 'promise of promise of 17 | data' is collapsed at runtime to a 'promise of data'. 18 | - Also because a Prometo promise is a 'promise of result of data', it 19 | encodes an error at the type-level using the Reason `result('a, 'e)` 20 | type. In fact, Prometo ensures that its wrapped promises are not 21 | rejected, as long as you use its operations. So you can be sure that a 22 | Prometo promise is actually not going to throw at runtime. The only 23 | point at which you need to care about catching a possible exception is 24 | when converting it back into a normal JavaScript promise. 25 | 26 | Because it's just a JavaScript promise wrapper, it's also easy to 27 | convert back and forth between JavaScript and Prometo promises. 28 | 29 | ## Fine-grained error management 30 | 31 | Using the technique described in 32 | [Composable Error Handling in OCaml](https://keleshev.com/composable-error-handling-in-ocaml), 33 | Prometo exposes an error type `'e` directly in its main polymorphic 34 | promise type `Prometo.t('a, 'e)`. This allows you to explicitly track 35 | and manage errors at every point of the code where you use these 36 | promises. 37 | 38 | ## Interoperability 39 | 40 | It's easy to interop with JavaScript promises: 41 | 42 | - Use `fromPromise` to convert from a JavaScript Promise to a Prometo 43 | promise 44 | - `toPromise` to convert from Prometo to a JavaScript promise 45 | - `thenPromise` to chain together a Prometo promise and a function that 46 | returns a JavaScript Promise, and keep the result as a type-safe 47 | Prometo promise. 48 | 49 | ## Cancellation 50 | 51 | Prometo promises can be cancelled at any point of usage in the code, 52 | using `Prometo.cancel`. The only caveat is that when you cancel a 53 | promise, it doesn't _immediately_ halt whatever is running inside that 54 | promise, but lets it run until its result is used by the next `flatMap` 55 | in the promise chain. At that point, the _next_ promise automatically 56 | turns into a cancelled promise, stopping whatever was about to happen 57 | next from happening. 58 | 59 | This also does mean that if you want to cancel a promise chain, you need 60 | to ensure that you keep a reference to the _first_ promise in the chain 61 | and cancel _that._ 62 | 63 | While not as immediate as something like 64 | [abortable fetch](https://developers.google.com/web/updates/2017/09/abortable-fetch), 65 | in practice this is more general-purpose (it works with any promise, not 66 | just ones returned by `fetch`), and it's enough to prevent errors like 67 | [calling `setState` on an unmounted React component](https://reactjs.org/blog/2015/12/16/ismounted-antipattern.html). 68 | For example: 69 | 70 | ```reason 71 | let example = fetch("https://example.com"); 72 | Prometo.forEach(~f=setState, example); 73 | 74 | // later... 75 | Prometo.cancel(example); // this will prevent setState from being called 76 | ``` 77 | 78 | Cancelling a promise too late in the chain won't work: 79 | 80 | ```reason 81 | let result = "https://example.com" 82 | |> fetch 83 | |> Prometo.map(~f=setState); 84 | 85 | // later... 86 | Prometo.cancel(result); // won't work, too late, setState has already been called 87 | ``` 88 | 89 | ## Add to project 90 | 91 | - Add the `@yawaramin/prometo` project to your `package.json` (version 92 | number in badge at the top of the readme) 93 | - Add the `@yawaramin/prometo` dependency to your `bs-dependencies` list 94 | in `bsconfig.json` 95 | - Run `npx bsb -clean-world`, then `npx bsb -make-world` 96 | - (Optional but 97 | [recmomended](https://dev.to/yawaramin/consuming-a-modular-ocaml-project-structure-1a2e)) 98 | if you don't have one already, create a `Yawaramin.re` (or `.ml`) file 99 | under `src/` and alias the Prometo main module there: 100 | `module Prometo = Yawaramin__Prometo`. This lets you access it under 101 | the `Yawaramin.Prometo` namespace throughout your project 102 | 103 | ## API docs 104 | 105 | Please go to https://yawaramin.github.io/prometo/ 106 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | name: $(Build.BuildId) - main 2 | 3 | trigger: 4 | batch: true 5 | branches: 6 | include: 7 | - main 8 | - refs/tags/v* 9 | 10 | stages: 11 | - stage: Build 12 | jobs: 13 | - job: macOS 14 | condition: eq(variables['build.sourceBranch'], 'refs/heads/main') 15 | pool: 16 | vmImage: macOS-latest 17 | variables: 18 | CACHE_FOLDER: $(Pipeline.Workspace)/cache 19 | steps: 20 | - template: .ci/build-platform.yml 21 | - template: .ci/build-docs.yml 22 | - template: .ci/utils/prepare-cache.yml 23 | 24 | - stage: Publish_docs 25 | displayName: Publish documentation 26 | dependsOn: [Build] 27 | jobs: 28 | - deployment: Publish_docs 29 | condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main')) 30 | displayName: Publish docs to GitHub 31 | environment: gh-pages 32 | pool: 33 | vmImage: ubuntu-16.04 34 | strategy: 35 | runOnce: 36 | deploy: 37 | steps: 38 | - template: .ci/publish-api-docs.yml 39 | 40 | - stage: Publish_npm 41 | displayName: Publish release to npm 42 | dependsOn: [Build] 43 | jobs: 44 | - job: Publish_npm 45 | condition: startsWith(variables['build.sourceBranch'], 'refs/tags/v') 46 | steps: 47 | - task: Npm@1 48 | inputs: 49 | command: publish 50 | publishEndpoint: NPM 51 | -------------------------------------------------------------------------------- /bsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@yawaramin/prometo", 3 | "version": "0.1.0", 4 | "sources": { 5 | "dir": "src", 6 | "subdirs": true 7 | }, 8 | "package-specs": { 9 | "module": "commonjs", 10 | "in-source": true 11 | }, 12 | "suffix": ".bs.js", 13 | "bs-dependencies": [], 14 | "warnings": { 15 | "number": "+A-48-105", 16 | "error": "+A-3-44-102" 17 | }, 18 | "refmt": 3 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@yawaramin/prometo", 3 | "version": "0.11.1", 4 | "scripts": { 5 | "clean": "bsb -clean-world", 6 | "build": "bsb -make-world", 7 | "watch": "bsb -make-world -w" 8 | }, 9 | "keywords": [ 10 | "BuckleScript" 11 | ], 12 | "author": "", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "bs-platform": "^7.0.1", 16 | "bsdoc": "^6.0.2-alpha" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | devDependencies: 2 | bs-platform: 7.3.2 3 | bsdoc: 6.0.2-alpha 4 | lockfileVersion: 5.1 5 | packages: 6 | /bs-platform/7.3.2: 7 | dev: true 8 | hasBin: true 9 | requiresBuild: true 10 | resolution: 11 | integrity: sha512-seJL5g4anK9la4erv+B2o2sMHQCxDF6OCRl9en3hbaUos/S3JsusQ0sPp4ORsbx5eXfHLYBwPljwKXlgpXtsgQ== 12 | /bsdoc/6.0.2-alpha: 13 | dev: true 14 | hasBin: true 15 | requiresBuild: true 16 | resolution: 17 | integrity: sha512-Zr3Z4U+lynz6FK9vqz+45jTBlKgWJ6iUssQcAU+2QUGtGMg0TNEJz2QEOvwkz5ZFVv+gLbF+c8sbhkYO4OjLJg== 18 | specifiers: 19 | bs-platform: ^7.0.1 20 | bsdoc: ^6.0.2-alpha 21 | -------------------------------------------------------------------------------- /src/Yawaramin__Prometo.re: -------------------------------------------------------------------------------- 1 | type error = [ | `Prometo_error(Js.Promise.error) | `Prometo_cancelled]; 2 | type t('a, 'e) = Js.Promise.t(result('a, [> error] as 'e)); 3 | 4 | exception Prometo_cancelled; 5 | 6 | type cancelledSymbol; 7 | [@bs.val] external cancelledSymbol: unit => cancelledSymbol = "Symbol"; 8 | let cancelledSymbol = cancelledSymbol(); 9 | 10 | [@bs.get_index] 11 | external cancelled: (t(_, _), cancelledSymbol) => option(bool); 12 | let cancelled = t => cancelled(t, cancelledSymbol); 13 | 14 | let make = a => Js.Promise.resolve(Ok(a)); 15 | 16 | module Error = { 17 | let ok = make; 18 | let make = e => Js.Promise.resolve(Error(e)); 19 | 20 | let flatMap = (~f, t) => 21 | Js.Promise.then_( 22 | result => 23 | switch (result, cancelled(t)) { 24 | | (_, Some(true)) => make(`Prometo_cancelled) 25 | | (Ok(_), _) 26 | | (Error(`Prometo_cancelled), _) => Obj.magic(t) 27 | | (Error(e), _) => 28 | e |> f |> Js.Promise.catch(e => make(`Prometo_error(e))) 29 | }, 30 | t, 31 | ); 32 | 33 | let map = (~f, t) => flatMap(~f=e => e |> f |> make, t); 34 | let recover = (~f, t) => flatMap(~f=e => e |> f |> ok, t); 35 | let forEach = (~f, t) => t |> recover(~f) |> ignore; 36 | }; 37 | 38 | [@bs.set_index] external cancel: (t(_, _), cancelledSymbol, bool) => unit; 39 | let cancel = t => cancel(t, cancelledSymbol, true); 40 | 41 | let updateResult = (result, elem) => 42 | switch (result, elem) { 43 | | (_, Error(_) as error) => error 44 | | (Error(_), _) => result 45 | | (Ok(elems), Ok(elem)) => 46 | elems |> Js.Array.push(elem) |> ignore; 47 | Ok(elems); 48 | }; 49 | 50 | let fromArray = array => 51 | array 52 | |> Js.Promise.all 53 | |> Js.Promise.then_(results => 54 | results 55 | |> Js.Array.reduce(updateResult, Ok([||])) 56 | |> Js.Promise.resolve 57 | ); 58 | 59 | let fromPromise = promise => 60 | promise 61 | |> Js.Promise.then_(make) 62 | |> Js.Promise.catch(e => Error.make(`Prometo_error(e))); 63 | 64 | let flatMap = (~f, t) => 65 | Js.Promise.then_( 66 | result => 67 | switch (result, cancelled(t)) { 68 | | (_, Some(true)) => Error.make(`Prometo_cancelled) 69 | | (Ok(a), _) => 70 | a |> f |> Js.Promise.catch(e => Error.make(`Prometo_error(e))) 71 | | (Error(err), _) => Error.make(err) 72 | }, 73 | t, 74 | ); 75 | 76 | let handle = (~f, t) => 77 | Js.Promise.( 78 | then_( 79 | result => 80 | switch (result, cancelled(t)) { 81 | | (Error(`Prometo_cancelled), _) 82 | | (_, Some(true)) => Error.make(`Prometo_cancelled) 83 | | (Ok(_) | Error(_), _) => 84 | result |> f |> resolve |> catch(e => Error.make(`Prometo_error(e))) 85 | }, 86 | t, 87 | ) 88 | ); 89 | 90 | let map = (~f) => flatMap(~f=a => a |> f |> make); 91 | 92 | let forEach = (~f, t) => t |> map(~f) |> ignore; 93 | 94 | let thenPromise = (~f) => flatMap(~f=a => a |> f |> fromPromise); 95 | 96 | let toPromise = t => 97 | Js.Promise.( 98 | then_( 99 | result => 100 | switch (result, cancelled(t)) { 101 | | (_, Some(true)) 102 | | (Error(`Prometo_cancelled), _) => reject(Prometo_cancelled) 103 | | (Ok(a), _) => resolve(a) 104 | | (Error(`Prometo_error(error)), _) => error |> Obj.magic |> reject 105 | | (Error(e), _) => e |> Obj.magic |> reject 106 | }, 107 | t, 108 | ) 109 | ); 110 | 111 | module Infix = { 112 | let (>>=) = (t, f) => flatMap(~f, t); 113 | let (>|=) = (t, f) => map(~f, t); 114 | }; 115 | 116 | open Infix; 117 | 118 | let zip2 = (t1, t2) => t1 >>= (a1 => t2 >|= (a2 => (a1, a2))); 119 | 120 | let zip3 = (t1, t2, t3) => 121 | t1 >>= (a1 => t2 >>= (a2 => t3 >|= (a3 => (a1, a2, a3)))); 122 | 123 | let zip4 = (t1, t2, t3, t4) => 124 | t1 >>= (a1 => t2 >>= (a2 => t3 >>= (a3 => t4 >|= (a4 => (a1, a2, a3, a4))))); 125 | 126 | let zip5 = (t1, t2, t3, t4, t5) => 127 | t1 128 | >>= ( 129 | a1 => 130 | t2 131 | >>= ( 132 | a2 => 133 | t3 >>= (a3 => t4 >>= (a4 => t5 >|= (a5 => (a1, a2, a3, a4, a5)))) 134 | ) 135 | ); 136 | 137 | let zip6 = (t1, t2, t3, t4, t5, t6) => 138 | t1 139 | >>= ( 140 | a1 => 141 | t2 142 | >>= ( 143 | a2 => 144 | t3 145 | >>= ( 146 | a3 => 147 | t4 148 | >>= ( 149 | a4 => t5 >>= (a5 => t6 >|= (a6 => (a1, a2, a3, a4, a5, a6))) 150 | ) 151 | ) 152 | ) 153 | ); 154 | -------------------------------------------------------------------------------- /src/Yawaramin__Prometo.rei: -------------------------------------------------------------------------------- 1 | [@text {|Type-safe, cancelable JavaScript promises for ReasonML.|}]; 2 | 3 | /** The 'base' error type of all Prometo promises. If a promise is in a 4 | failed state, it is always possible for the failure to be from a 5 | caught exception, or because the promise was cancelled. */ 6 | type error = [ | `Prometo_error(Js.Promise.error) | `Prometo_cancelled]; 7 | 8 | /** Promise with result type ['a] and polymorphic variant errors of type 9 | ['e]. At runtime this is just a standard JavaScript promise that is 10 | never rejected (as long as your code doesn't throw any exceptions. 11 | 12 | We can't tell until runtime whether the promise has been cancelled or 13 | not, so the error type ['e] always needs to have a 'minimum error' of 14 | [error]. */ 15 | type t(+_, 'e) constraint 'e = [> error]; 16 | 17 | /** [cancel(t)] sets the promise [t] as cancelled. This means that all 18 | subsequent operations on [t], like [flatMap], [map], [toPromise], etc. 19 | will be short-circuited to ensure no further work is done. 20 | 21 | In other words, you can cancel a complex promise chain as long as you 22 | have a reference to the first promise in the chain and call [cancel] 23 | on it. 24 | 25 | Note that this doesn't include any callbacks registered on the 26 | promise before it was converted into a Prometo type [t]. In other 27 | words, any callbacks registered using [Js.Promise.then_] will {i not} 28 | be cancelled. */ 29 | let cancel: t(_, _) => unit; 30 | 31 | [@text {|{1 Creating promises}|}]; 32 | 33 | /** [fromArray(array)] chains the given [array] of promises in sequence, 34 | returning a single promise containing the array of results of the 35 | input promises. */ 36 | let fromArray: array(t('a, 'e)) => t(array('a), 'e); 37 | 38 | /** [fromPromise(promise)] converts a JavaScript promise into a Prometo 39 | promise (this means ensuring that the promise is not rejected). */ 40 | let fromPromise: Js.Promise.t('a) => t('a, _); 41 | 42 | /** [make(a)] returns a promise which contains the successful result [a]. */ 43 | let make: 'a => t('a, [> error ]); 44 | 45 | [@text {|{1 Processing promises}|}]; 46 | 47 | /** [flatMap(~f, t)] runs the function [f] on the completion result of 48 | promise [t], unless [t] is in an error state or is cancelled. If 49 | successful it returns the result of [f]. If [f] throws an exception 50 | it catches and safely returns a promise in an error state. */ 51 | let flatMap: (~f: 'a => t('b, 'e), t('a, 'e)) => t('b, 'e); 52 | 53 | /** [forEach(~f, t)] calls [f] with the successful result of [t] and 54 | discards the result. If [t] is errored or cancelled, is a no-op. */ 55 | let forEach: (~f: 'a => unit, t('a, _)) => unit; 56 | 57 | /** [handle(~f, t)] calls [f] with the result of [t] and returns a 58 | promise with the transformed result. If [t] is cancelled, it's a 59 | no-op. I.e., it's impossible (by design) to handle a cancelled 60 | promise. 61 | 62 | @since 0.11.0 */ 63 | let handle: (~f: result('a, 'e) => result('b, 'e), t('a, 'e)) => t('b, 'e); 64 | 65 | /** [map(~f, t)] returns a promise which has the result of calling [f] on 66 | the input promise [t]'s result. If [t] is errored or cancelled, 67 | returns it directly. */ 68 | let map: (~f: 'a => 'b, t('a, 'e)) => t('b, 'e); 69 | 70 | /** [thenPromise(~f, t)] runs [f] on the result of the promise [t], 71 | safely converting its resulting JavaScript Promise into a Prometo 72 | promise. I.e., it lifts up JavaScript promise-chain functions into 73 | Prometo promise-chain functions. */ 74 | let thenPromise: (~f: 'a => Js.Promise.t('b), t('a, 'e)) => t('b, 'e); 75 | 76 | /** [toPromise(t)] converts the Prometo promise [t] into a standard 77 | JavaScript promise. This means 'unwrapping' the contained result 78 | value and rejecting the promise if the result was an error or if [t] 79 | was cancelled. */ 80 | let toPromise: 81 | t('a, [> error | `Prometo_error(Js.Promise.error)]) => Js.Promise.t('a); 82 | 83 | [@text 84 | {|{1 Joining promises together} 85 | 86 | The following functions join multiple promises together into a single 87 | promise containing the results of all the individual promises.|} 88 | ]; 89 | 90 | let zip2: (t('a1, 'e), t('a2, 'e)) => t(('a1, 'a2), 'e); 91 | let zip3: (t('a1, 'e), t('a2, 'e), t('a3, 'e)) => t(('a1, 'a2, 'a3), 'e); 92 | let zip4: 93 | (t('a1, 'e), t('a2, 'e), t('a3, 'e), t('a4, 'e)) => 94 | t(('a1, 'a2, 'a3, 'a4), 'e); 95 | let zip5: 96 | (t('a1, 'e), t('a2, 'e), t('a3, 'e), t('a4, 'e), t('a5, 'e)) => 97 | t(('a1, 'a2, 'a3, 'a4, 'a5), 'e); 98 | let zip6: 99 | ( 100 | t('a1, 'e), 101 | t('a2, 'e), 102 | t('a3, 'e), 103 | t('a4, 'e), 104 | t('a5, 'e), 105 | t('a6, 'e) 106 | ) => 107 | t(('a1, 'a2, 'a3, 'a4, 'a5, 'a6), 'e); 108 | 109 | /** Operators that may be handy when processing lots of promises. */ 110 | module Infix: { 111 | /** The same as [flatMap]. */ 112 | let (>>=): (t('a, 'e), 'a => t('b, 'e)) => t('b, 'e); 113 | 114 | /** The same as [map]. */ 115 | let (>|=): (t('a, 'e), 'a => 'b) => t('b, 'e); 116 | }; 117 | 118 | /** Operations to handle promises containing errors. */ 119 | module Error: { 120 | /** [forEach(~f, t)] calls [f] with the error result of [t] and 121 | discards the result. If [t] is succeeded or cancelled, is a no-op. */ 122 | let forEach: (~f: 'e => unit, t(_, 'e)) => unit; 123 | 124 | /** [flatMap(~f, t)] converts an errored promise [t] into a new promise 125 | using the function [f]. The new promise may be succeeded or errored, 126 | unless [t] is cancelled, in which case the new promise is also 127 | cancelled. 128 | 129 | If [f] throws an exception, safely catches it and returns a promise 130 | in an error state. 131 | 132 | If [t] is a succeeded promise, returns [t] directly. */ 133 | let flatMap: (~f: 'e1 => t('a2, 'e2), t('a1, 'e1)) => t('a2, 'e2); 134 | 135 | /** [make(e)] returns a promise which contains the error result [e]. */ 136 | let make: 'e => t('a, 'e); 137 | 138 | /** [map(~f, t)] returns a promise which has the error result of 139 | calling [f] on the input promise [t]'s error result. If [t] is 140 | succeeded or cancelled, returns it directly. */ 141 | let map: (~f: 'e1 => 'e2, t('a, 'e1)) => t('a, 'e2); 142 | 143 | /** [recover(~f, t)] returns a promise with a success result obtained 144 | by calling [f] on the failure result of [t]. This is unless [t] has 145 | a successful result, or is cancelled, in which case this is a no-op. 146 | In other words: it's not possible (by design) to recover from a 147 | cancelled promise. */ 148 | let recover: (~f: 'e => 'a2, t('a1, 'e)) => t('a2, _); 149 | }; 150 | --------------------------------------------------------------------------------