├── .envrc ├── .github ├── CODEOWNERS └── workflows │ ├── typescript-ci.yaml │ └── bump-version.yaml ├── .npmignore ├── COPYRIGHT ├── .gitignore ├── .eslintignore ├── tsconfig.json ├── CHANGELOG.md ├── jest.config.js ├── flake.nix ├── LICENSE ├── package.json ├── flake.lock ├── index.d.ts ├── .eslintrc.js ├── README.md ├── Future.ts └── Future.test.ts /.envrc: -------------------------------------------------------------------------------- 1 | use flake; 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @IronCoreLabs/js-dev 2 | /.github/ @IronCoreLabs/ops 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .travis.yml 2 | tsconfig.json 3 | tslint.json 4 | coverage 5 | yarn.lock -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018-Present IronCore Labs Inc. All rights reserved. 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules 3 | yarn-error.log 4 | Future.js 5 | .direnv 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # don't ever lint node_modules 2 | node_modules 3 | # don't lint build output (make sure it's set to your correct build folder name) 4 | dist 5 | # don't lint nyc coverage output 6 | coverage -------------------------------------------------------------------------------- /.github/workflows/typescript-ci.yaml: -------------------------------------------------------------------------------- 1 | name: Typescript CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: null 7 | workflow_dispatch: null 8 | jobs: 9 | typescript-ci: 10 | uses: IronCoreLabs/workflows/.github/workflows/typescript-ci.yaml@typescript-ci-v0 11 | secrets: inherit 12 | with: 13 | test_matrix_node_version: '["20"]' -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "CommonJS", 5 | "strict": true, 6 | "noImplicitReturns": true, 7 | "noUnusedParameters": true, 8 | "noUnusedLocals": true, 9 | "removeComments": true, 10 | "declaration": false, 11 | "lib": ["es5", "es2015.iterable", "es2015.promise", "dom"] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2.2.1 4 | 5 | ### Fixes 6 | 7 | - Fixed [#44](https://github.com/IronCoreLabs/FutureJS/issues/44). 8 | 9 | ## 2.2.0 10 | 11 | ### Additions 12 | 13 | - made `Future` `Thenable`, so `await` can used with it and it can be chained with `Promises`. `await` will immediately kick off the `Future`, similarly to `engage`. 14 | 15 | ## 2.1.2 16 | 17 | ### Fixes 18 | 19 | - fixed a bug in `Future.all` that prevented an empty array from ever resolving. 20 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | restoreMocks: true, 4 | errorOnDeprecated: true, 5 | coverageThreshold: { 6 | global: { 7 | branches: 95, 8 | functions: 95, 9 | lines: 95, 10 | statements: -5, 11 | }, 12 | }, 13 | //Use ts-jest for all .ts files 14 | transform: { 15 | "^.+\\.ts$": "ts-jest", 16 | }, 17 | testRegex: "(\\.|/)(test)\\.(js|ts)$", 18 | moduleFileExtensions: ["ts", "js", "json"], 19 | }; 20 | -------------------------------------------------------------------------------- /.github/workflows/bump-version.yaml: -------------------------------------------------------------------------------- 1 | name: Bump Version 2 | 3 | concurrency: 4 | group: ${{ github.workflow }}-${{ github.ref }} 5 | cancel-in-progress: true 6 | 7 | on: 8 | # This repo doesn't bump version on push to `main` 9 | workflow_dispatch: 10 | inputs: 11 | version: 12 | description: New semver release version. 13 | 14 | jobs: 15 | bump: 16 | uses: IronCoreLabs/workflows/.github/workflows/bump-version.yaml@bump-version-v1 17 | with: 18 | version: ${{ inputs.version }} 19 | secrets: inherit 20 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "FutureJS"; 3 | inputs.flake-utils.url = "github:numtide/flake-utils"; 4 | 5 | outputs = { self, nixpkgs, flake-utils }: 6 | flake-utils.lib.eachDefaultSystem 7 | (system: 8 | let 9 | pkgs = nixpkgs.legacyPackages.${system}; 10 | in 11 | { 12 | devShell = pkgs.mkShell 13 | { 14 | buildInputs = 15 | [ 16 | pkgs.nodejs_20 17 | (pkgs.yarn.override { 18 | nodejs = pkgs.nodejs_20; 19 | }) 20 | ]; 21 | }; 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 IronCore Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "futurejs", 3 | "version": "2.2.2-pre", 4 | "description": "Promise-alternative library for doing asynchronous operations", 5 | "repository": "https://github.com/IronCoreLabs/futurejs", 6 | "author": "IronCore Labs", 7 | "license": "MIT", 8 | "main": "Future.js", 9 | "types": "index.d.ts", 10 | "scripts": { 11 | "lint": "eslint . --ext .ts,.tsx", 12 | "test": "tsc --noEmit && yarn run lint && yarn run unit", 13 | "unit": "jest --coverage", 14 | "build": "tsc -d --lib es5,es2015.promise Future.ts && mv Future.d.ts index.d.ts" 15 | }, 16 | "devDependencies": { 17 | "@types/jest": "^26.0.22", 18 | "@typescript-eslint/eslint-plugin": "^5.59.11", 19 | "@typescript-eslint/parser": "^5.59.11", 20 | "eslint": "^7.23.0", 21 | "eslint-plugin-import": "^2.22.1", 22 | "eslint-plugin-jsdoc": "^32.3.0", 23 | "eslint-plugin-prefer-arrow": "^1.2.3", 24 | "jest": "^26.6.0", 25 | "jest-extended": "^0.11.5", 26 | "ts-jest": "^26.4.4", 27 | "typescript": "^4.2.4" 28 | }, 29 | "prettier": { 30 | "printWidth": 160, 31 | "tabWidth": 4, 32 | "trailingComma": "es5", 33 | "bracketSpacing": false, 34 | "jsxBracketSameLine": true, 35 | "arrowParens": "always" 36 | }, 37 | "dependencies": {} 38 | } 39 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1731533236, 9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 0, 24 | "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=", 25 | "path": "/nix/store/0d3h8gi2q46fb4l563h6pginjw2a90r4-source", 26 | "type": "path" 27 | }, 28 | "original": { 29 | "id": "nixpkgs", 30 | "type": "indirect" 31 | } 32 | }, 33 | "root": { 34 | "inputs": { 35 | "flake-utils": "flake-utils", 36 | "nixpkgs": "nixpkgs" 37 | } 38 | }, 39 | "systems": { 40 | "locked": { 41 | "lastModified": 1681028828, 42 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 43 | "owner": "nix-systems", 44 | "repo": "default", 45 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 46 | "type": "github" 47 | }, 48 | "original": { 49 | "owner": "nix-systems", 50 | "repo": "default", 51 | "type": "github" 52 | } 53 | } 54 | }, 55 | "root": "root", 56 | "version": 7 57 | } 58 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export type Resolve = (result: R) => void; 2 | export type Reject = (e: L) => void; 3 | export type RejectResolveAction = (reject: Reject, resolve: Resolve) => void; 4 | export default class Future { 5 | action: RejectResolveAction; 6 | constructor(action: RejectResolveAction); 7 | /** 8 | * Start execution of the Future. Accepts resolve/reject parameters 9 | * @param {Function} reject Handler if error occured during Future execution 10 | * @param {Function} resolve Handler if Future fully executed successfully 11 | */ 12 | engage(reject: Reject, resolve: Resolve): void; 13 | /** 14 | * Presents a Thenable interface to the Future, which immediatly starts execution of the Future. This allows for 15 | * await syntax to be used on Futures, as well as chaining with other Thenable objects (e.g. Promises). 16 | * @param {Function} resolve Callback if Future fully executed successfully. 17 | * @param {Function} reject Callback if error occured during Future execution. 18 | * @returns {Promise} Promise that will resolve when the immediately executed Future is 19 | * resolved, with resolve/reject applied to the result. 20 | */ 21 | then(resolve: (r: R) => TResult1 | PromiseLike, reject: (l: any) => TResult2 | PromiseLike): Promise; 22 | /** 23 | * Similar to engage. Starts execution of the Future and returns the resolve/reject wrapped up in a Promise instead 24 | * of taking reject/resolve parameters. 25 | * @return {Promise} Start execution of the Future but return a Promise which will be resolved/reject when the Future is 26 | */ 27 | toPromise(): Promise; 28 | /** 29 | * Modify the data within the pipeline synchronously 30 | * @param {Function} mapper Method which will receive the current data and map it to a new value 31 | */ 32 | map(mapper: (data: R) => MapType): Future; 33 | /** 34 | * Run another asynchronous operation recieving the data from the previous operation 35 | * @param {Function} next Method to execute to run the operation 36 | */ 37 | flatMap(next: (data: R) => Future): Future; 38 | /** 39 | * Attempt to recover from an error in the pipeline in order to continue without rejecting the Future. The repaired type must extend 40 | * the original R as to make sure follow-on Futures can handle the data further in the chain 41 | * @param {Function} errHandler Error handler which should return a new Future and resolve or reject result 42 | */ 43 | handleWith(errHandler: (e: L) => Future): Future; 44 | /** 45 | * Map errors to a new error type. 46 | * @param {Function} mapper Mapping function which will recieve the current error can map it to a new type 47 | */ 48 | errorMap(mapper: (error: L) => LB): Future; 49 | /** 50 | * Wrap the provided function in a Future which will either resolve with it's return value or reject with any exception it throws. 51 | * @param {Function} fn Function to invoke when Future is engaged 52 | */ 53 | static tryF(fn: () => RS): Future; 54 | /** 55 | * Wrap the provided function which returns a Promise within a Future. If the function either throws an error or the resulting Promise rejects, the Future will 56 | * also reject. Otherwise, the Future will resolve with the result of the resolved Promise. 57 | * @param {Function} fn Function to invoke which returns a Promise 58 | */ 59 | static tryP(fn: () => Promise | PromiseLike): Future; 60 | /** 61 | * Create a new synchronous Future which will automatically resolve with the provided value 62 | */ 63 | static of(result: RS): Future; 64 | /** 65 | * Create a new synchronous Future which will automatically reject with the provided value 66 | */ 67 | static reject(error: LS): Future; 68 | /** 69 | * Takes a function and a value and creates a new Future which will attempt to run the function with the value 70 | * and reject if the method throws an exception. 71 | * @param {Function} fn The function to execute 72 | * @param {A} a The value to pass to the function 73 | */ 74 | static encase(fn: (a: A) => RS, a: A): Future; 75 | /** 76 | * Returns a new Future which will run the two provided futures in "parallel". The returned Future will be resolved if both 77 | * of the futures resolve and the results will be in an array properly indexed to how they were passed in. If any of the Futures 78 | * reject, then no results are returned and the Future is rejected. 79 | */ 80 | static gather2(future1: Future, future2: Future): Future; 81 | /** 82 | * Same as gather2 except supports running three concurrent Futures 83 | */ 84 | static gather3(future1: Future, future2: Future, future3: Future): Future; 85 | /** 86 | * Same as gather2 except supports running four concurrent Futures 87 | */ 88 | static gather4(future1: Future, future2: Future, future3: Future, future4: Future): Future; 89 | /** 90 | * Returns a new Future which will run all of the provided futures in parallel. The returned Future will be resolved if all of the Futures 91 | * resolve. If an array of Futures is provided the results will be in an array properly indexed to how they were provided. If an object of 92 | * Futures is provided the results will be an object with the same keys as the objected provided. If any of the Futures reject, then no results 93 | * are returned. 94 | */ 95 | static all(futures: Future[]): Future; 96 | static all(futures: { 97 | [key: string]: Future; 98 | }): Future; 101 | /** 102 | * Run all of the Futures in the provided array in parallel and resolve with an array where the results are in the same index 103 | * as the provided array. 104 | */ 105 | private static allArray; 106 | /** 107 | * Run all of the Futures in the provided Future map in parallel and resolve with an object where the results are in the same key 108 | * as the provided map. 109 | */ 110 | private static allObject; 111 | } 112 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true 4 | }, 5 | "extends": [ 6 | "plugin:@typescript-eslint/recommended", 7 | "plugin:@typescript-eslint/recommended-requiring-type-checking" 8 | ], 9 | "parser": "@typescript-eslint/parser", 10 | "parserOptions": { 11 | "project": "tsconfig.json", 12 | "sourceType": "module" 13 | }, 14 | "plugins": [ 15 | "eslint-plugin-import", 16 | "eslint-plugin-jsdoc", 17 | "eslint-plugin-prefer-arrow", 18 | "@typescript-eslint", 19 | ], 20 | "rules": { 21 | "@typescript-eslint/adjacent-overload-signatures": "error", 22 | "@typescript-eslint/array-type": [ 23 | "error", 24 | { 25 | "default": "array" 26 | } 27 | ], 28 | "@typescript-eslint/ban-types": [ 29 | "error", 30 | { 31 | "types": { 32 | "Object": { 33 | "message": "Avoid using the `Object` type. Did you mean `object`?" 34 | }, 35 | "Function": { 36 | "message": "Avoid using the `Function` type. Prefer a specific function type, like `() => void`." 37 | }, 38 | "Boolean": { 39 | "message": "Avoid using the `Boolean` type. Did you mean `boolean`?" 40 | }, 41 | "Number": { 42 | "message": "Avoid using the `Number` type. Did you mean `number`?" 43 | }, 44 | "String": { 45 | "message": "Avoid using the `String` type. Did you mean `string`?" 46 | }, 47 | "Symbol": { 48 | "message": "Avoid using the `Symbol` type. Did you mean `symbol`?" 49 | } 50 | } 51 | } 52 | ], 53 | "@typescript-eslint/consistent-type-assertions": "off", 54 | "@typescript-eslint/dot-notation": "error", 55 | "@typescript-eslint/explicit-member-accessibility": [ 56 | "off", 57 | { 58 | "accessibility": "explicit" 59 | } 60 | ], 61 | "@typescript-eslint/indent": [ 62 | "error", 63 | 4, 64 | { 65 | "ObjectExpression": "first", 66 | "FunctionDeclaration": { 67 | "parameters": "first" 68 | }, 69 | "FunctionExpression": { 70 | "parameters": "first" 71 | } 72 | } 73 | ], 74 | "@typescript-eslint/member-ordering": "off", 75 | "@typescript-eslint/naming-convention": "off", 76 | "@typescript-eslint/no-empty-function": "error", 77 | "@typescript-eslint/no-empty-interface": "error", 78 | "@typescript-eslint/no-explicit-any": "off", 79 | "@typescript-eslint/no-floating-promises": "error", 80 | "@typescript-eslint/no-misused-new": "error", 81 | "@typescript-eslint/no-namespace": "error", 82 | "@typescript-eslint/no-parameter-properties": "off", 83 | "@typescript-eslint/no-shadow": [ 84 | "error", 85 | { 86 | "hoist": "all" 87 | } 88 | ], 89 | "@typescript-eslint/no-this-alias": "error", 90 | "@typescript-eslint/no-unnecessary-type-assertion": "error", 91 | "@typescript-eslint/no-unused-expressions": "error", 92 | "@typescript-eslint/no-use-before-define": "off", 93 | "@typescript-eslint/no-var-requires": "error", 94 | "@typescript-eslint/prefer-for-of": "off", 95 | "@typescript-eslint/prefer-function-type": "error", 96 | "@typescript-eslint/prefer-namespace-keyword": "error", 97 | "@typescript-eslint/quotes": "off", 98 | "@typescript-eslint/triple-slash-reference": [ 99 | "error", 100 | { 101 | "path": "always", 102 | "types": "prefer-import", 103 | "lib": "always" 104 | } 105 | ], 106 | "@typescript-eslint/unified-signatures": "error", 107 | "arrow-body-style": "off", 108 | "brace-style": [ 109 | "error", 110 | "1tbs" 111 | ], 112 | "comma-dangle": "off", 113 | "complexity": [ 114 | "error", 115 | { 116 | "max": 10 117 | } 118 | ], 119 | "constructor-super": "error", 120 | "default-case": "off", 121 | "eol-last": "off", 122 | "eqeqeq": [ 123 | "error", 124 | "smart" 125 | ], 126 | "guard-for-in": "error", 127 | "id-blacklist": [ 128 | "error", 129 | "any", 130 | "Number", 131 | "number", 132 | "String", 133 | "string", 134 | "Boolean", 135 | "boolean", 136 | "Undefined", 137 | "undefined" 138 | ], 139 | "id-match": "error", 140 | "import/no-extraneous-dependencies": "error", 141 | "import/no-internal-modules": "error", 142 | "import/order": "off", 143 | "jsdoc/check-alignment": "off", 144 | "jsdoc/check-indentation": "off", 145 | "jsdoc/newline-after-description": "off", 146 | "max-classes-per-file": [ 147 | "error", 148 | 1 149 | ], 150 | "max-len": "off", 151 | "new-parens": "error", 152 | "no-bitwise": "error", 153 | "no-caller": "error", 154 | "no-cond-assign": "error", 155 | "no-console": "error", 156 | "no-debugger": "error", 157 | "no-duplicate-case": "error", 158 | "no-duplicate-imports": "error", 159 | "no-empty": "error", 160 | "no-eval": "error", 161 | "no-extra-bind": "error", 162 | "no-fallthrough": "off", 163 | "no-invalid-this": "off", 164 | "no-new-func": "error", 165 | "no-new-wrappers": "error", 166 | "no-redeclare": "error", 167 | "no-return-await": "error", 168 | "no-sequences": "error", 169 | "no-sparse-arrays": "error", 170 | "no-template-curly-in-string": "error", 171 | "no-throw-literal": "error", 172 | "no-trailing-spaces": "error", 173 | "no-undef-init": "error", 174 | "no-underscore-dangle": "error", 175 | "no-unsafe-finally": "error", 176 | "no-unused-labels": "error", 177 | "no-var": "error", 178 | "object-shorthand": "error", 179 | "one-var": [ 180 | "off", 181 | "never" 182 | ], 183 | "prefer-arrow/prefer-arrow-functions": "error", 184 | "prefer-const": "error", 185 | "prefer-object-spread": "error", 186 | "quote-props": "off", 187 | "radix": "off", 188 | "space-in-parens": [ 189 | "error", 190 | "never" 191 | ], 192 | "use-isnan": "error", 193 | "valid-typeof": "off", 194 | } 195 | }; 196 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/IronCoreLabs/FutureJS.svg?branch=main)](https://travis-ci.org/IronCoreLabs/FutureJS) 2 | [![NPM Version](https://badge.fury.io/js/futurejs.svg)](https://www.npmjs.com/package/futurejs) 3 | 4 | # FutureJS 5 | 6 | A library that expands and improves upon the general concepts supported by Promises. Provides improved control flow of asynchronous operations that are also lazy and not run until asked. 7 | 8 | ## Reasoning 9 | 10 | Promises in JavaScript are great start, but they're somewhat limited in the ways you can use them to control the flow of asynchronous operations. Async operations can be easily chained within Promises, but chaining async and synchronous operations together is a bit confusing and requires more code than should be necessary. 11 | 12 | In addition, Promises are eager, meaning that it will kick off your action as soon as your Promise constructor is invoked. This means you cannot pass around Promises without running them. 13 | 14 | Futures provide better granular control over data flow and are also lazy. The Future chain that you construct will not kick off until you tell it to. That means you can pass them around between functions and only run them when necessary. This library also provides a number of useful control functions for chaining async actions with synchronous actions, handling error cases, and doing parallel operations. You can also use this library to wrap Promises to convert them to Futures as well as back into Promises. This allows you to integrate with existing APIs which only talk Promises. 15 | 16 | ## Installation 17 | 18 | `npm install --save futurejs` 19 | 20 | or 21 | 22 | `yarn add futurejs` 23 | 24 | ## Types 25 | 26 | This library is written in TypeScript and published to NPM as JavaScript. TypeScript types are provided as part of this repo, but you can also look at the source to get a better sense of how the functions are typed. 27 | 28 | ## API 29 | 30 | ### Constructor 31 | 32 | `new Future((reject: (e: L) => void, resolve: (result: R) => void) => void)` 33 | 34 | Futures are constructed by providing a function which will be invoked with two callbacks, `reject` and `resolve`. The function you provide to the constructor will not be run until you run the Future (explained later). Within this function you'll perform your async operation and invoke the `resolve` action with your successful result or the `reject` action with an `Error` (or similar) on failure. 35 | 36 | ```js 37 | //Successful action 38 | new Future((reject, resolve) => { 39 | setTimeout(() => { 40 | resolve('It worked!'); 41 | }, 1000); 42 | }); 43 | 44 | //Failure action 45 | new Future((reject, resolve) => { 46 | setTimeout(() => { 47 | reject(new Error('It failed.')); 48 | }, 1000); 49 | }); 50 | ``` 51 | 52 | ### engage 53 | 54 | `engage(reject: (e: L) => void, resolve: (result: R) => void): void` 55 | 56 | As mentioned earlier, Futures are lazy and won't evaluate your constructor action until you ask. This evaluation can be done via the `engage` function. Calling this function will start your Future chain. The `engage` method takes two function arguments which are the functions to execute on success or failure of the Future computation. 57 | 58 | ```js 59 | const request = new Future((reject, resolve) => { 60 | setTimeout(() => { 61 | resolve('It worked!'); 62 | }, 1000); 63 | }); 64 | 65 | request.engage( 66 | (error) => {/*handle Error scenario*/}, 67 | (result) => {/*handle successful fetch response*/} 68 | ); 69 | ``` 70 | 71 | ### toPromise 72 | 73 | `toPromise(): Promise` 74 | 75 | You can also convert your Future chain back into a Promise using the `toPromise` method. This function can be used as an alternative to the `engage` method as it will also kick off computation of your Future. 76 | 77 | ```js 78 | const request = new Future((reject, resolve) => { 79 | setTimeout(() => { 80 | resolve('It worked!'); 81 | }, 1000); 82 | }); 83 | 84 | request 85 | .toPromise() 86 | .then(() => ...) 87 | .catch(() => ...) 88 | ``` 89 | 90 | ### flatMap 91 | 92 | `flatMap(next: (data: R) => Future): Future` 93 | 94 | Run another asynchronous operation in sequence based on the prior Futures resolution value. This second operation will only be run if the first operation succeeded. This operation works similar to how you can chain Promises by returning a new Promise in the `then` callback. 95 | 96 | ```js 97 | const request = new Future((reject, resolve) => { 98 | setTimeout(() => { 99 | resolve('It worked!'); 100 | }, 1000); 101 | }); 102 | 103 | request.flatMap((fetchResult) => { 104 | return new Future(() => fetch('/some/other/api/request')); 105 | }); 106 | ``` 107 | 108 | ### map 109 | 110 | `map(mapper: (data: ResultType) => T): Future` 111 | 112 | Run a synchronous operation that maps the prior Futures resolution value to a different value. This function is also useful if you need to decision off a prior resolution value to resolve or reject the Future. 113 | 114 | ```js 115 | const request = new Future((reject, resolve) => { 116 | setTimeout(() => { 117 | resolve('It worked!'); 118 | }, 1000); 119 | }); 120 | 121 | request.map((fetchResult) => { 122 | //Convert the resolution of this Future from the fetch() Response type to a boolean based on the status field 123 | return fetchResult.status === '204'; 124 | }); 125 | ``` 126 | 127 | ### errorMap 128 | 129 | `errorMap(mapper: (error: Error) => LB): Future` 130 | 131 | Map but for the reject case. Allows you to modify Error objects that might occur during the chain. 132 | 133 | ```js 134 | const request = new Future((reject, resolve) => { 135 | setTimeout(() => { 136 | resolve('It worked!'); 137 | }, 1000); 138 | }); 139 | 140 | request.errorMap((error) => { 141 | if(error.message.contains('no network')){ 142 | return new Error('Please connect to a network before making requests.'); 143 | } 144 | return error; 145 | }); 146 | ``` 147 | 148 | ### handleWith 149 | 150 | `handleWith(errHandler: (e: Error) => Future): Future;` 151 | 152 | Recover from an error in your Future chain and return a repaired result that can be passed to the rest of your chain. This allows you to possibly recover from an error if there are scenarios where error conditions shouldn't be propagated out from your computation. This method will not be invoked if the prior chain does not error, but it will be invoked if any of the prior chains failed, so placement of the `handleWith` call is important to avoid catch all situations. 153 | 154 | ```js 155 | const request = new Future((reject, resolve) => { 156 | setTimeout(() => { 157 | reject(new Error('forced failure')); 158 | }, 1000); 159 | }); 160 | 161 | request.handleWith((fetchError) => { 162 | //Convert the error case from the failed fetch() into something that can be handled in the rest of the chain 163 | return {requestFailed: true}; 164 | }); 165 | ``` 166 | 167 | ### tryF (static) 168 | 169 | `Future.tryF(fn: () => R): Future;` 170 | 171 | Creates a Future which will attempt to execute the provided function and will resolve with it's returned value. If the function throws an exception then the Future will be rejected with the thrown exception. 172 | 173 | ```js 174 | const parse = Future.tryF(() => JSON.parse(value)) 175 | ``` 176 | 177 | ### tryP (static) 178 | 179 | `tryP(fn: () => Promise): Future;` 180 | 181 | Creates a Future which will execute the provided function which should return a Promise. The `.then` and `.catch` methods for the Promise will resolve the Future. 182 | 183 | ```js 184 | const request = Future.tryP(() => fetch('/some/api/endpoint')); 185 | 186 | request.engage( 187 | () => console.log('Request to API failed'), 188 | (response) => { 189 | console.log(response.statusCode); 190 | } 191 | ) 192 | ``` 193 | 194 | ### of (static) 195 | 196 | `Future.of(result: R): Future;` 197 | 198 | Creates a Future which will be immediately resolved with the provided value once computation is kicked off. This function is equivalent to `Promise.resolve()` except that it is still lazily evaluated. 199 | 200 | ```js 201 | const fixed = Future.of({foo: 'bar'}); 202 | 203 | fixed.engage( 204 | () => console.log('will never happen'), 205 | console.log //Will log {"foo": "bar"} 206 | ) 207 | ``` 208 | 209 | ### reject (static) 210 | 211 | `reject(error: L): Future;` 212 | 213 | Creates a Future which will be immediately rejected with the provided Error once computation is kicked off. This function is equivalent to `Promise.reject()` except that it is still lazily evaluated. 214 | 215 | ```js 216 | const request = Future.tryP(() => fetch('/some/api/request')); 217 | 218 | request.flatMap((result) => { 219 | if(result.statusCode === 200){ 220 | return Future.of(true); 221 | } 222 | return Future.reject(new Error(result.status)); 223 | }); 224 | ``` 225 | 226 | ### encase (static) 227 | 228 | `encase(fn: (a: A) => R, a: A): Future;` 229 | 230 | Creates a Future from a function and a value. It will then invoke the function with the value and resolve with the result or reject with any exception thrown by the method. This function is roughly the same as `tryF` but allows you to pass a single argument to the function. 231 | 232 | ```js 233 | const parse = Future.encase(JSON.parse, '{"foo":"bar"}'); 234 | 235 | parse.engage( 236 | (e) => console.log(e.message), 237 | console.log //Will log {"foo": "bar"} as an object 238 | ); 239 | ``` 240 | 241 | ### gather2 (static) 242 | 243 | `gather2(future1: Future, future2: Future): Future;` 244 | 245 | Runs two Futures together in parallel which resolve in different result types. If either of the futures reject then the Future that this returns will reject as well. The resulting arrays values will be fixed by the parameter index. That is, the resolved value from the first Future will be in the 0 index of the array while the resolved value from the second Future will be in the 1 index of the array. 246 | 247 | ```js 248 | const requests = Future.gather2( 249 | Future.tryP(() => fetch('/api/request/one')), 250 | Future.tryP(() => fetch('/api/request/two')); 251 | ); 252 | 253 | requests.engage( 254 | (error) => /*error from the first Future that rejected*/, 255 | (result) => /*result[0] is success from request/one and result[1] is success from request/two*/ 256 | ) 257 | ``` 258 | 259 | ### gather3 (static) 260 | 261 | `gather3(future1: Future, future2: Future, future3: Future): Future` 262 | 263 | Same as above, but runs three Futures together in parallel which resolve in different result types. 264 | 265 | ### gather4 (static) 266 | 267 | `gather4(future1: Future, future2: Future, future3: Future, future4: Future): Future` 268 | 269 | Same as above, but runs four Futures together in parallel which resolve in different result types. 270 | 271 | ### all (static) 272 | 273 | `all(futures: Array>): Future` 274 | 275 | `all(futures: {[key: string]: Future}: Future` 276 | 277 | Same as above but runs an arbitrary number of Futures in parallel, all of which result in the same result type. Supports both arbitrary length arrays and objects of any size. If an array is provided the result will be an array with the indices preserved. If an object is provided the result will be an object with the keys preserved. 278 | 279 | ```js 280 | //As array 281 | const requests = Future.all([ 282 | Future.tryP(() => fetch('/api/request/one')), 283 | Future.tryP(() => fetch('/api/request/two')), 284 | ]); 285 | 286 | requests.engage( 287 | (error) => /*error from the first Future that rejected*/, 288 | (result) => /*result[0] is success from request/one and result[1] is success from request/two*/ 289 | ); 290 | 291 | //As object 292 | const requests = Future.all({ 293 | requestOne: Future.tryP(() => fetch('/api/request/one')), 294 | requestTwo: Future.tryP(() => fetch('/api/request/two')), 295 | }); 296 | 297 | requests.engage( 298 | (error) => /*error from the first Future that rejected*/, 299 | (result) => /*result.requestOne is success from request/one and result.requestTwo is success from request/two*/ 300 | ); 301 | ``` 302 | 303 | ## License 304 | 305 | [MIT licensed](LICENSE) 306 | 307 | Copyright (c) 2018-present IronCore Labs, Inc. 308 | All rights reserved. 309 | -------------------------------------------------------------------------------- /Future.ts: -------------------------------------------------------------------------------- 1 | export type Resolve = (result: R) => void; 2 | export type Reject = (e: L) => void; 3 | export type RejectResolveAction = (reject: Reject, resolve: Resolve) => void; 4 | 5 | export default class Future { 6 | action: RejectResolveAction; 7 | constructor(action: RejectResolveAction) { 8 | this.action = action; 9 | } 10 | 11 | /** 12 | * Start execution of the Future. Accepts resolve/reject parameters 13 | * @param {Function} reject Handler if error occured during Future execution 14 | * @param {Function} resolve Handler if Future fully executed successfully 15 | */ 16 | engage(reject: Reject, resolve: Resolve): void { 17 | this.action(reject, resolve); 18 | } 19 | 20 | private engageAndCatch(reject: Reject, resolve: Resolve): void { 21 | try { 22 | this.engage(reject, resolve); 23 | } catch (e) { 24 | reject(e as L); 25 | } 26 | } 27 | 28 | /** 29 | * Presents a Thenable interface to the Future, which immediatly starts execution of the Future. This allows for 30 | * await syntax to be used on Futures, as well as chaining with other Thenable objects (e.g. Promises). 31 | * @param {Function} resolve Callback if Future fully executed successfully. 32 | * @param {Function} reject Callback if error occured during Future execution. 33 | * @returns {Promise} Promise that will resolve when the immediately executed Future is 34 | * resolved, with resolve/reject applied to the result. 35 | */ 36 | then( 37 | resolve: (r: R) => TResult1 | PromiseLike, 38 | reject: (l: any) => TResult2 | PromiseLike 39 | ): Promise { 40 | return this.toPromise().then(resolve, reject); 41 | } 42 | 43 | /** 44 | * Similar to engage. Starts execution of the Future and returns the resolve/reject wrapped up in a Promise instead 45 | * of taking reject/resolve parameters. 46 | * @return {Promise} Start execution of the Future but return a Promise which will be resolved/reject when the Future is 47 | */ 48 | toPromise(): Promise { 49 | return new Promise((resolve: Resolve, reject: Reject) => this.engage(reject, resolve)); 50 | } 51 | 52 | /** 53 | * Modify the data within the pipeline synchronously 54 | * @param {Function} mapper Method which will receive the current data and map it to a new value 55 | */ 56 | map(mapper: (data: R) => MapType): Future { 57 | return this.flatMap((x: R) => Future.of(mapper(x))); 58 | } 59 | 60 | /** 61 | * Run another asynchronous operation recieving the data from the previous operation 62 | * @param {Function} next Method to execute to run the operation 63 | */ 64 | flatMap(next: (data: R) => Future): Future { 65 | return new Future((reject: Reject, resolve: Resolve) => { 66 | this.engageAndCatch(reject, (data: R) => next(data).engageAndCatch(reject, resolve)); 67 | }); 68 | } 69 | 70 | /** 71 | * Attempt to recover from an error in the pipeline in order to continue without rejecting the Future. The repaired type must extend 72 | * the original R as to make sure follow-on Futures can handle the data further in the chain 73 | * @param {Function} errHandler Error handler which should return a new Future and resolve or reject result 74 | */ 75 | handleWith(errHandler: (e: L) => Future): Future { 76 | return new Future((reject: Reject, resolve: Resolve) => { 77 | this.engage((error) => { 78 | errHandler(error).engage(reject, resolve); 79 | }, resolve as Resolve); //Type cast this as the resolved method should be able to handle both R and RepairedType 80 | }); 81 | } 82 | 83 | /** 84 | * Map errors to a new error type. 85 | * @param {Function} mapper Mapping function which will recieve the current error can map it to a new type 86 | */ 87 | errorMap(mapper: (error: L) => LB): Future { 88 | return new Future((reject: Reject, resolve: Resolve) => { 89 | this.engageAndCatch((error) => reject(mapper(error)), resolve); 90 | }); 91 | } 92 | 93 | /** 94 | * Wrap the provided function in a Future which will either resolve with it's return value or reject with any exception it throws. 95 | * @param {Function} fn Function to invoke when Future is engaged 96 | */ 97 | static tryF(fn: () => RS): Future { 98 | return new Future((reject: Reject, resolve: Resolve) => { 99 | let result: RS; 100 | try { 101 | result = fn(); 102 | } catch (e: any) { 103 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument 104 | return reject(e); 105 | } 106 | resolve(result); 107 | }); 108 | } 109 | 110 | /** 111 | * Wrap the provided function which returns a Promise within a Future. If the function either throws an error or the resulting Promise rejects, the Future will 112 | * also reject. Otherwise, the Future will resolve with the result of the resolved Promise. 113 | * @param {Function} fn Function to invoke which returns a Promise 114 | */ 115 | static tryP(fn: () => Promise | PromiseLike): Future { 116 | return new Future((reject: Reject, resolve: Resolve) => { 117 | let promiseResult: Promise | PromiseLike; 118 | try { 119 | promiseResult = fn(); 120 | } catch (e: any) { 121 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument 122 | return reject(e); 123 | } 124 | //We have to support both Promise and PromiseLike methods as input here, but treat them all as normal Promises when executing them 125 | (promiseResult as Promise).then(resolve).catch(reject); 126 | }); 127 | } 128 | 129 | /** 130 | * Create a new synchronous Future which will automatically resolve with the provided value 131 | */ 132 | static of(result: RS): Future { 133 | return new Future((_, resolve: Resolve) => { 134 | resolve(result); 135 | }); 136 | } 137 | 138 | /** 139 | * Create a new synchronous Future which will automatically reject with the provided value 140 | */ 141 | static reject(error: LS): Future { 142 | return new Future((reject: Reject) => { 143 | reject(error); 144 | }); 145 | } 146 | 147 | /** 148 | * Takes a function and a value and creates a new Future which will attempt to run the function with the value 149 | * and reject if the method throws an exception. 150 | * @param {Function} fn The function to execute 151 | * @param {A} a The value to pass to the function 152 | */ 153 | static encase(fn: (a: A) => RS, a: A): Future { 154 | return new Future((reject: Reject, resolve: Resolve) => { 155 | let result: RS; 156 | try { 157 | result = fn(a); 158 | } catch (e: any) { 159 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument 160 | return reject(e); 161 | } 162 | resolve(result); 163 | }); 164 | } 165 | 166 | /** 167 | * Returns a new Future which will run the two provided futures in "parallel". The returned Future will be resolved if both 168 | * of the futures resolve and the results will be in an array properly indexed to how they were passed in. If any of the Futures 169 | * reject, then no results are returned and the Future is rejected. 170 | */ 171 | static gather2(future1: Future, future2: Future): Future { 172 | return new Future((reject: Reject, resolve: Resolve<[R1, R2]>) => { 173 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 174 | const results: [R1, R2] = [] as any; 175 | let count = 0; 176 | let done = false; 177 | 178 | future1.engageAndCatch( 179 | (error) => { 180 | if (!done) { 181 | done = true; 182 | reject(error); 183 | } 184 | }, 185 | (result) => { 186 | results[0] = result; 187 | if (++count === 2) { 188 | resolve(results); 189 | } 190 | } 191 | ); 192 | 193 | future2.engageAndCatch( 194 | (error) => { 195 | if (!done) { 196 | done = true; 197 | reject(error); 198 | } 199 | }, 200 | (result) => { 201 | results[1] = result; 202 | if (++count === 2) { 203 | resolve(results); 204 | } 205 | } 206 | ); 207 | }); 208 | } 209 | 210 | /** 211 | * Same as gather2 except supports running three concurrent Futures 212 | */ 213 | static gather3(future1: Future, future2: Future, future3: Future): Future { 214 | const firstTwo = this.gather2(future1, future2); 215 | return this.gather2(firstTwo, future3).map<[R1, R2, R3]>(([[f1, f2], f3]) => [f1, f2, f3]); 216 | } 217 | 218 | /** 219 | * Same as gather2 except supports running four concurrent Futures 220 | */ 221 | static gather4( 222 | future1: Future, 223 | future2: Future, 224 | future3: Future, 225 | future4: Future 226 | ): Future { 227 | const firstTwo = this.gather2(future1, future2); 228 | const secondTwo = this.gather2(future3, future4); 229 | return this.gather2(firstTwo, secondTwo).map<[R1, R2, R3, R4]>(([[f1, f2], [f3, f4]]) => [f1, f2, f3, f4]); 230 | } 231 | 232 | /** 233 | * Returns a new Future which will run all of the provided futures in parallel. The returned Future will be resolved if all of the Futures 234 | * resolve. If an array of Futures is provided the results will be in an array properly indexed to how they were provided. If an object of 235 | * Futures is provided the results will be an object with the same keys as the objected provided. If any of the Futures reject, then no results 236 | * are returned. 237 | */ 238 | static all(futures: Future[]): Future; 239 | static all(futures: {[key: string]: Future}): Future; 240 | static all(futures: Future[] | {[key: string]: Future}): Future | Future { 241 | return Array.isArray(futures) ? this.allArray(futures) : this.allObject(futures); 242 | } 243 | 244 | /** 245 | * Run all of the Futures in the provided array in parallel and resolve with an array where the results are in the same index 246 | * as the provided array. 247 | */ 248 | private static allArray(futures: Future[]) { 249 | return new Future((reject: Reject, resolve: Resolve) => { 250 | const results: RS[] = []; 251 | let count = 0; 252 | 253 | if (futures.length === 0) { 254 | resolve(results); 255 | } 256 | 257 | futures.forEach((futureInstance, index) => { 258 | futureInstance.engageAndCatch( 259 | (error) => { 260 | reject(error); 261 | }, 262 | (result) => { 263 | results[index] = result; 264 | count += 1; 265 | if (count === futures.length) { 266 | resolve(results); 267 | } 268 | } 269 | ); 270 | }); 271 | }); 272 | } 273 | 274 | /** 275 | * Run all of the Futures in the provided Future map in parallel and resolve with an object where the results are in the same key 276 | * as the provided map. 277 | */ 278 | private static allObject(futures: {[key: string]: Future}) { 279 | const futureKeys = Object.keys(futures); 280 | //Convert the Future map into an array in the same order as we get back from Object.keys 281 | const futuresArray = futureKeys.map((key) => futures[key]); 282 | return this.allArray(futuresArray).map((futureResults) => { 283 | //Now iterate over the original keys and build a new map from key to Future result 284 | return futureKeys.reduce((futureMap, futureKey, index) => { 285 | //The index of the object keys will be the same index as the expected result 286 | futureMap[futureKey] = futureResults[index]; 287 | return futureMap; 288 | }, {} as {[key: string]: RS}); 289 | }); 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /Future.test.ts: -------------------------------------------------------------------------------- 1 | import "jest-extended"; 2 | import Future from "./Future"; 3 | 4 | describe("Future", () => { 5 | /* eslint-disable @typescript-eslint/unbound-method */ 6 | describe("engage", () => { 7 | test("runs action provided in constructor with reject/resolve callbacks", () => { 8 | const actionSpy = jasmine.createSpy("futureAction"); 9 | const rejectSpy = jasmine.createSpy("rejectSpy"); 10 | const resolveSpy = jasmine.createSpy("resolveSpy"); 11 | 12 | new Future(actionSpy).engage(rejectSpy, resolveSpy); 13 | 14 | expect(actionSpy).toHaveBeenCalledWith(rejectSpy, resolveSpy); 15 | expect(rejectSpy).not.toHaveBeenCalled(); 16 | expect(resolveSpy).not.toHaveBeenCalled(); 17 | }); 18 | 19 | test("does not invoke reject when action throws exception", () => { 20 | const actionSpy = jasmine.createSpy("futureAction").and.callFake(() => { 21 | throw new Error("forced error"); 22 | }); 23 | const rejectSpy = jasmine.createSpy("rejectSpy"); 24 | const resolveSpy = jasmine.createSpy("resolveSpy"); 25 | expect(() => { 26 | new Future(actionSpy).engage(rejectSpy, resolveSpy); 27 | }).toThrowError("forced error"); 28 | expect(actionSpy).toHaveBeenCalledWith(rejectSpy, resolveSpy); 29 | expect(rejectSpy).not.toHaveBeenCalled(); 30 | expect(resolveSpy).not.toHaveBeenCalled(); 31 | }); 32 | 33 | test("resolves with expected value on success", () => { 34 | const action = (_reject: (e: Error) => void, resolve: (val: string) => void) => { 35 | resolve("my value"); 36 | }; 37 | const rejectSpy = jasmine.createSpy("rejectSpy"); 38 | const resolveSpy = jasmine.createSpy("resolveSpy"); 39 | new Future(action).engage(rejectSpy, resolveSpy); 40 | 41 | expect(resolveSpy).toHaveBeenCalledWith("my value"); 42 | }); 43 | test("handleWith for current Future does not get run if errors above its scope throw", (done) => { 44 | let mapCalledTimes = 0; 45 | let handleWithCalledTimes = 0; 46 | const action = Future.of(33) 47 | .handleWith((e: Error) => { 48 | // eslint-disable-next-line no-console 49 | console.log(`failed an infallible future: ${e.message}`); 50 | handleWithCalledTimes++; 51 | return Future.of(-1); 52 | }) 53 | .map((r) => { 54 | mapCalledTimes++; 55 | return r; 56 | }); 57 | 58 | try { 59 | action.engage( 60 | (e) => { 61 | throw e; 62 | }, 63 | (r) => { 64 | throw new Error(`oh no, something went wrong after the future has run to completion on ${r}`); 65 | } 66 | ); 67 | } catch (e) { 68 | expect(handleWithCalledTimes).toBe(0); 69 | expect(mapCalledTimes).toBe(1); 70 | done(); 71 | } 72 | }); 73 | 74 | test("does not get run if engages above this scope throw in the rejection", (done) => { 75 | let mapCalledTimes = 0; 76 | let handleWithCalledTimes = 0; 77 | const expectedError = new Error("error message"); 78 | let errorInHandleWith = null; 79 | const action: Future = Future.reject(expectedError) 80 | .handleWith((e): any => { 81 | handleWithCalledTimes++; 82 | errorInHandleWith = e; 83 | return Future.of(-1); 84 | }) 85 | .map((r) => { 86 | mapCalledTimes++; 87 | return r; 88 | }); 89 | 90 | try { 91 | action.engage( 92 | (e) => { 93 | throw e; 94 | }, 95 | (r) => { 96 | throw new Error(`oh no, something went wrong after the future has run to completion on ${r}`); 97 | } 98 | ); 99 | } catch (e) { 100 | expect(handleWithCalledTimes).toBe(1); 101 | expect(errorInHandleWith).toBe(expectedError); 102 | expect(mapCalledTimes).toBe(1); 103 | done(); 104 | } 105 | }); 106 | }); 107 | 108 | describe("toPromise", () => { 109 | test("returns a promise which can be chained with return value", () => { 110 | const actionSpy = jasmine.createSpy("futureAction"); 111 | 112 | const prom = new Future(actionSpy).toPromise(); 113 | 114 | expect(prom instanceof Promise).toBeTrue(); 115 | expect(actionSpy).toHaveBeenCalledWith(jasmine.any(Function), jasmine.any(Function)); 116 | }); 117 | 118 | test("runs then result with value", (done) => { 119 | const action = (_reject: (e: Error) => void, resolve: (val: string) => void) => { 120 | resolve("my value"); 121 | }; 122 | 123 | new Future(action) 124 | .toPromise() 125 | .then((val: string) => { 126 | expect(val).toEqual("my value"); 127 | done(); 128 | }) 129 | .catch(() => fail("shouldn't fail")); 130 | }); 131 | 132 | test("catches error during exception", (done) => { 133 | const action = (reject: (err: Error) => void) => { 134 | reject(new Error("error value")); 135 | }; 136 | 137 | new Future(action).toPromise().catch((val: Error) => { 138 | expect(val).toEqual(new Error("error value")); 139 | done(); 140 | }); 141 | }); 142 | }); 143 | 144 | describe("map", () => { 145 | test("converts value to new value", (done) => { 146 | const action = (_reject: (e: Error) => void, resolve: (val: string) => void) => { 147 | resolve("my value"); 148 | }; 149 | 150 | new Future(action) 151 | .map((val) => { 152 | expect(val).toEqual("my value"); 153 | return "changed value"; 154 | }) 155 | .engage( 156 | () => fail("engage reject callback should not have been invoked"), 157 | (val) => { 158 | expect(val).toEqual("changed value"); 159 | done(); 160 | } 161 | ); 162 | }); 163 | 164 | test("does not run map when first action fails", (done) => { 165 | const action = (reject: (err: Error) => void) => { 166 | reject(new Error("error value")); 167 | }; 168 | 169 | new Future(action) 170 | .map(() => { 171 | fail("map should not be called when action fails"); 172 | }) 173 | .engage( 174 | (error) => { 175 | expect(error).toEqual(new Error("error value")); 176 | done(); 177 | }, 178 | () => fail("success callback should not be invoked on failure") 179 | ); 180 | }); 181 | }); 182 | 183 | describe("flatMap", () => { 184 | test("maps futures together", (done) => { 185 | const action = (_reject: (e: Error) => void, resolve: (val: string) => void) => { 186 | resolve("my value"); 187 | }; 188 | 189 | new Future(action) 190 | .flatMap((val) => { 191 | expect(val).toEqual("my value"); 192 | return new Future((_reject: (e: Error) => void, resolve: (val: string) => void) => { 193 | resolve("my new future value"); 194 | }); 195 | }) 196 | .engage( 197 | () => fail("Failure handler should not be invoked when handleWith succeeds"), 198 | (val) => { 199 | expect(val).toEqual("my new future value"); 200 | done(); 201 | } 202 | ); 203 | }); 204 | 205 | test("does not invoke flatMap when first action fails", (done) => { 206 | const action = (reject: (e: Error) => void) => { 207 | reject(new Error("error value")); 208 | }; 209 | 210 | new Future(action) 211 | .flatMap(() => { 212 | fail("flatMap should not be invoked if first action fails"); 213 | return Future.of("does not matter"); 214 | }) 215 | .engage( 216 | (err) => { 217 | expect(err).toEqual(new Error("error value")); 218 | done(); 219 | }, 220 | () => fail("resolve handler should not be called when action failed") 221 | ); 222 | }); 223 | 224 | test("invokes reject handler if flatMap fails", (done) => { 225 | const action = (_reject: (e: Error) => void, resolve: (val: string) => void) => { 226 | resolve("my value"); 227 | }; 228 | 229 | new Future(action) 230 | .flatMap((val) => { 231 | expect(val).toEqual("my value"); 232 | return new Future((reject: (e: Error) => void) => { 233 | reject(new Error("flatMap error value")); 234 | }); 235 | }) 236 | .engage( 237 | (err) => { 238 | expect(err).toEqual(new Error("flatMap error value")); 239 | done(); 240 | }, 241 | () => fail("resolve handler should not be called when action failed") 242 | ); 243 | }); 244 | }); 245 | 246 | describe("handleWith", () => { 247 | test("allows mapping of error to new result", (done) => { 248 | const action = (reject: (e: Error) => void) => { 249 | reject(new Error("failure content")); 250 | }; 251 | 252 | new Future(action) 253 | .handleWith((err) => { 254 | expect(err).toEqual(new Error("failure content")); 255 | return new Future((_reject: (e: Error) => void, resolve: (val: string) => void) => { 256 | resolve("my value"); 257 | }); 258 | }) 259 | .engage( 260 | () => fail("Failure handler should not be invoked when handleWith succeeds"), 261 | (val) => { 262 | expect(val).toEqual("my value"); 263 | done(); 264 | } 265 | ); 266 | }); 267 | 268 | test("does not get run if action succeeds", (done) => { 269 | const action = (_reject: (e: Error) => void, resolve: (val: string) => void) => { 270 | resolve("my value"); 271 | }; 272 | 273 | new Future(action) 274 | .handleWith(() => { 275 | fail("handleWith should not be called during success case"); 276 | return Future.of("does not matter"); 277 | }) 278 | .engage( 279 | () => fail("reject handler shouldnt be invoked "), 280 | (val) => { 281 | expect(val).toEqual("my value"); 282 | done(); 283 | } 284 | ); 285 | }); 286 | }); 287 | 288 | describe("errorMap", () => { 289 | test("converts error to new error value", (done) => { 290 | const action = (reject: (err: Error) => void) => { 291 | reject(new Error("error value")); 292 | }; 293 | 294 | new Future(action) 295 | .errorMap((err) => { 296 | expect(err).toEqual(new Error("error value")); 297 | return new Error("mapped error"); 298 | }) 299 | .engage( 300 | (err) => { 301 | expect(err).toEqual(new Error("mapped error")); 302 | done(); 303 | }, 304 | () => fail("resolve callback should not be invoked on error") 305 | ); 306 | }); 307 | 308 | test("does not get invoked when action succeeds", (done) => { 309 | const action = (_reject: (e: Error) => void, resolve: (val: string) => void) => { 310 | resolve("my value"); 311 | }; 312 | 313 | new Future(action) 314 | .errorMap((err) => { 315 | expect(err).toEqual(new Error("error value")); 316 | return new Error("mapped error"); 317 | }) 318 | .engage( 319 | () => fail("engage reject callback should not have been invoked"), 320 | (val) => { 321 | expect(val).toEqual("my value"); 322 | done(); 323 | } 324 | ); 325 | }); 326 | }); 327 | 328 | describe("tryF", () => { 329 | test("resolves with the return value of the provided function", () => { 330 | const fnToValue = jasmine.createSpy("fnToValue").and.returnValue("test"); 331 | 332 | Future.tryF(fnToValue).engage( 333 | (e) => fail(e.message), 334 | (result) => { 335 | expect(result).toEqual("test"); 336 | expect(fnToValue).toHaveBeenCalledWith(); 337 | } 338 | ); 339 | }); 340 | 341 | test("rejects if the provided function throws an exception", () => { 342 | const fnToException = jasmine.createSpy("fnToPromise").and.throwError("forced failure"); 343 | 344 | Future.tryF(fnToException).engage( 345 | (error) => { 346 | expect(error.message).toEqual("forced failure"); 347 | expect(fnToException).toHaveBeenCalledWith(); 348 | }, 349 | () => fail("should reject when function throws an exception") 350 | ); 351 | }); 352 | }); 353 | 354 | describe("tryP", () => { 355 | test("wraps returned promise in then/catch if returned", () => { 356 | const fauxCatch = jasmine.createSpy("fauxCatch"); 357 | const fauxThen = jasmine.createSpy("fauxThen").and.returnValue({ 358 | catch: fauxCatch, 359 | }); 360 | const fauxPromise = jasmine.createSpy("fauxPromise").and.returnValue({ 361 | then: fauxThen, 362 | }); 363 | 364 | const rejectSpy = jasmine.createSpy("rejectSpy"); 365 | const resolveSpy = jasmine.createSpy("resolveSpy"); 366 | Future.tryP(fauxPromise).engage(rejectSpy, resolveSpy); 367 | 368 | expect(fauxPromise).toHaveBeenCalledWith(); 369 | expect(fauxThen).toHaveBeenCalledWith(resolveSpy); 370 | expect(fauxCatch).toHaveBeenCalledWith(rejectSpy); 371 | }); 372 | 373 | test("rejects if function that creates the promise throws an exception", () => { 374 | const fnToPromise = jasmine.createSpy("fnToPromise").and.throwError("forced failure"); 375 | 376 | Future.tryP(fnToPromise).engage( 377 | (error) => { 378 | expect(error.message).toEqual("forced failure"); 379 | expect(fnToPromise).toHaveBeenCalledWith(); 380 | }, 381 | () => fail("should reject when function throws an exception") 382 | ); 383 | }); 384 | }); 385 | 386 | describe("of", () => { 387 | test("resolves Future with provided value", (done) => { 388 | Future.of("fixed value").engage( 389 | () => fail("reject handler should never be called when using Future.of"), 390 | (val) => { 391 | expect(val).toEqual("fixed value"); 392 | done(); 393 | } 394 | ); 395 | }); 396 | }); 397 | 398 | describe("reject", () => { 399 | test("rejects Future with provided value", (done) => { 400 | Future.reject(new Error("fixed error")).engage( 401 | (err) => { 402 | expect(err).toEqual(new Error("fixed error")); 403 | done(); 404 | }, 405 | () => fail("resolve handler should never be called when using Future.reject") 406 | ); 407 | }); 408 | }); 409 | 410 | describe("encase", () => { 411 | test("resolves with value of calling method", () => { 412 | const fn = jasmine.createSpy("fn").and.returnValue("test"); 413 | const val = "provided value"; 414 | 415 | Future.encase(fn, val).engage( 416 | (e) => fail(e), 417 | (value) => { 418 | expect(value).toEqual("test"); 419 | expect(fn).toHaveBeenCalledWith("provided value"); 420 | } 421 | ); 422 | }); 423 | 424 | test("rejects if function throws an exception", () => { 425 | const fn = jasmine.createSpy("fn").and.throwError("forced failure"); 426 | const val = "provided value"; 427 | 428 | Future.encase(fn, val).engage( 429 | (e) => { 430 | expect(e.message).toEqual("forced failure"); 431 | }, 432 | () => fail("Should not resolve when method fails") 433 | ); 434 | }); 435 | }); 436 | 437 | describe("gather2", () => { 438 | test("resolves both futures provided", () => { 439 | const f1 = Future.of("first future value"); 440 | const f2 = Future.of("second future value"); 441 | 442 | spyOn(f1, "engage").and.callThrough(); 443 | spyOn(f2, "engage").and.callThrough(); 444 | 445 | Future.gather2(f1, f2).engage( 446 | (e) => fail(e), 447 | (val) => { 448 | expect(val).toBeArrayOfSize(2); 449 | expect(val[0]).toEqual("first future value"); 450 | expect(val[1]).toEqual("second future value"); 451 | 452 | expect(f1.engage).toHaveBeenCalled(); 453 | expect(f2.engage).toHaveBeenCalled(); 454 | } 455 | ); 456 | }); 457 | 458 | test("rejects with error when first future fails", () => { 459 | const f1 = Future.reject(new Error("first failed future")); 460 | const f2 = Future.of("second future value"); 461 | 462 | spyOn(f1, "engage").and.callThrough(); 463 | spyOn(f2, "engage").and.callThrough(); 464 | 465 | Future.gather2(f1, f2).engage( 466 | (err) => { 467 | expect(err).toEqual(new Error("first failed future")); 468 | 469 | expect(f1.engage).toHaveBeenCalled(); 470 | expect(f2.engage).not.toHaveBeenCalled(); 471 | }, 472 | () => fail("resolve callback should not be invoked when any of the futures fail") 473 | ); 474 | }); 475 | 476 | test("rejects with error when any subsequent future fails", () => { 477 | const f1 = Future.of("first future value"); 478 | const f2 = Future.reject(new Error("second failure message")); 479 | 480 | spyOn(f1, "engage").and.callThrough(); 481 | spyOn(f2, "engage").and.callThrough(); 482 | 483 | Future.gather2(f1, f2).engage( 484 | (err) => { 485 | expect(err).toEqual(new Error("second failure message")); 486 | expect(f1.engage).toHaveBeenCalled(); 487 | expect(f2.engage).toHaveBeenCalled(); 488 | }, 489 | () => fail("resolve callback should not be invoked when any of the futures fail") 490 | ); 491 | }); 492 | 493 | test("rejects with first error that occured when multiple errors", (done) => { 494 | const f1 = new Future((reject: (err: Error) => void) => { 495 | setTimeout(() => { 496 | reject(new Error("first failure message")); 497 | }); 498 | }); 499 | const f2 = Future.reject(new Error("second failure message")); 500 | 501 | spyOn(f1, "engage").and.callThrough(); 502 | spyOn(f2, "engage").and.callThrough(); 503 | 504 | Future.gather2(f1, f2).engage( 505 | (err) => { 506 | expect(err).toEqual(new Error("second failure message")); 507 | expect(f1.engage).toHaveBeenCalled(); 508 | expect(f2.engage).toHaveBeenCalled(); 509 | done(); 510 | }, 511 | () => fail("resolve callback should not be invoked when any of the futures fail") 512 | ); 513 | }); 514 | 515 | test("engages all futures without waiting", (done) => { 516 | const f1 = new Future((_, resolve) => { 517 | setTimeout(() => resolve("first future value")); 518 | }); 519 | 520 | const f2 = Future.of("second future value"); 521 | 522 | spyOn(f1, "engage").and.callThrough(); 523 | spyOn(f2, "engage").and.callThrough(); 524 | 525 | Future.gather2(f1, f2).engage( 526 | (e) => fail(e), 527 | (val) => { 528 | expect(val).toBeArrayOfSize(2); 529 | expect(val[0]).toEqual("first future value"); 530 | expect(val[1]).toEqual("second future value"); 531 | 532 | expect(f1.engage).toHaveBeenCalled(); 533 | expect(f2.engage).toHaveBeenCalled(); 534 | done(); 535 | } 536 | ); 537 | }); 538 | }); 539 | 540 | describe("gather3", () => { 541 | test("resolves with array of values when all 3 Futures succeed", () => { 542 | const f1 = Future.of("first future value"); 543 | const f2 = Future.of("second future value"); 544 | const f3 = Future.of("third future value"); 545 | 546 | spyOn(f1, "engage").and.callThrough(); 547 | spyOn(f2, "engage").and.callThrough(); 548 | spyOn(f3, "engage").and.callThrough(); 549 | 550 | Future.gather3(f1, f2, f3).engage( 551 | (e) => fail(e), 552 | (val) => { 553 | expect(val).toBeArrayOfSize(3); 554 | expect(val[0]).toEqual("first future value"); 555 | expect(val[1]).toEqual("second future value"); 556 | expect(val[2]).toEqual("third future value"); 557 | 558 | expect(f1.engage).toHaveBeenCalled(); 559 | expect(f2.engage).toHaveBeenCalled(); 560 | expect(f3.engage).toHaveBeenCalled(); 561 | } 562 | ); 563 | }); 564 | 565 | test("rejects with error when first future fails", () => { 566 | const f1 = Future.reject(new Error("first failed future")); 567 | const f2 = Future.of("second future value"); 568 | const f3 = Future.of("third future value"); 569 | 570 | spyOn(f1, "engage").and.callThrough(); 571 | spyOn(f2, "engage").and.callThrough(); 572 | spyOn(f3, "engage").and.callThrough(); 573 | 574 | Future.gather3(f1, f2, f3).engage( 575 | (err) => { 576 | expect(err).toEqual(new Error("first failed future")); 577 | expect(f1.engage).toHaveBeenCalled(); 578 | expect(f2.engage).not.toHaveBeenCalled(); 579 | expect(f3.engage).not.toHaveBeenCalled(); 580 | }, 581 | () => fail("resolve callback should not be invoked when any of the futures fail") 582 | ); 583 | }); 584 | 585 | test("rejects with error when any subsequent future fails", () => { 586 | const f1 = Future.of("first future value"); 587 | const f2 = Future.reject(new Error("second failure message")); 588 | const f3 = Future.of("third future value"); 589 | 590 | spyOn(f1, "engage").and.callThrough(); 591 | spyOn(f2, "engage").and.callThrough(); 592 | spyOn(f3, "engage").and.callThrough(); 593 | 594 | Future.gather3(f1, f2, f3).engage( 595 | (err) => { 596 | expect(err).toEqual(new Error("second failure message")); 597 | expect(f1.engage).toHaveBeenCalled(); 598 | expect(f2.engage).toHaveBeenCalled(); 599 | expect(f3.engage).not.toHaveBeenCalled(); 600 | }, 601 | () => fail("resolve callback should not be invoked when any of the futures fail") 602 | ); 603 | }); 604 | 605 | test("rejects with first error that occured when multiple errors", (done) => { 606 | const f1 = new Future((reject: (err: Error) => void) => { 607 | setTimeout(() => { 608 | reject(new Error("first failure message")); 609 | }); 610 | }); 611 | const f2 = Future.of("second future result"); 612 | const f3 = Future.reject(new Error("third failure message")); 613 | 614 | spyOn(f1, "engage").and.callThrough(); 615 | spyOn(f2, "engage").and.callThrough(); 616 | spyOn(f3, "engage").and.callThrough(); 617 | 618 | Future.gather3(f1, f2, f3).engage( 619 | (err) => { 620 | expect(err).toEqual(new Error("third failure message")); 621 | expect(f1.engage).toHaveBeenCalled(); 622 | expect(f2.engage).toHaveBeenCalled(); 623 | expect(f3.engage).toHaveBeenCalled(); 624 | done(); 625 | }, 626 | () => fail("resolve callback should not be invoked when any of the futures fail") 627 | ); 628 | }); 629 | 630 | test("engages all futures without waiting", (done) => { 631 | const f1 = new Future((reject: (err: Error) => void) => { 632 | setTimeout(() => reject(new Error("first failure message"))); 633 | }); 634 | 635 | const f2 = new Future((reject: (err: Error) => void) => { 636 | setTimeout(() => reject(new Error("second failure message"))); 637 | }); 638 | 639 | const f3 = Future.of("third future value"); 640 | 641 | spyOn(f1, "engage").and.callThrough(); 642 | spyOn(f2, "engage").and.callThrough(); 643 | spyOn(f3, "engage").and.callThrough(); 644 | 645 | Future.gather3(f1, f2, f3).engage( 646 | (err) => { 647 | expect(err).toEqual(new Error("first failure message")); 648 | expect(f1.engage).toHaveBeenCalled(); 649 | expect(f2.engage).toHaveBeenCalled(); 650 | expect(f3.engage).toHaveBeenCalled(); 651 | done(); 652 | }, 653 | () => fail("resolve callback should not be invoked when any of the futures fail") 654 | ); 655 | }); 656 | }); 657 | 658 | describe("gather4", () => { 659 | test("resolves with array of values when all 3 Futures succeed", () => { 660 | const f1 = Future.of("first future value"); 661 | const f2 = Future.of("second future value"); 662 | const f3 = Future.of("third future value"); 663 | const f4 = Future.of("fourth future value"); 664 | 665 | spyOn(f1, "engage").and.callThrough(); 666 | spyOn(f2, "engage").and.callThrough(); 667 | spyOn(f3, "engage").and.callThrough(); 668 | spyOn(f4, "engage").and.callThrough(); 669 | 670 | Future.gather4(f1, f2, f3, f4).engage( 671 | (e) => fail(e), 672 | (val) => { 673 | expect(val).toBeArrayOfSize(4); 674 | expect(val[0]).toEqual("first future value"); 675 | expect(val[1]).toEqual("second future value"); 676 | expect(val[2]).toEqual("third future value"); 677 | expect(val[3]).toEqual("fourth future value"); 678 | 679 | expect(f1.engage).toHaveBeenCalled(); 680 | expect(f2.engage).toHaveBeenCalled(); 681 | expect(f3.engage).toHaveBeenCalled(); 682 | expect(f4.engage).toHaveBeenCalled(); 683 | } 684 | ); 685 | }); 686 | 687 | test("rejects with error when any subsequent future fails", () => { 688 | const f1 = Future.of("first future value"); 689 | const f2 = Future.reject(new Error("second failure message")); 690 | const f3 = Future.of("third future value"); 691 | const f4 = Future.reject(new Error("fourth failure message")); 692 | 693 | spyOn(f1, "engage").and.callThrough(); 694 | spyOn(f2, "engage").and.callThrough(); 695 | spyOn(f3, "engage").and.callThrough(); 696 | spyOn(f4, "engage").and.callThrough(); 697 | 698 | Future.gather4(f1, f2, f3, f4).engage( 699 | (err) => { 700 | expect(err).toEqual(new Error("second failure message")); 701 | expect(f1.engage).toHaveBeenCalled(); 702 | expect(f2.engage).toHaveBeenCalled(); 703 | expect(f3.engage).not.toHaveBeenCalled(); 704 | expect(f4.engage).not.toHaveBeenCalled(); 705 | }, 706 | () => fail("resolve callback should not be invoked when any of the futures fail") 707 | ); 708 | }); 709 | 710 | test("engages all futures without waiting", (done) => { 711 | const f1 = new Future((reject: (err: Error) => void) => { 712 | setTimeout(() => reject(new Error("first failure message"))); 713 | }); 714 | 715 | const f2 = new Future((reject: (err: Error) => void) => { 716 | setTimeout(() => reject(new Error("second failure message"))); 717 | }); 718 | 719 | const f3 = Future.of("third future value"); 720 | const f4 = Future.of("fourth future value"); 721 | 722 | spyOn(f1, "engage").and.callThrough(); 723 | spyOn(f2, "engage").and.callThrough(); 724 | spyOn(f3, "engage").and.callThrough(); 725 | spyOn(f4, "engage").and.callThrough(); 726 | 727 | Future.gather4(f1, f2, f3, f4).engage( 728 | (err) => { 729 | expect(err).toEqual(new Error("first failure message")); 730 | expect(f1.engage).toHaveBeenCalled(); 731 | expect(f2.engage).toHaveBeenCalled(); 732 | expect(f3.engage).toHaveBeenCalled(); 733 | expect(f4.engage).toHaveBeenCalled(); 734 | done(); 735 | }, 736 | () => fail("resolve callback should not be invoked when any of the futures fail") 737 | ); 738 | }); 739 | }); 740 | 741 | describe("all", () => { 742 | test("resolves with array of values when all Futures succeed", (done) => { 743 | const f1 = Future.of("first future value"); 744 | const f2 = Future.of("second future value"); 745 | const f3 = Future.of("third future value"); 746 | 747 | spyOn(f1, "engage").and.callThrough(); 748 | spyOn(f2, "engage").and.callThrough(); 749 | spyOn(f3, "engage").and.callThrough(); 750 | 751 | Future.all([f1, f2, f3]).engage( 752 | () => { 753 | fail("reject handler should not be invoked when all futures resolve"); 754 | }, 755 | (val: string[]) => { 756 | expect(val).toBeArrayOfSize(3); 757 | expect(val[0]).toEqual("first future value"); 758 | expect(val[1]).toEqual("second future value"); 759 | expect(val[2]).toEqual("third future value"); 760 | 761 | expect(f1.engage).toHaveBeenCalled(); 762 | expect(f2.engage).toHaveBeenCalled(); 763 | expect(f3.engage).toHaveBeenCalled(); 764 | 765 | done(); 766 | } 767 | ); 768 | }); 769 | 770 | test("rejects with error when first future fails", (done) => { 771 | const f1 = Future.reject(new Error("first failed future")); 772 | const f2 = Future.of("second future value"); 773 | 774 | spyOn(f1, "engage").and.callThrough(); 775 | spyOn(f2, "engage").and.callThrough(); 776 | 777 | Future.all([f1, f2]).engage( 778 | (err: Error) => { 779 | expect(err).toEqual(new Error("first failed future")); 780 | expect(f1.engage).toHaveBeenCalled(); 781 | expect(f2.engage).not.toHaveBeenCalled(); 782 | done(); 783 | }, 784 | () => { 785 | fail("resolve callback should not be invoked when any of the futures fail"); 786 | } 787 | ); 788 | }); 789 | 790 | test("rejects with error when any subsequent future fails", (done) => { 791 | const f1 = Future.of("first future value"); 792 | const f2 = Future.reject(new Error("second failure message")); 793 | 794 | spyOn(f1, "engage").and.callThrough(); 795 | spyOn(f2, "engage").and.callThrough(); 796 | 797 | Future.all([f1, f2]).engage( 798 | (err: Error) => { 799 | expect(err).toEqual(new Error("second failure message")); 800 | expect(f1.engage).toHaveBeenCalled(); 801 | expect(f2.engage).toHaveBeenCalled(); 802 | done(); 803 | }, 804 | () => { 805 | fail("resolve callback should not be invoked when any of the futures fail"); 806 | } 807 | ); 808 | }); 809 | 810 | test("rejects with first error that occured when multiple errors", (done) => { 811 | const f1 = new Future((reject: (err: Error) => void) => { 812 | setTimeout(() => { 813 | reject(new Error("first failure message")); 814 | }); 815 | }); 816 | const f2 = Future.reject(new Error("second failure message")); 817 | 818 | spyOn(f1, "engage").and.callThrough(); 819 | spyOn(f2, "engage").and.callThrough(); 820 | 821 | Future.all([f1, f2]).engage( 822 | (err: Error) => { 823 | expect(err).toEqual(new Error("second failure message")); 824 | expect(f1.engage).toHaveBeenCalled(); 825 | expect(f2.engage).toHaveBeenCalled(); 826 | done(); 827 | }, 828 | () => { 829 | fail("resolve callback should not be invoked when any of the futures fail"); 830 | } 831 | ); 832 | }); 833 | 834 | test("resolves immediately if the array is empty.", (done) => { 835 | Future.all([]).engage( 836 | () => { 837 | fail("an empty array of futures shouldn't fail"); 838 | }, 839 | (arr) => { 840 | expect(arr).toEqual([]); 841 | done(); 842 | } 843 | ); 844 | }); 845 | }); 846 | 847 | describe("allObject", () => { 848 | test("resolves with object with expected keys", (done) => { 849 | const f1 = Future.of("first future value"); 850 | const f2 = Future.of("second future value"); 851 | const f3 = Future.of("third future value"); 852 | 853 | spyOn(f1, "engage").and.callThrough(); 854 | spyOn(f2, "engage").and.callThrough(); 855 | spyOn(f3, "engage").and.callThrough(); 856 | 857 | Future.all({f1, f2, f3}).engage( 858 | () => { 859 | fail("reject handler should not be invoked when all futures resolve"); 860 | }, 861 | (result) => { 862 | expect(Object.keys(result)).toBeArrayOfSize(3); 863 | expect(result.f1).toEqual("first future value"); 864 | expect(result.f2).toEqual("second future value"); 865 | expect(result.f3).toEqual("third future value"); 866 | 867 | expect(f1.engage).toHaveBeenCalled(); 868 | expect(f2.engage).toHaveBeenCalled(); 869 | expect(f3.engage).toHaveBeenCalled(); 870 | 871 | done(); 872 | } 873 | ); 874 | }); 875 | 876 | test("rejects with error when first future fails", (done) => { 877 | const f1 = Future.reject(new Error("first failed future")); 878 | const f2 = Future.of("second future value"); 879 | 880 | spyOn(f1, "engage").and.callThrough(); 881 | spyOn(f2, "engage").and.callThrough(); 882 | 883 | Future.all({f1, f2}).engage( 884 | (err: Error) => { 885 | expect(err).toEqual(new Error("first failed future")); 886 | expect(f1.engage).toHaveBeenCalled(); 887 | expect(f2.engage).not.toHaveBeenCalled(); 888 | done(); 889 | }, 890 | () => { 891 | fail("resolve callback should not be invoked when any of the futures fail"); 892 | } 893 | ); 894 | }); 895 | 896 | test("rejects with error when any subsequent future fails", (done) => { 897 | const f1 = Future.of("first future value"); 898 | const f2 = Future.reject(new Error("second failure message")); 899 | 900 | spyOn(f1, "engage").and.callThrough(); 901 | spyOn(f2, "engage").and.callThrough(); 902 | 903 | Future.all({f1, f2}).engage( 904 | (err: Error) => { 905 | expect(err).toEqual(new Error("second failure message")); 906 | expect(f1.engage).toHaveBeenCalled(); 907 | expect(f2.engage).toHaveBeenCalled(); 908 | done(); 909 | }, 910 | () => { 911 | fail("resolve callback should not be invoked when any of the futures fail"); 912 | } 913 | ); 914 | }); 915 | 916 | test("rejects with first error that occured when multiple errors", (done) => { 917 | const f1 = new Future((reject: (err: Error) => void) => { 918 | setTimeout(() => { 919 | reject(new Error("first failure message")); 920 | }); 921 | }); 922 | const f2 = Future.reject(new Error("second failure message")); 923 | 924 | spyOn(f1, "engage").and.callThrough(); 925 | spyOn(f2, "engage").and.callThrough(); 926 | 927 | Future.all({f1, f2}).engage( 928 | (err: Error) => { 929 | expect(err).toEqual(new Error("second failure message")); 930 | expect(f1.engage).toHaveBeenCalled(); 931 | expect(f2.engage).toHaveBeenCalled(); 932 | done(); 933 | }, 934 | () => { 935 | fail("resolve callback should not be invoked when any of the futures fail"); 936 | } 937 | ); 938 | }); 939 | 940 | test("resolves immediately if the object is empty.", (done) => { 941 | Future.all({}).engage( 942 | () => { 943 | fail("an empty object of futures shouldn't fail"); 944 | }, 945 | (obj) => { 946 | expect(obj).toEqual({}); 947 | done(); 948 | } 949 | ); 950 | }); 951 | }); 952 | 953 | describe("then-able", () => { 954 | test("works with await syntax.", async () => { 955 | const f = Future.of(1); 956 | const result = await f; 957 | expect(result).toEqual(1); 958 | }); 959 | 960 | test("blows up correctly with await syntax.", async () => { 961 | const f = Future.reject(new Error("bad stuff")); 962 | 963 | await expect(f).rejects.toThrow("bad stuff"); 964 | }); 965 | 966 | test("can be chained with normal promises.", async () => { 967 | const p = Promise.resolve(1); 968 | const result = await p.then((num) => Future.of(num + 1)); 969 | expect(result).toEqual(2); 970 | }); 971 | 972 | test("blows up correctly when chained with normal promises.", async () => { 973 | const p = Promise.resolve(1); 974 | const chainedPromiseFuture = p.then(() => Future.reject(new Error("bad stuff"))); 975 | 976 | await expect(chainedPromiseFuture).rejects.toThrow("bad stuff"); 977 | }); 978 | 979 | test("catches exceptions if thrown in the map of the future", async () => { 980 | const p = Promise.resolve(1); 981 | const chainedPromiseFuture = p.then(() => 982 | Future.of(1).map(() => { 983 | throw new Error("bad stuff"); 984 | }) 985 | ); 986 | 987 | await expect(chainedPromiseFuture).rejects.toThrow("bad stuff"); 988 | }); 989 | }); 990 | }); 991 | --------------------------------------------------------------------------------