├── .babelrc.js ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .travis.yml ├── README.md ├── package-lock.json ├── package.json └── src ├── Cancel.js ├── CancelToken.js ├── CancelToken.spec.js ├── Disposable.js ├── Disposable.spec.js ├── TimeoutError.js ├── _ExitStack.js ├── _evalDisposable.js ├── _finally.js ├── _identity.js ├── _isDisposable.js ├── _isProgrammerError.js ├── _makeEventAdder.js ├── _matchError.js ├── _noop.js ├── _once.js ├── _resolve.js ├── _setFunctionNameAndLength.js ├── _symbols.js ├── _utils.js ├── asCallback.js ├── asyncFn.js ├── asyncFn.spec.js ├── cancelable.js ├── cancelable.spec.js ├── catch.js ├── catch.spec.js ├── defer.js ├── delay.js ├── finally.js ├── finally.spec.js ├── fixtures.js ├── forArray.js ├── forEach.js ├── forEach.spec.js ├── forIn.js ├── forIterable.js ├── forOwn.js ├── fromCallback.js ├── fromCallback.spec.js ├── fromEvent.js ├── fromEvent.spec.js ├── fromEvents.js ├── fromEvents.spec.js ├── ignoreErrors.js ├── ignoreErrors.spec.js ├── index.js ├── isPromise.js ├── makeAsyncIterator.js ├── nodeify.js ├── nodeify.spec.js ├── pipe.js ├── promisify.js ├── promisify.spec.js ├── promisifyAll.js ├── promisifyAll.spec.js ├── reflect.js ├── retry.js ├── retry.spec.js ├── some.js ├── suppressUnhandledRejections.js ├── tap.js ├── tap.spec.js ├── tapCatch.js ├── timeout.js ├── timeout.spec.js ├── try.js ├── try.spec.js ├── unpromisify.js ├── unpromisify.spec.js ├── wrapApply.js ├── wrapCall.js └── wrapping.spec.js /.babelrc.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const NODE_ENV = process.env.NODE_ENV || "development"; 4 | const __PROD__ = NODE_ENV === "production"; 5 | const __TEST__ = NODE_ENV === "test"; 6 | 7 | const pkg = require("./package"); 8 | 9 | const plugins = {}; 10 | 11 | const presets = { 12 | "@babel/preset-env": { 13 | debug: !__TEST__, 14 | loose: true, 15 | }, 16 | }; 17 | 18 | Object.keys(pkg.devDependencies || {}).forEach((name) => { 19 | if (!(name in presets) && /@babel\/plugin-.+/.test(name)) { 20 | plugins[name] = {}; 21 | } else if (!(name in presets) && /@babel\/preset-.+/.test(name)) { 22 | presets[name] = {}; 23 | } 24 | }); 25 | 26 | module.exports = { 27 | comments: !__PROD__, 28 | ignore: __PROD__ ? [/\.spec\.js$/] : undefined, 29 | plugins: Object.keys(plugins).map((plugin) => [plugin, plugins[plugin]]), 30 | presets: Object.keys(presets).map((preset) => [preset, presets[preset]]), 31 | targets: (() => { 32 | let node = (pkg.engines || {}).node; 33 | if (node !== undefined) { 34 | const trimChars = "^=>~"; 35 | while (trimChars.includes(node[0])) { 36 | node = node.slice(1); 37 | } 38 | } 39 | return { browsers: pkg.browserslist, node }; 40 | })(), 41 | }; 42 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://EditorConfig.org 2 | # 3 | # Julien Fontanet's configuration 4 | # https://gist.github.com/julien-f/8096213 5 | 6 | root = true 7 | 8 | [*] 9 | charset = utf-8 10 | end_of_line = lf 11 | indent_size = 2 12 | indent_style = space 13 | insert_final_newline = true 14 | trim_trailing_whitespace = true 15 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | // standard configuration 4 | "standard", 5 | 6 | // https://github.com/mysticatea/eslint-plugin-node#-rules 7 | "plugin:node/recommended", 8 | 9 | // disable rules handled by prettier 10 | "prettier", 11 | ], 12 | 13 | parser: "@babel/eslint-parser", 14 | parserOptions: { 15 | sourceType: "module", // or "script" if not using ES modules 16 | }, 17 | 18 | rules: { 19 | // uncomment if you are using a builder like Babel 20 | "node/no-unsupported-features/es-syntax": "off", 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /[^.]*.js 2 | /*.js.map 3 | /coverage/ 4 | /node_modules/ 5 | 6 | npm-debug.log 7 | npm-debug.log.* 8 | pnpm-debug.log 9 | pnpm-debug.log.* 10 | yarn-error.log 11 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | 3 | /benchmark/ 4 | /benchmarks/ 5 | *.bench.js 6 | *.bench.js.map 7 | 8 | /coverage/ 9 | 10 | /examples/ 11 | example.js 12 | example.js.map 13 | *.example.js 14 | *.example.js.map 15 | 16 | /fixture/ 17 | /fixtures/ 18 | *.fixture.js 19 | *.fixture.js.map 20 | *.fixtures.js 21 | *.fixtures.js.map 22 | 23 | /test/ 24 | /tests/ 25 | *.spec.js 26 | *.spec.js.map 27 | 28 | __snapshots__/ 29 | 30 | /docs/ 31 | /src/ 32 | /USAGE.md 33 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | - lts/* 5 | 6 | # Use containers. 7 | # http://docs.travis-ci.com/user/workers/container-based-infrastructure/ 8 | sudo: false 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # promise-toolbox 2 | 3 | [![Package Version](https://badgen.net/npm/v/promise-toolbox)](https://npmjs.org/package/promise-toolbox) [![Build Status](https://travis-ci.org/JsCommunity/promise-toolbox.png?branch=master)](https://travis-ci.org/JsCommunity/promise-toolbox) [![PackagePhobia](https://badgen.net/packagephobia/install/promise-toolbox)](https://packagephobia.now.sh/result?p=promise-toolbox) [![Latest Commit](https://badgen.net/github/last-commit/JsCommunity/promise-toolbox)](https://github.com/JsCommunity/promise-toolbox/commits/master) 4 | 5 | > Essential utils for promises. 6 | 7 | **Features:** 8 | 9 | - compatible with all promise implementations 10 | - small (< 150 KB with all dependencies, < 5 KB with gzip) 11 | - nice with ES2015 / ES2016 syntax 12 | 13 | **Table of contents:** 14 | 15 | - [Cancelation](#cancelation) 16 | - [Creation](#creation) 17 | - [Consumption](#consumption) 18 | - [Registering async handlers](#registering-async-handlers) 19 | - [Is cancel token?](#is-cancel-token) 20 | - [Compatibility with AbortSignal](#compatibility-with-abortsignal) 21 | - [@cancelable decorator](#cancelable-decorator) 22 | - [Resource management](#resource-management) 23 | - [Creation](#creation-1) 24 | - [Combination](#combination) 25 | - [Consumption](#consumption-1) 26 | - [Functions](#functions) 27 | - [asyncFn(generator)](#asyncfngenerator) 28 | - [asyncFn.cancelable(generator, [getCancelToken])](#asyncfncancelablegenerator-getcanceltoken) 29 | - [defer()](#defer) 30 | - [fromCallback(fn, arg1, ..., argn)](#fromcallbackfn-arg1--argn) 31 | - [fromEvent(emitter, event, [options]) => Promise](#fromeventemitter-event-options--promise) 32 | - [fromEvents(emitter, successEvents, errorEvents) => Promise](#fromeventsemitter-successevents-errorevents--promise) 33 | - [isPromise(value)](#ispromisevalue) 34 | - [nodeify(fn)](#nodeifyfn) 35 | - [pipe(fns)](#pipefns) 36 | - [pipe(value, ...fns)](#pipevalue-fns) 37 | - [promisify(fn, [ context ]) / promisifyAll(obj)](#promisifyfn--context---promisifyallobj) 38 | - [retry(fn, options, [args])](#retryfn-options-args) 39 | - [try(fn)](#tryfn) 40 | - [wrapApply(fn, args, [thisArg]) / wrapCall(fn, arg, [thisArg])](#wrapapplyfn-args-thisarg--wrapcallfn-arg-thisarg) 41 | - [Pseudo-methods](#pseudo-methods) 42 | - [promise::asCallback(cb)](#promiseascallbackcb) 43 | - [promise::catch(predicate, cb)](#promisecatchpredicate-cb) 44 | - [promise::delay(ms, [value])](#promisedelayms-value) 45 | - [collection::forEach(cb)](#collectionforeachcb) 46 | - [promise::ignoreErrors()](#promiseignoreerrors) 47 | - [promise::finally(cb)](#promisefinallycb) 48 | - [promise::reflect()](#promisereflect) 49 | - [promises::some(count)](#promisessomecount) 50 | - [promise::suppressUnhandledRejections()](#promisesuppressunhandledrejections) 51 | - [promise::tap(onResolved, onRejected)](#promisetaponresolved-onrejected) 52 | - [promise::tapCatch(onRejected)](#promisetapcatchonrejected) 53 | - [promise::timeout(ms, [cb])](#promisetimeoutms-cb-or-rejectionvalue) 54 | 55 | ### Node & [Browserify](http://browserify.org/)/[Webpack](https://webpack.js.org/) 56 | 57 | Installation of the [npm package](https://npmjs.org/package/promise-toolbox): 58 | 59 | ``` 60 | > npm install --save promise-toolbox 61 | ``` 62 | 63 | ### Browser 64 | 65 | You can directly use the build provided at [unpkg.com](https://unpkg.com): 66 | 67 | ```html 68 | 69 | ``` 70 | 71 | ## Usage 72 | 73 | ### Promise support 74 | 75 | If your environment may not natively support promises, you should use a polyfill such as [native-promise-only](https://github.com/getify/native-promise-only). 76 | 77 | On Node, if you want to use a specific promise implementation, 78 | [Bluebird](http://bluebirdjs.com/docs/why-bluebird.html) for instance 79 | to have better performance, you can override the global Promise 80 | variable: 81 | 82 | ```js 83 | global.Promise = require("bluebird"); 84 | ``` 85 | 86 | > Note that it should only be done at the application level, never in 87 | > a library! 88 | 89 | ### Imports 90 | 91 | You can either import all the tools directly: 92 | 93 | ```js 94 | import * as PT from "promise-toolbox"; 95 | 96 | console.log(PT.isPromise(value)); 97 | ``` 98 | 99 | Or import individual tools from the main module: 100 | 101 | ```js 102 | import { isPromise } from "promise-toolbox"; 103 | 104 | console.log(isPromise(value)); 105 | ``` 106 | 107 | Each tool is also exported with a `p` prefix to work around reserved keywords 108 | and to help differentiate with other tools (like `lodash.map`): 109 | 110 | ```js 111 | import { pCatch, pMap } from "promise-toolbox"; 112 | ``` 113 | 114 | If you are bundling your application (Browserify, Rollup, Webpack, etc.), you 115 | can cherry-pick the tools directly: 116 | 117 | ```js 118 | import isPromise from "promise-toolbox/isPromise"; 119 | import pCatch from "promise-toolbox/catch"; 120 | ``` 121 | 122 | ## API 123 | 124 | ### Cancelation 125 | 126 | This library provides an implementation of `CancelToken` from the 127 | [cancelable promises specification](https://tc39.github.io/proposal-cancelable-promises/). 128 | 129 | A cancel token is an object which can be passed to asynchronous 130 | functions to represent cancelation state. 131 | 132 | ```js 133 | import { CancelToken } from "promise-toolbox"; 134 | ``` 135 | 136 | #### Creation 137 | 138 | A cancel token is created by the initiator of the async work and its 139 | cancelation state may be requested at any time. 140 | 141 | ```js 142 | // Create a token which requests cancelation when a button is clicked. 143 | const token = new CancelToken((cancel) => { 144 | $("#some-button").on("click", () => cancel("button clicked")); 145 | }); 146 | ``` 147 | 148 | ```js 149 | const { cancel, token } = CancelToken.source(); 150 | ``` 151 | 152 | A list of existing tokens can be passed to `source()` to make the created token 153 | follow their cancelation: 154 | 155 | ```js 156 | // `source.token` will be canceled (synchronously) as soon as `token1` or 157 | // `token2` or token3` is, with the same reason. 158 | const { cancel, token } = CancelToken.source([token1, token2, token3]); 159 | ``` 160 | 161 | #### Consumption 162 | 163 | The receiver of the token (the function doing the async work) can: 164 | 165 | 1. synchronously check whether cancelation has been requested 166 | 2. synchronously throw if cancelation has been requested 167 | 3. register a callback that will be executed if cancelation is requested 168 | 4. pass the token to subtasks 169 | 170 | ```js 171 | // 1. 172 | if (token.reason) { 173 | console.log("cancelation has been requested", token.reason.message); 174 | } 175 | 176 | // 2. 177 | try { 178 | token.throwIfRequested(); 179 | } catch (reason) { 180 | console.log("cancelation has been requested", reason.message); 181 | } 182 | 183 | // 3. 184 | token.promise.then((reason) => { 185 | console.log("cancelation has been requested", reason.message); 186 | }); 187 | 188 | // 4. 189 | subtask(token); 190 | ``` 191 | 192 | See [`asyncFn.cancelable`](#asyncfncancelablegenerator) for an easy way to create async functions with built-in cancelation support. 193 | 194 | #### Registering async handlers 195 | 196 | > Asynchronous handlers are executed on token cancelation and the 197 | > promise returned by the `cancel` function will wait for all handlers 198 | > to settle. 199 | 200 | ```js 201 | function httpRequest(cancelToken, opts) { 202 | const req = http.request(opts); 203 | req.end(); 204 | cancelToken.addHandler(() => { 205 | req.abort(); 206 | 207 | // waits for the socket to really close for the cancelation to be 208 | // complete 209 | return fromEvent(req, "close"); 210 | }); 211 | return fromEvent(req, "response"); 212 | } 213 | 214 | const { cancel, token } = CancelToken.source(); 215 | 216 | httpRequest(token, { 217 | hostname: "example.org", 218 | }).then((response) => { 219 | // do something with the response of the request 220 | }); 221 | 222 | // wraps with Promise.resolve() because cancel only returns a promise 223 | // if a handler has returned a promise 224 | Promise.resolve(cancel()).then(() => { 225 | // the request has been properly canceled 226 | }); 227 | ``` 228 | 229 | #### Is cancel token? 230 | 231 | ```js 232 | if (CancelToken.isCancelToken(value)) { 233 | console.log("value is a cancel token"); 234 | } 235 | ``` 236 | 237 | #### @cancelable decorator 238 | 239 | > **This is deprecated, instead explicitely pass a cancel token or an abort signal:** 240 | 241 | ```js 242 | const asyncFunction = async (a, b, { cancelToken = CancelToken.none } = {}) => { 243 | cancelToken.promise.then(() => { 244 | // do stuff regarding the cancelation request. 245 | }); 246 | 247 | // do other stuff. 248 | }; 249 | ``` 250 | 251 | > Make your async functions cancelable. 252 | 253 | If the first argument passed to the cancelable function is not a 254 | cancel token, a new one is created and injected and the returned 255 | promise will have a `cancel()` method. 256 | 257 | ```js 258 | import { cancelable, CancelToken } from "promise-toolbox"; 259 | 260 | const asyncFunction = cancelable(async ($cancelToken, a, b) => { 261 | $cancelToken.promise.then(() => { 262 | // do stuff regarding the cancelation request. 263 | }); 264 | 265 | // do other stuff. 266 | }); 267 | 268 | // Either a cancel token is passed: 269 | const source = CancelToken.source(); 270 | const promise1 = asyncFunction(source.token, "foo", "bar"); 271 | source.cancel("reason"); 272 | 273 | // Or the returned promise will have a cancel() method: 274 | const promise2 = asyncFunction("foo", "bar"); 275 | promise2.cancel("reason"); 276 | ``` 277 | 278 | If the function is a method of a class or an object, you can use 279 | `cancelable` as a decorator: 280 | 281 | ```js 282 | class MyClass { 283 | @cancelable 284 | async asyncMethod($cancelToken, a, b) { 285 | // ... 286 | } 287 | } 288 | ``` 289 | 290 | #### Compatibility with AbortSignal 291 | 292 | A cancel token can be created from an abort signal: 293 | 294 | ```js 295 | const token = CancelToken.from(abortSignal); 296 | ``` 297 | 298 | > If `abortSignal` is already a `CancelToken`, it will be returned directly, making it a breeze to create code accepting both :-) 299 | 300 | A cancel token is API compatible with an abort signal and can be used as such: 301 | 302 | ```js 303 | const { cancel, token } = CancelToken.source(); 304 | 305 | await fetch(url, { signal: token }); 306 | ``` 307 | 308 | ### Resource management 309 | 310 | > See [Bluebird documentation](http://bluebirdjs.com/docs/api/resource-management.html) for a good explanation. 311 | 312 | #### Creation 313 | 314 | A disposable is a simple object, which contains a dispose method and possibily a value: 315 | 316 | ```js 317 | const disposable = { dispose: () => db.close(), value: db }; 318 | ``` 319 | 320 | The dispose method may be asynchronous and return a promise. 321 | 322 | As a convenience, you can use the `Disposable` class: 323 | 324 | ```js 325 | import { Disposable } from "promise-toolbox"; 326 | 327 | const disposable = new Disposable(() => db.close(), db); 328 | ``` 329 | 330 | If the process is more complicated, maybe because this disposable depends on 331 | other disposables, you can use a generator function alongside the 332 | `Disposable.factory` decorator: 333 | 334 | ```js 335 | const getTable = Disposable.factory(async function* () { 336 | // simply yield a disposable to use it 337 | const db = yield getDb(); 338 | 339 | const table = await db.getTable(); 340 | try { 341 | // yield the value to expose it 342 | yield table; 343 | } finally { 344 | // this is where you can dispose of the resource 345 | await table.close(); 346 | } 347 | }); 348 | ``` 349 | 350 | #### Combination 351 | 352 | Independent disposables can be acquired and disposed in parallel, to achieve 353 | this, you can use `Disposable.all`: 354 | 355 | ```js 356 | const combined = await Disposable.all([disposable1, disposable2]); 357 | ``` 358 | 359 | Similarly to `Promise.all`, the value of such a disposable, is an array whose 360 | values are the values of the disposables combined. 361 | 362 | #### Consumption 363 | 364 | To ensure all resources are properly disposed of, disposables must never be 365 | used manually, but via the `Disposable.use` function: 366 | 367 | ```js 368 | import { Disposable } from "promise-toolbox"; 369 | 370 | await Disposable.use( 371 | // Don't await the promise here, resource acquisition should be handled by 372 | // `Disposable.use` otherwise, in case of failure, other resources may failed 373 | // to be disposed of. 374 | getTable(), 375 | 376 | // If the function can throw synchronously, a wrapper function can be passed 377 | // directly to `Disposable.use`. 378 | () => getTable(), 379 | 380 | async (table1, table2) => { 381 | // do something with table1 and table 2 382 | // 383 | // both `table1` and `table2` are guaranteed to be deallocated by the time 384 | // the promise returned by `Disposable.use` is settled 385 | } 386 | ); 387 | ``` 388 | 389 | For more complex use cases, just like `Disposable.factory`, the handler can be 390 | a generator function when no disposables are passed: 391 | 392 | ```js 393 | await Disposable.use(async function* () { 394 | const table1 = yield getTable(); 395 | const table2 = yield getTable(); 396 | 397 | // do something with table1 and table 2 398 | // 399 | // both `table1` and `table2` are guaranteed to be deallocated by the time 400 | // the promise returned by `Disposable.use` is settled 401 | }); 402 | ``` 403 | 404 | To enable an entire function to use disposables, you can use `Disposable.wrap`: 405 | 406 | ```js 407 | // context (`this`) and all arguments are forwarded from the call 408 | const disposableUser = Disposable.wrap(async function* (arg1, arg2) { 409 | const table = yield getDisposable(arg1, arg2); 410 | 411 | // do something with table 412 | }); 413 | 414 | // similar to 415 | function disposableUser(arg1, arg2) { 416 | return Disposable.use(async function* (arg1, arg2) { 417 | const table = yield getDisposable(arg1, arg2); 418 | 419 | // do something with table 420 | }); 421 | } 422 | ``` 423 | 424 | ### Functions 425 | 426 | #### asyncFn(generator) 427 | 428 | > Create an [async function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function) from [a generator function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*) 429 | > 430 | > Similar to [`Bluebird.coroutine`](http://bluebirdjs.com/docs/api/promise.coroutine.html). 431 | 432 | ```js 433 | import { asyncFn } from 'promise-toolbox' 434 | 435 | const getUserName = asyncFn(function * (db, userId)) { 436 | const user = yield db.getRecord(userId) 437 | return user.name 438 | }) 439 | ``` 440 | 441 | #### asyncFn.cancelable(generator, [getCancelToken]) 442 | 443 | > Like [`asyncFn(generator)`](#asyncfngenerator) but the created async function supports [cancelation](#cancelation). 444 | > 445 | > Similar to [CAF](https://github.com/getify/CAF). 446 | 447 | ```js 448 | import { asyncFn, CancelToken } from 'promise-toolbox' 449 | 450 | const getUserName = asyncFn.cancelable(function * (cancelToken, db, userId)) { 451 | // this yield will throw if the cancelToken is activated 452 | const user = yield db.getRecord(userId) 453 | return user.name 454 | }) 455 | 456 | const source = CancelToken.source() 457 | 458 | getUserName(source.token, db, userId).then( 459 | name => { 460 | console.log('user name is', name) 461 | }, 462 | error => { 463 | console.error(error) 464 | } 465 | ) 466 | 467 | // only wait 5 seconds to fetch the user from the database 468 | setTimeout(source.cancel, 5e3) 469 | ``` 470 | 471 | ```js 472 | const cancelableAsyncFunction = asyncFn.cancelable(function* ( 473 | cancelToken, 474 | ...args 475 | ) { 476 | // await otherAsyncFunction() but will throw if cancelToken is activated 477 | yield otherAsyncFunction(); 478 | 479 | // if aborting on cancelation is unwanted (eg because the called function 480 | // already handles cancelation), wrap the promise in an array 481 | yield [otherAsyncFunction(cancelToken)]; 482 | 483 | // cancelation, just like any rejection, can be catch 484 | try { 485 | yield otherAsyncFunction(); 486 | } catch (error) { 487 | if (CancelToken.isCancelToken(error)) { 488 | // do some clean-up here 489 | // the rest of the function has been added as an async handler of the 490 | // CancelToken which will make `cancel` waits for it 491 | } 492 | 493 | throw error; 494 | } 495 | 496 | return result; 497 | }); 498 | ``` 499 | 500 | If the cancel token is not the first param of the decorated function, a getter should be passed to `asyncFn.cancelable`, it's called with the same context and arguments as the decorated function and returns the cancel token: 501 | 502 | ```js 503 | const cancelableAsyncFunction = asyncFn.cancelable( 504 | function*(arg1, arg2, options) { 505 | // function logic 506 | }, 507 | (_arg1, _arg2, { cancelToken = CancelToken.none } = {}) => cancelToken; 508 | ); 509 | ``` 510 | 511 | #### defer() 512 | 513 | > Discouraged but sometimes necessary way to create a promise. 514 | 515 | ```js 516 | import { defer } from "promise-toolbox"; 517 | 518 | const { promise, resolve } = defer(); 519 | 520 | promise.then((value) => { 521 | console.log(value); 522 | }); 523 | 524 | resolve(3); 525 | ``` 526 | 527 | #### fromCallback(fn, arg1, ..., argn) 528 | 529 | > Easiest and most efficient way to promisify a function call. 530 | 531 | ```js 532 | import { fromCallback } from "promise-toolbox"; 533 | 534 | // callback is appended to the list of arguments passed to the function 535 | fromCallback(fs.readFile, "foo.txt").then((content) => { 536 | console.log(content); 537 | }); 538 | 539 | // if the callback does not go at the end, you can wrap the call 540 | fromCallback((cb) => foo("bar", cb, "baz")).then(() => { 541 | // ... 542 | }); 543 | 544 | // you can use `.call` to specify the context of execution 545 | fromCallback.call(thisArg, fn, ...args).then(() => { 546 | // ... 547 | }); 548 | 549 | // finally, if you want to call a method, you can pass its name instead of a 550 | // function 551 | fromCallback.call(object, "method", ...args).then(() => { 552 | // ... 553 | }); 554 | ``` 555 | 556 | #### fromEvent(emitter, event, [options]) => Promise 557 | 558 | > Wait for one event. The first parameter of the emitted event is used 559 | > to resolve/reject the promise. 560 | 561 | ```js 562 | const promise = fromEvent(emitter, "foo", { 563 | // whether the promise resolves to an array of all the event args 564 | // instead of simply the first arg 565 | array: false, 566 | 567 | // whether the error event can reject the promise 568 | ignoreErrors: false, 569 | 570 | // name of the error event 571 | error: "error", 572 | }); 573 | 574 | promise.then( 575 | (value) => { 576 | console.log("foo event was emitted with value", value); 577 | }, 578 | (reason) => { 579 | console.error("an error has been emitted", reason); 580 | } 581 | ); 582 | ``` 583 | 584 | #### fromEvents(emitter, successEvents, errorEvents) => Promise 585 | 586 | > Wait for one of multiple events. The array of all the parameters of 587 | > the emitted event is used to resolve/reject the promise. 588 | > 589 | > The array also has an `event` property indicating which event has 590 | > been emitted. 591 | 592 | ```js 593 | fromEvents(emitter, ["foo", "bar"], ["error1", "error2"]).then( 594 | (event) => { 595 | console.log( 596 | "event %s have been emitted with values", 597 | event.name, 598 | event.args 599 | ); 600 | }, 601 | (reasons) => { 602 | console.error( 603 | "error event %s has been emitted with errors", 604 | event.names, 605 | event.args 606 | ); 607 | } 608 | ); 609 | ``` 610 | 611 | #### isPromise(value) 612 | 613 | ```js 614 | import { isPromise } from "promise-toolbox"; 615 | 616 | if (isPromise(foo())) { 617 | console.log("foo() returns a promise"); 618 | } 619 | ``` 620 | 621 | #### nodeify(fn) 622 | 623 | > From async functions return promises, create new ones taking node-style 624 | > callbacks. 625 | 626 | ```js 627 | import { nodeify } = require('promise-toolbox') 628 | 629 | const writable = new Writable({ 630 | write: nodeify(async function (chunk, encoding) { 631 | // ... 632 | }) 633 | }) 634 | ``` 635 | 636 | #### pipe(fns) 637 | 638 | > Create a new function from the composition of async functions. 639 | 640 | ```js 641 | import { pipe } from "promise-toolbox"; 642 | 643 | const getUserPreferences = pipe(getUser, getPreferences); 644 | ``` 645 | 646 | #### pipe(value, ...fns) 647 | 648 | > Makes value flow through a list of async functions. 649 | 650 | ```js 651 | import { pipe } from "promise-toolbox"; 652 | 653 | const output = await pipe( 654 | input, // plain value or promise 655 | transform1, // sync or async function 656 | transform2, 657 | transform3 658 | ); 659 | ``` 660 | 661 | #### promisify(fn, [ context ]) / promisifyAll(obj) 662 | 663 | > From async functions taking node-style callbacks, create new ones 664 | > returning promises. 665 | 666 | ```js 667 | import fs from "fs"; 668 | import { promisify, promisifyAll } from "promise-toolbox"; 669 | 670 | // Promisify a single function. 671 | // 672 | // If possible, the function name is kept and the new length is set. 673 | const readFile = promisify(fs.readFile); 674 | 675 | // Or all functions (own or inherited) exposed on a object. 676 | const fsPromise = promisifyAll(fs); 677 | 678 | readFile(__filename).then((content) => console.log(content)); 679 | 680 | fsPromise.readFile(__filename).then((content) => console.log(content)); 681 | ``` 682 | 683 | #### retry(fn, options, [args]) 684 | 685 | > Retries an async function when it fails. 686 | 687 | ```js 688 | import { retry } from "promise-toolbox"; 689 | 690 | (async () => { 691 | await retry( 692 | async () => { 693 | const response = await fetch("https://pokeapi.co/api/v2/pokemon/3/"); 694 | 695 | if (response.status === 500) { 696 | // no need to retry in this case 697 | throw retry.bail(new Error(response.statusText)); 698 | } 699 | 700 | if (response.status !== 200) { 701 | throw new Error(response.statusText); 702 | } 703 | 704 | return response.json(); 705 | }, 706 | { 707 | // predicate when to retry, default on always but programmer errors 708 | // (ReferenceError, SyntaxError and TypeError) 709 | // 710 | // similar to `promise-toolbox/catch`, it can be a constructor, an object, 711 | // a function, or an array of the previous 712 | when: { message: "my error message" }, 713 | 714 | // this function is called before a retry is scheduled (before the delay) 715 | async onRetry(error) { 716 | console.warn("attempt", this.attemptNumber, "failed with error", error); 717 | console.warn("next try in", this.delay, "milliseconds"); 718 | 719 | // Other information available: 720 | // - this.fn: function that failed 721 | // - this.arguments: arguments passed to fn 722 | // - this.this: context passed to fn 723 | 724 | // This function can throw to prevent any retry. 725 | 726 | // The retry delay will start only after this function has finished. 727 | }, 728 | 729 | // delay before a retry, default to 1000 ms 730 | delay: 2000, 731 | 732 | // number of tries including the first one, default to 10 733 | // 734 | // cannot be used with `retries` 735 | tries: 3, 736 | 737 | // number of retries (excluding the initial run), default to undefined 738 | // 739 | // cannot be used with `tries` 740 | retries: 4, 741 | 742 | // instead of passing `delay`, `tries` and `retries`, you can pass an 743 | // iterable of delays to use to retry 744 | // 745 | // in this example, it will retry 3 times, first after 1 second, then 746 | // after 2 seconds and one last time after 4 seconds 747 | // 748 | // for more advanced uses, see https://github.com/JsCommunity/iterable-backoff 749 | delays: [1e3, 2e3, 4e3], 750 | } 751 | ); 752 | })().catch(console.error.bind(console)); 753 | ``` 754 | 755 | The most efficient way to make a function automatically retry is to wrap it: 756 | 757 | ```js 758 | MyClass.prototype.myMethod = retry.wrap(MyClass.prototype.myMethod, { 759 | delay: 1e3, 760 | retries: 10, 761 | when: MyError, 762 | }); 763 | ``` 764 | 765 | In that case `options` can also be a function which will be used to compute the options from the context and the arguments: 766 | 767 | ```js 768 | MyClass.prototype.myMethod = retry.wrap( 769 | MyClass.prototype.myMethod, 770 | function getOptions(arg1, arg2) { 771 | return this._computeRetryOptions(arg1, arg2); 772 | } 773 | ); 774 | ``` 775 | 776 | #### try(fn) 777 | 778 | > Starts a chain of promises. 779 | 780 | ```js 781 | import PromiseToolbox from "promise-toolbox"; 782 | 783 | const getUserById = (id) => 784 | PromiseToolbox.try(() => { 785 | if (typeof id !== "number") { 786 | throw new Error("id must be a number"); 787 | } 788 | return db.getUserById(id); 789 | }); 790 | ``` 791 | 792 | > Note: similar to `Promise.resolve().then(fn)` but calls `fn()` 793 | > synchronously. 794 | 795 | #### wrapApply(fn, args, [thisArg]) / wrapCall(fn, arg, [thisArg]) 796 | 797 | > Wrap a call to a function to always return a promise. 798 | 799 | ```js 800 | function getUserById(id) { 801 | if (typeof id !== "number") { 802 | throw new TypeError("id must be a number"); 803 | } 804 | return db.getUser(id); 805 | } 806 | 807 | wrapCall(getUserById, "foo").catch((error) => { 808 | // id must be a number 809 | }); 810 | ``` 811 | 812 | ### Pseudo-methods 813 | 814 | This function can be used as if they were methods, i.e. by passing the 815 | promise (or promises) as the context. 816 | 817 | This is extremely easy using [ES2016's bind syntax](https://github.com/zenparsing/es-function-bind). 818 | 819 | ```js 820 | const promises = [Promise.resolve("foo"), Promise.resolve("bar")]; 821 | 822 | promises::all().then((values) => { 823 | console.log(values); 824 | }); 825 | // → [ 'foo', 'bar' ] 826 | ``` 827 | 828 | If you are still an older version of ECMAScript, fear not: simply pass 829 | the promise (or promises) as the first argument of the `.call()` 830 | method: 831 | 832 | ```js 833 | const promises = [Promise.resolve("foo"), Promise.resolve("bar")]; 834 | 835 | all.call(promises).then(function (values) { 836 | console.log(values); 837 | }); 838 | // → [ 'foo', 'bar' ] 839 | ``` 840 | 841 | #### promise::asCallback(cb) 842 | 843 | > Register a node-style callback on this promise. 844 | 845 | ```js 846 | import { asCallback } from "promise-toolbox"; 847 | 848 | // This function can be used either with node-style callbacks or with 849 | // promises. 850 | function getDataFor(input, callback) { 851 | return dataFromDataBase(input)::asCallback(callback); 852 | } 853 | ``` 854 | 855 | #### promise::catch(predicate, cb) 856 | 857 | > Similar to `Promise#catch()` but: 858 | > 859 | > - support predicates 860 | > - do not catch `ReferenceError`, `SyntaxError` or `TypeError` unless 861 | > they match a predicate because they are usually programmer errors 862 | > and should be handled separately. 863 | 864 | ```js 865 | somePromise 866 | .then(() => { 867 | return a.b.c.d(); 868 | }) 869 | ::pCatch(TypeError, ReferenceError, (reason) => { 870 | // Will end up here on programmer error 871 | }) 872 | ::pCatch(NetworkError, TimeoutError, (reason) => { 873 | // Will end up here on expected everyday network errors 874 | }) 875 | ::pCatch((reason) => { 876 | // Catch any unexpected errors 877 | }); 878 | ``` 879 | 880 | #### promise::delay(ms, [value]) 881 | 882 | > Delays the resolution of a promise by `ms` milliseconds. 883 | > 884 | > Note: the rejection is not delayed. 885 | 886 | ```js 887 | console.log(await Promise.resolve("500ms passed")::delay(500)); 888 | // → 500 ms passed 889 | ``` 890 | 891 | Also works with a value: 892 | 893 | ```js 894 | console.log(await delay(500, "500ms passed")); 895 | // → 500 ms passed 896 | ``` 897 | 898 | Like `setTimeout` in Node, it is possible to 899 | [`unref`](https://nodejs.org/dist/latest-v11.x/docs/api/timers.html#timers_timeout_unref) 900 | the timer: 901 | 902 | ```js 903 | await delay(500).unref(); 904 | ``` 905 | 906 | #### collection::forEach(cb) 907 | 908 | > Iterates in order over a collection, or promise of collection, which 909 | > contains a mix of promises and values, waiting for each call of cb 910 | > to be resolved before the next one. 911 | 912 | The returned promise will resolve to `undefined` when the iteration is 913 | complete. 914 | 915 | ```js 916 | ["foo", Promise.resolve("bar")]::forEach((value) => { 917 | console.log(value); 918 | 919 | // Wait for the promise to be resolve before the next item. 920 | return new Promise((resolve) => setTimeout(resolve, 10)); 921 | }); 922 | // → 923 | // foo 924 | // bar 925 | ``` 926 | 927 | #### promise::ignoreErrors() 928 | 929 | > Ignore (operational) errors for this promise. 930 | 931 | ```js 932 | import { ignoreErrors } from "promise-toolbox"; 933 | 934 | // will not emit an unhandled rejection error if the file does not 935 | // exist 936 | readFileAsync("foo.txt") 937 | .then((content) => { 938 | console.log(content); 939 | }) 940 | ::ignoreErrors(); 941 | 942 | // will emit an unhandled rejection error due to the typo 943 | readFileAsync("foo.txt") 944 | .then((content) => { 945 | console.lgo(content); // typo 946 | }) 947 | ::ignoreErrors(); 948 | ``` 949 | 950 | #### promise::finally(cb) 951 | 952 | > Execute a handler regardless of the promise fate. Similar to the 953 | > `finally` block in synchronous codes. 954 | > 955 | > The resolution value or rejection reason of the initial promise is 956 | > forwarded unless the callback rejects. 957 | 958 | ```js 959 | import { pFinally } from "promise-toolbox"; 960 | 961 | function ajaxGetAsync(url) { 962 | return new Promise((resolve, reject) => { 963 | const xhr = new XMLHttpRequest(); 964 | xhr.addEventListener("error", reject); 965 | xhr.addEventListener("load", resolve); 966 | xhr.open("GET", url); 967 | xhr.send(null); 968 | })::pFinally(() => { 969 | $("#ajax-loader-animation").hide(); 970 | }); 971 | } 972 | ``` 973 | 974 | #### promise::reflect() 975 | 976 | > Returns a promise which resolves to an objects which reflects the 977 | > resolution of this promise. 978 | 979 | ```js 980 | import { reflect } from "promise-toolbox"; 981 | 982 | const inspection = await promise::reflect(); 983 | 984 | if (inspection.isFulfilled()) { 985 | console.log(inspection.value()); 986 | } else { 987 | console.error(inspection.reason()); 988 | } 989 | ``` 990 | 991 | #### promises::some(count) 992 | 993 | > Waits for `count` promises in a collection to be resolved. 994 | 995 | ```js 996 | import { some } from "promise-toolbox"; 997 | 998 | const [first, seconds] = await [ 999 | ping("ns1.example.org"), 1000 | ping("ns2.example.org"), 1001 | ping("ns3.example.org"), 1002 | ping("ns4.example.org"), 1003 | ]::some(2); 1004 | ``` 1005 | 1006 | #### promise::suppressUnhandledRejections() 1007 | 1008 | > Suppress unhandled rejections, needed when error handlers are attached 1009 | > asynchronously after the promise has rejected. 1010 | > 1011 | > Similar to [`Bluebird#suppressUnhandledRejections()`](http://bluebirdjs.com/docs/api/suppressunhandledrejections.html). 1012 | 1013 | ```js 1014 | const promise = getUser()::suppressUnhandledRejections(); 1015 | $(document).on("ready", () => { 1016 | promise.catch((error) => { 1017 | console.error("error while getting user", error); 1018 | }); 1019 | }); 1020 | ``` 1021 | 1022 | #### promise::tap(onResolved, onRejected) 1023 | 1024 | > Like `.then()` but the original resolution/rejection is forwarded. 1025 | > 1026 | > Like `::finally()`, if the callback rejects, it takes over the 1027 | > original resolution/rejection. 1028 | 1029 | ```js 1030 | import { tap } from "promise-toolbox"; 1031 | 1032 | // Contrary to .then(), using ::tap() does not change the resolution 1033 | // value. 1034 | const promise1 = Promise.resolve(42)::tap((value) => { 1035 | console.log(value); 1036 | }); 1037 | 1038 | // Like .then, the second param is used in case of rejection. 1039 | const promise2 = Promise.reject(42)::tap(null, (reason) => { 1040 | console.error(reason); 1041 | }); 1042 | ``` 1043 | 1044 | #### promise::tapCatch(onRejected) 1045 | 1046 | > Alias to [`promise:tap(null, onRejected)`](#promisetaponresolved-onrejected). 1047 | 1048 | #### promise::timeout(ms, [cb or rejectionValue]) 1049 | 1050 | > Call a callback if the promise is still pending after `ms` 1051 | > milliseconds. Its resolution/rejection is forwarded. 1052 | > 1053 | > If the callback is omitted, the returned promise is rejected with a 1054 | > `TimeoutError`. 1055 | 1056 | ```js 1057 | import { timeout, TimeoutError } from "promise-toolbox"; 1058 | 1059 | await doLongOperation()::timeout(100, () => { 1060 | return doFallbackOperation(); 1061 | }); 1062 | 1063 | await doLongOperation()::timeout(100); 1064 | 1065 | await doLongOperation()::timeout( 1066 | 100, 1067 | new Error("the long operation has failed") 1068 | ); 1069 | ``` 1070 | 1071 | > Note: `0` is a special value which disable the timeout, useful if the delay is 1072 | > configurable in your app. 1073 | 1074 | ## Development 1075 | 1076 | ``` 1077 | # Install dependencies 1078 | > npm install 1079 | 1080 | # Run the tests 1081 | > npm test 1082 | 1083 | # Continuously compile 1084 | > npm run dev 1085 | 1086 | # Continuously run the tests 1087 | > npm run dev-test 1088 | 1089 | # Build for production 1090 | > npm run build 1091 | ``` 1092 | 1093 | ## Contributions 1094 | 1095 | Contributions are _very_ welcomed, either on the documentation or on 1096 | the code. 1097 | 1098 | You may: 1099 | 1100 | - report any [issue](https://github.com/JsCommunity/promise-toolbox/issues) 1101 | you've encountered; 1102 | - fork and create a pull request. 1103 | 1104 | ## License 1105 | 1106 | ISC © [Julien Fontanet](https://github.com/julien-f) 1107 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "promise-toolbox", 3 | "version": "0.21.0", 4 | "license": "ISC", 5 | "description": "Essential utils for promises", 6 | "keywords": [ 7 | "callback", 8 | "cancel", 9 | "cancellable", 10 | "cancelable", 11 | "cancellation", 12 | "cancelation", 13 | "token", 14 | "CancelToken", 15 | "compose", 16 | "delay", 17 | "event", 18 | "fromCallback", 19 | "fromEvent", 20 | "fromEvents", 21 | "nodeify", 22 | "pipe", 23 | "promise", 24 | "promisification", 25 | "promisify", 26 | "retry", 27 | "sleep", 28 | "thenification", 29 | "thenify", 30 | "timeout", 31 | "utils" 32 | ], 33 | "homepage": "https://github.com/JsCommunity/promise-toolbox", 34 | "bugs": "https://github.com/JsCommunity/promise-toolbox/issues", 35 | "repository": { 36 | "type": "git", 37 | "url": "https://github.com/JsCommunity/promise-toolbox" 38 | }, 39 | "author": { 40 | "name": "Julien Fontanet", 41 | "email": "julien.fontanet@isonoe.net" 42 | }, 43 | "browserslist": [ 44 | ">2%" 45 | ], 46 | "engines": { 47 | "node": ">=6" 48 | }, 49 | "dependencies": { 50 | "make-error": "^1.3.2" 51 | }, 52 | "devDependencies": { 53 | "@babel/cli": "^7.0.0", 54 | "@babel/core": "^7.0.0", 55 | "@babel/eslint-parser": "^7.13.14", 56 | "@babel/plugin-proposal-function-bind": "^7.0.0", 57 | "@babel/preset-env": "^7.0.0", 58 | "babelify": "^10.0.0", 59 | "browserify": "^17.0.0", 60 | "cross-env": "^7.0.3", 61 | "eslint": "^7.25.0", 62 | "eslint-config-prettier": "^8.3.0", 63 | "eslint-config-standard": "^16.0.2", 64 | "eslint-plugin-import": "^2.22.1", 65 | "eslint-plugin-node": "^11.1.0", 66 | "eslint-plugin-promise": "^4.3.1", 67 | "husky": "^4.3.8", 68 | "jest": "^26.6.3", 69 | "lint-staged": "^10.5.4", 70 | "prettier": "^2.2.1", 71 | "rimraf": "^3.0.0", 72 | "terser": "^5.7.0" 73 | }, 74 | "scripts": { 75 | "build": "cross-env NODE_ENV=production babel --out-dir=./ src/", 76 | "clean": "rimraf '*.js' '*.js.map'", 77 | "dev": "cross-env NODE_ENV=development babel --watch --out-dir=./ src/", 78 | "dev-test": "jest --bail --watch", 79 | "postbuild": "browserify -s promiseToolbox index.js | terser -cm > umd.js", 80 | "prebuild": "npm run clean", 81 | "predev": "npm run prebuild", 82 | "prepublishOnly": "npm run build", 83 | "pretest": "eslint --ignore-path .gitignore src/", 84 | "test": "jest" 85 | }, 86 | "jest": { 87 | "testEnvironment": "node", 88 | "roots": [ 89 | "/src" 90 | ], 91 | "testRegex": "\\.spec\\.js$" 92 | }, 93 | "husky": { 94 | "hooks": { 95 | "pre-commit": "lint-staged" 96 | } 97 | }, 98 | "lint-staged": { 99 | "*.js": [ 100 | "echo", 101 | "prettier --write", 102 | "eslint --ignore-pattern '!*'", 103 | "jest --findRelatedTests --passWithNoTests" 104 | ] 105 | }, 106 | "exports": { 107 | ".": "./index.js", 108 | "./_*": null, 109 | "./.*": null, 110 | "./*": "./*.js", 111 | "./index": null, 112 | "./umd": null 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Cancel.js: -------------------------------------------------------------------------------- 1 | module.exports = class Cancel { 2 | constructor(message = "this action has been canceled") { 3 | Object.defineProperty(this, "message", { 4 | enumerable: true, 5 | value: message, 6 | }); 7 | } 8 | 9 | toString() { 10 | return `Cancel: ${this.message}`; 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /src/CancelToken.js: -------------------------------------------------------------------------------- 1 | const defer = require("./defer"); 2 | const Cancel = require("./Cancel"); 3 | const isPromise = require("./isPromise"); 4 | const noop = require("./_noop"); 5 | const { $$toStringTag } = require("./_symbols"); 6 | 7 | const cancelTokenTag = "CancelToken"; 8 | 9 | function cancel(message) { 10 | if (this._reason !== undefined) { 11 | return; 12 | } 13 | 14 | const reason = (this._reason = 15 | message instanceof Cancel ? message : new Cancel(message)); 16 | 17 | const resolve = this._resolve; 18 | if (resolve !== undefined) { 19 | this._resolve = undefined; 20 | resolve(reason); 21 | } 22 | 23 | const { onabort } = this; 24 | if (typeof onabort === "function") { 25 | onabort(); 26 | } 27 | 28 | const handlers = this._handlers; 29 | if (handlers !== undefined) { 30 | this._handlers = undefined; 31 | 32 | const { promise, resolve } = defer(); 33 | let wait = 0; 34 | const onSettle = () => { 35 | if (--wait === 0) { 36 | return resolve(); 37 | } 38 | }; 39 | for (let i = 0, n = handlers.length; i < n; ++i) { 40 | try { 41 | const result = handlers[i](reason); 42 | if (isPromise(result)) { 43 | ++wait; 44 | result.then(onSettle, onSettle); 45 | } 46 | } catch (_) {} 47 | } 48 | if (wait !== 0) { 49 | return promise; 50 | } 51 | } 52 | } 53 | 54 | function cancelFromSignal(signal) { 55 | cancel.call(this, signal.reason); 56 | } 57 | 58 | function removeHandler(handler) { 59 | const handlers = this._handlers; 60 | if (handlers !== undefined) { 61 | const i = handlers.indexOf(handler); 62 | if (i !== -1) { 63 | handlers.splice(i, 1); 64 | } 65 | } 66 | } 67 | 68 | const INTERNAL = {}; 69 | 70 | function CancelTokenSource(tokens) { 71 | const cancel_ = (this.cancel = cancel.bind( 72 | (this.token = new CancelToken(INTERNAL)) 73 | )); 74 | 75 | if (tokens == null) { 76 | return; 77 | } 78 | 79 | tokens.forEach((token) => { 80 | const { reason } = token; 81 | if (reason !== undefined) { 82 | cancel_(reason); 83 | return false; 84 | } 85 | 86 | token.addHandler(cancel_); 87 | }); 88 | } 89 | 90 | // https://github.com/zenparsing/es-cancel-token 91 | // https://tc39.github.io/proposal-cancelable-promises/ 92 | class CancelToken { 93 | static from(abortSignal) { 94 | if (CancelToken.isCancelToken(abortSignal)) { 95 | return abortSignal; 96 | } 97 | 98 | const token = new CancelToken(INTERNAL); 99 | abortSignal.addEventListener( 100 | "abort", 101 | cancelFromSignal.bind(token, abortSignal) 102 | ); 103 | return token; 104 | } 105 | 106 | static isCancelToken(value) { 107 | return value != null && value[$$toStringTag] === cancelTokenTag; 108 | } 109 | 110 | // https://github.com/zenparsing/es-cancel-token/issues/3#issuecomment-221173214 111 | static source(tokens) { 112 | return new CancelTokenSource(tokens); 113 | } 114 | 115 | static timeout(delay) { 116 | return new CancelToken((cancel) => { 117 | setTimeout(cancel, delay, "TimeoutError"); 118 | }); 119 | } 120 | 121 | constructor(executor) { 122 | this._handlers = undefined; 123 | this._promise = undefined; 124 | this._reason = undefined; 125 | this._resolve = undefined; 126 | this.onabort = undefined; 127 | 128 | if (executor !== INTERNAL) { 129 | executor(cancel.bind(this)); 130 | } 131 | } 132 | 133 | get promise() { 134 | let promise = this._promise; 135 | if (promise === undefined) { 136 | const reason = this._reason; 137 | promise = this._promise = 138 | reason !== undefined 139 | ? Promise.resolve(reason) 140 | : new Promise((resolve) => { 141 | this._resolve = resolve; 142 | }); 143 | } 144 | return promise; 145 | } 146 | 147 | get reason() { 148 | return this._reason; 149 | } 150 | 151 | get requested() { 152 | return this._reason !== undefined; 153 | } 154 | 155 | // register a handler to execute when the token is canceled 156 | // 157 | // handlers are all executed in parallel, the promise returned by 158 | // the `cancel` function will wait for all handlers to be settled 159 | addHandler(handler) { 160 | let handlers = this._handlers; 161 | if (handlers === undefined) { 162 | if (this.requested) { 163 | throw new TypeError( 164 | "cannot add a handler to an already canceled token" 165 | ); 166 | } 167 | 168 | handlers = this._handlers = []; 169 | } 170 | handlers.push(handler); 171 | return removeHandler.bind(this, handler); 172 | } 173 | 174 | throwIfRequested() { 175 | const reason = this._reason; 176 | if (reason !== undefined) { 177 | throw reason; 178 | } 179 | } 180 | 181 | get [$$toStringTag]() { 182 | return cancelTokenTag; 183 | } 184 | 185 | // ---- 186 | // Minimal AbortSignal compatibility 187 | // ---- 188 | 189 | get aborted() { 190 | return this.requested; 191 | } 192 | 193 | addEventListener(type, listener) { 194 | if (type !== "abort") { 195 | return; 196 | } 197 | 198 | const event = { type: "abort" }; 199 | const handler = 200 | typeof listener === "function" 201 | ? () => listener(event) 202 | : () => listener.handleEvent(event); 203 | handler.listener = listener; 204 | 205 | this.addHandler(handler); 206 | } 207 | 208 | removeEventListener(type, listener) { 209 | if (type !== "abort") { 210 | return; 211 | } 212 | 213 | const handlers = this._handlers; 214 | if (handlers !== undefined) { 215 | const i = handlers.findIndex((handler) => handler.listener === listener); 216 | if (i !== -1) { 217 | handlers.splice(i, 1); 218 | } 219 | } 220 | } 221 | } 222 | cancel.call((CancelToken.canceled = new CancelToken(INTERNAL))); 223 | 224 | CancelToken.none = new CancelToken(INTERNAL); 225 | 226 | // Never add handlers to this special token 227 | // 228 | // It would create a memory leak because this token never get canceled, and 229 | // therefore the handlers will never get removed. 230 | CancelToken.none.addHandler = function addHandler(handler) { 231 | return noop; 232 | }; 233 | CancelToken.none._promise = { 234 | catch() { 235 | return this; 236 | }, 237 | then() { 238 | return this; 239 | }, 240 | }; 241 | 242 | module.exports = CancelToken; 243 | -------------------------------------------------------------------------------- /src/CancelToken.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | const Cancel = require("./Cancel"); 4 | const CancelToken = require("./CancelToken"); 5 | const noop = require("./_noop"); 6 | 7 | describe("Cancel", () => { 8 | it("accept a message", () => { 9 | const cancel = new Cancel("foo"); 10 | expect(cancel.message).toBe("foo"); 11 | }); 12 | 13 | it("#toString()", () => { 14 | const cancel = new Cancel("foo"); 15 | expect(String(cancel)).toBe("Cancel: foo"); 16 | }); 17 | }); 18 | 19 | // ------------------------------------------------------------------- 20 | 21 | describe("CancelToken", () => { 22 | describe(".from()", () => { 23 | it("returns arg if already a CancelToken", () => { 24 | expect(CancelToken.from(CancelToken.none)).toBe(CancelToken.none); 25 | }); 26 | 27 | if ("AbortController" in global) { 28 | it("creates a CancelToken from an AbortSignal", () => { 29 | const controller = new global.AbortController(); 30 | const token = CancelToken.from(controller.signal); 31 | 32 | expect(token).toBeInstanceOf(CancelToken); 33 | expect(token.requested).toBe(false); 34 | 35 | controller.abort(); 36 | 37 | expect(token.requested).toBe(true); 38 | }); 39 | } 40 | }); 41 | 42 | describe(".isCancelToken()", () => { 43 | it("determines whether the passed value is a CancelToken", () => { 44 | expect(CancelToken.isCancelToken(null)).toBe(false); 45 | expect(CancelToken.isCancelToken({})).toBe(false); 46 | expect(CancelToken.isCancelToken(new CancelToken(noop))).toBe(true); 47 | }); 48 | }); 49 | 50 | describe(".source()", () => { 51 | it("creates a new token", () => { 52 | const { cancel, token } = CancelToken.source(); 53 | 54 | expect(token.requested).toBe(false); 55 | cancel(); 56 | expect(token.requested).toBe(true); 57 | }); 58 | 59 | it("creates a token which resolves when the passed one does", () => { 60 | const { cancel, token } = CancelToken.source(); 61 | const { token: fork } = CancelToken.source([token]); 62 | 63 | expect(fork.requested).toBe(false); 64 | cancel(); 65 | expect(fork.requested).toBe(true); 66 | }); 67 | 68 | it("creates a token which resolves when the cancel is called", () => { 69 | const { token } = CancelToken.source(); 70 | const { cancel, token: fork } = CancelToken.source([token]); 71 | 72 | expect(fork.requested).toBe(false); 73 | cancel(); 74 | expect(fork.requested).toBe(true); 75 | }); 76 | }); 77 | 78 | describe("#promise", () => { 79 | it("returns a promise resolving on cancel", async () => { 80 | const { cancel, token } = CancelToken.source(); 81 | 82 | const { promise } = token; 83 | cancel("foo"); 84 | 85 | const value = await promise; 86 | expect(value).toBeInstanceOf(Cancel); 87 | expect(value.message).toBe("foo"); 88 | }); 89 | }); 90 | 91 | describe("#reason", () => { 92 | it("synchronously returns the cancelation reason", () => { 93 | const { cancel, token } = CancelToken.source(); 94 | 95 | expect(token.reason).toBeUndefined(); 96 | cancel("foo"); 97 | expect(token.reason.message).toBe("foo"); 98 | }); 99 | }); 100 | 101 | describe("#requested", () => { 102 | it("synchronously returns whether cancelation has been requested", () => { 103 | const { cancel, token } = CancelToken.source(); 104 | 105 | expect(token.requested).toBe(false); 106 | cancel(); 107 | expect(token.requested).toBe(true); 108 | }); 109 | }); 110 | 111 | describe("#throwIfRequested()", () => { 112 | it("synchronously throws if cancelation has been requested", () => { 113 | const { cancel, token } = CancelToken.source(); 114 | 115 | token.throwIfRequested(); 116 | cancel("foo"); 117 | try { 118 | token.throwIfRequested(); 119 | expect(false).toBe("should have thrown"); 120 | } catch (error) { 121 | expect(error).toBeInstanceOf(Cancel); 122 | expect(error.message).toBe("foo"); 123 | } 124 | }); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /src/Disposable.js: -------------------------------------------------------------------------------- 1 | const evalDisposable = require("./_evalDisposable"); 2 | const isDisposable = require("./_isDisposable"); 3 | const pFinally = require("./_finally"); 4 | const setFunctionNameAndLength = require("./_setFunctionNameAndLength"); 5 | const wrapApply = require("./wrapApply"); 6 | const wrapCall = require("./wrapCall"); 7 | 8 | class Disposable { 9 | constructor(dispose, value) { 10 | if (typeof dispose !== "function") { 11 | throw new Error("dispose must be a function"); 12 | } 13 | 14 | this._dispose = dispose; 15 | this._value = value; 16 | } 17 | 18 | get value() { 19 | if (this._dispose === undefined) { 20 | throw new TypeError("cannot get value of already disposed disposable"); 21 | } 22 | return this._value; 23 | } 24 | 25 | dispose() { 26 | if (this._dispose === undefined) { 27 | throw new TypeError("cannot dispose already disposed disposable"); 28 | } 29 | const d = this._dispose; 30 | this._dispose = this._value = undefined; 31 | return d(); 32 | } 33 | } 34 | module.exports = Disposable; 35 | 36 | Disposable.all = function all(iterable) { 37 | let disposables = []; 38 | const dispose = () => { 39 | const d = disposables; 40 | disposables = undefined; 41 | d.forEach((disposable) => disposable.dispose()); 42 | }; 43 | const onFulfill = (maybeDisposable) => { 44 | if (disposables === undefined) { 45 | return isDisposable(maybeDisposable) && maybeDisposable.dispose(); 46 | } 47 | 48 | if (isDisposable(maybeDisposable)) { 49 | disposables.push(maybeDisposable); 50 | return maybeDisposable.value; 51 | } 52 | return maybeDisposable; 53 | }; 54 | const onReject = (error) => { 55 | if (disposables === undefined) { 56 | return; 57 | } 58 | 59 | dispose(); 60 | throw error; 61 | }; 62 | return Promise.all( 63 | Array.from(iterable, (maybeDisposable) => 64 | evalDisposable(maybeDisposable).then(onFulfill, onReject) 65 | ) 66 | ).then((values) => new Disposable(dispose, values)); 67 | }; 68 | 69 | // Requires this circular dependency as late as possible to avoid problems with jest 70 | const ExitStack = require("./_ExitStack"); 71 | 72 | // inspired by 73 | // 74 | // - https://github.com/tc39/proposal-explicit-resource-management 75 | // - https://book.pythontips.com/en/latest/context_managers.html 76 | Disposable.factory = (genFn) => 77 | setFunctionNameAndLength( 78 | function () { 79 | const gen = genFn.apply(this, arguments); 80 | 81 | const { dispose, value: stack } = new ExitStack(); 82 | 83 | const onEvalDisposable = (value) => 84 | isDisposable(value) ? loop(stack.enter(value)) : value; 85 | const onFulfill = ({ value }) => 86 | evalDisposable(value).then(onEvalDisposable); 87 | const loop = (value) => wrapCall(gen.next, value, gen).then(onFulfill); 88 | 89 | return loop().then( 90 | (value) => 91 | new Disposable( 92 | () => wrapCall(gen.return, undefined, gen).then(dispose), 93 | value 94 | ), 95 | (error) => { 96 | const forwardError = () => { 97 | throw error; 98 | }; 99 | return dispose().then(forwardError, forwardError); 100 | } 101 | ); 102 | }, 103 | genFn.name, 104 | genFn.length 105 | ); 106 | 107 | const onHandlerFulfill = (result) => { 108 | const { dispose, value: stack } = new ExitStack(); 109 | 110 | const onEvalDisposable = (disposable) => loop(stack.enter(disposable)); 111 | const onFulfill = (cursor) => 112 | cursor.done 113 | ? cursor.value 114 | : evalDisposable(cursor.value).then(onEvalDisposable); 115 | const loop = (value) => wrapCall(result.next, value, result).then(onFulfill); 116 | 117 | return pFinally(loop(), dispose); 118 | }; 119 | 120 | // Usage: 121 | // Disposable.use(maybeDisposable…, handler) 122 | // Disposable.use(maybeDisposable[], handler) 123 | Disposable.use = function use() { 124 | let nDisposables = arguments.length - 1; 125 | 126 | if (nDisposables < 0) { 127 | throw new TypeError("Disposable.use expects at least 1 argument"); 128 | } 129 | 130 | const handler = arguments[nDisposables]; 131 | 132 | if (nDisposables === 0) { 133 | return new Promise((resolve) => resolve(handler.call(this))).then( 134 | onHandlerFulfill 135 | ); 136 | } 137 | 138 | let disposables; 139 | const spread = !Array.isArray((disposables = arguments[0])); 140 | if (spread) { 141 | disposables = Array.prototype.slice.call(arguments, 0, nDisposables); 142 | } else { 143 | nDisposables = disposables.length; 144 | } 145 | 146 | return Disposable.all(disposables).then((dAll) => 147 | pFinally((spread ? wrapApply : wrapCall)(handler, dAll.value, this), () => 148 | dAll.dispose() 149 | ) 150 | ); 151 | }; 152 | 153 | Disposable.wrap = function wrap(generator) { 154 | return setFunctionNameAndLength( 155 | function () { 156 | return Disposable.use(() => generator.apply(this, arguments)); 157 | }, 158 | generator.name, 159 | generator.length 160 | ); 161 | }; 162 | -------------------------------------------------------------------------------- /src/Disposable.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | const Disposable = require("./Disposable"); 4 | const { reject } = require("./fixtures"); 5 | 6 | const d = () => ({ dispose: jest.fn(), value: Math.random() }); 7 | 8 | describe("Disposable", () => { 9 | it("cannot be used after being disposed of", async () => { 10 | const { dispose, value } = d(); 11 | const disposable = new Disposable(dispose, value); 12 | 13 | expect(disposable.value).toBe(value); 14 | expect(dispose).toHaveBeenCalledTimes(0); 15 | 16 | disposable.dispose(); 17 | 18 | expect(dispose).toHaveBeenCalledTimes(1); 19 | 20 | expect(() => disposable.value).toThrow( 21 | "cannot get value of already disposed disposable" 22 | ); 23 | expect(() => disposable.dispose()).toThrow( 24 | "cannot dispose already disposed disposable" 25 | ); 26 | }); 27 | 28 | describe(".all()", () => { 29 | it("combine multiple disposables", async () => { 30 | const d1 = d(); 31 | const d2 = d(); 32 | 33 | await Disposable.use(function* () { 34 | expect(yield Disposable.all([d1, d2])).toEqual([d1.value, d2.value]); 35 | expect(d1.dispose).not.toHaveBeenCalled(); 36 | expect(d2.dispose).not.toHaveBeenCalled(); 37 | }); 38 | 39 | expect(d1.dispose).toHaveBeenCalledTimes(1); 40 | expect(d2.dispose).toHaveBeenCalledTimes(1); 41 | }); 42 | }); 43 | 44 | describe(".factory()", () => { 45 | it("creates a disposable factory from a generator function", async () => { 46 | let disposed = false; 47 | const value = {}; 48 | 49 | const dep1 = d(); 50 | const dep2 = d(); 51 | const callArgs = [{}, {}]; 52 | const callThis = {}; 53 | const d1 = await Disposable.factory(function* (...args) { 54 | expect(args).toEqual(callArgs); 55 | expect(this).toBe(callThis); 56 | 57 | expect(yield dep1).toBe(dep1.value); 58 | expect(yield Promise.resolve(dep2)).toBe(dep2.value); 59 | try { 60 | yield value; 61 | } finally { 62 | // expect(dep2.dispose).not.toHaveBeenCalledTimes(1); 63 | expect(dep1.dispose).not.toHaveBeenCalledTimes(1); 64 | disposed = true; 65 | } 66 | }).apply(callThis, callArgs); 67 | 68 | await Disposable.use(function* () { 69 | expect(yield d1).toBe(value); 70 | expect(disposed).toBe(false); 71 | }); 72 | expect(disposed).toBe(true); 73 | }); 74 | 75 | it("supports returning the value if no dispose", async () => { 76 | const value = {}; 77 | const d1 = await Disposable.factory(function* () { 78 | return value; 79 | })(); 80 | await Disposable.use(function* () { 81 | expect(yield d1).toBe(value); 82 | }); 83 | }); 84 | }); 85 | describe(".use()", () => { 86 | it("called with flat params", async () => { 87 | const d1 = d(); 88 | const r1 = Promise.resolve(d1); 89 | const r2 = Promise.resolve("r2"); 90 | const r3 = "r3"; 91 | const handler = jest.fn(() => "handler"); 92 | 93 | expect(await Disposable.use(r1, r2, r3, handler)).toBe("handler"); 94 | expect(handler.mock.calls).toEqual([[d1.value, "r2", "r3"]]); 95 | expect(d1.dispose).toHaveBeenCalledTimes(1); 96 | }); 97 | 98 | it("called with array param", async () => { 99 | const d1 = d(); 100 | const p1 = Promise.resolve(d1); 101 | const p2 = Promise.resolve("p2"); 102 | const handler = jest.fn(() => "handler"); 103 | 104 | expect(await Disposable.use([p1, p2], handler)).toBe("handler"); 105 | expect(handler.mock.calls).toEqual([[[d1.value, "p2"]]]); 106 | expect(d1.dispose).toHaveBeenCalledTimes(1); 107 | }); 108 | 109 | it("error in a provider", async () => { 110 | const d1 = d(); 111 | const p1 = Promise.resolve(d1); 112 | const p2 = reject("p2"); 113 | const p3 = Promise.resolve("p3"); 114 | const handler = jest.fn(); 115 | 116 | await expect(Disposable.use(p1, p2, p3, handler)).rejects.toBe("p2"); 117 | expect(handler).not.toHaveBeenCalled(); 118 | expect(d1.dispose).toHaveBeenCalledTimes(1); 119 | }); 120 | 121 | it("error in handler", async () => { 122 | const d1 = d(); 123 | const p1 = Promise.resolve(d1); 124 | const d2 = d(); 125 | const p2 = Promise.resolve(d2); 126 | const p3 = Promise.resolve("p3"); 127 | const handler = jest.fn(() => reject("handler")); 128 | 129 | await expect(Disposable.use(p1, p2, p3, handler)).rejects.toBe("handler"); 130 | expect(handler.mock.calls).toEqual([[d1.value, d2.value, "p3"]]); 131 | expect(d1.dispose).toHaveBeenCalledTimes(1); 132 | expect(d2.dispose).toHaveBeenCalledTimes(1); 133 | }); 134 | 135 | it.skip("error in a disposer", () => {}); 136 | 137 | it("accepts a generator", async () => { 138 | const d1 = d(); 139 | const d2 = d(); 140 | const d3 = d(); 141 | const d4 = d(); 142 | 143 | expect( 144 | await Disposable.use(function* () { 145 | expect(yield d1).toBe(d1.value); 146 | expect(yield Promise.resolve(d2)).toBe(d2.value); 147 | expect(yield () => d3).toBe(d3.value); 148 | expect(yield () => Promise.resolve(d4)).toBe(d4.value); 149 | 150 | expect(d1.dispose).not.toHaveBeenCalled(); 151 | expect(d2.dispose).not.toHaveBeenCalled(); 152 | expect(d3.dispose).not.toHaveBeenCalled(); 153 | expect(d4.dispose).not.toHaveBeenCalled(); 154 | 155 | return "handler"; 156 | }) 157 | ).toBe("handler"); 158 | 159 | expect(d1.dispose).toHaveBeenCalledTimes(1); 160 | expect(d2.dispose).toHaveBeenCalledTimes(1); 161 | expect(d3.dispose).toHaveBeenCalledTimes(1); 162 | expect(d4.dispose).toHaveBeenCalledTimes(1); 163 | }); 164 | 165 | it("does not swallow a returned generator", async () => { 166 | const gen = (function* () {})(); 167 | expect(await Disposable.use(d(), () => gen)).toBe(gen); 168 | }); 169 | }); 170 | 171 | describe(".wrap()", () => { 172 | it("creates a disposable user from a generator function", async () => { 173 | const d1 = d(); 174 | 175 | const callArgs = [{}, {}]; 176 | const callThis = {}; 177 | 178 | const generator = function* foo(a, b) { 179 | expect(Array.from(arguments)).toEqual(callArgs); 180 | expect(this).toBe(callThis); 181 | 182 | expect(yield d1).toBe(d1.value); 183 | 184 | return "handler"; 185 | }; 186 | const wrapped = Disposable.wrap(generator); 187 | 188 | expect(wrapped.name).toBe(generator.name); 189 | expect(wrapped.length).toBe(generator.length); 190 | 191 | expect(await wrapped.apply(callThis, callArgs)).toBe("handler"); 192 | }); 193 | }); 194 | }); 195 | -------------------------------------------------------------------------------- /src/TimeoutError.js: -------------------------------------------------------------------------------- 1 | const { BaseError } = require("make-error"); 2 | 3 | module.exports = class TimeoutError extends BaseError { 4 | constructor() { 5 | super("operation timed out"); 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /src/_ExitStack.js: -------------------------------------------------------------------------------- 1 | const isDisposable = require("./_isDisposable"); 2 | const resolve = require("./_resolve"); 3 | 4 | // Inspired by https://docs.python.org/3/library/contextlib.html#contextlib.ExitStack 5 | module.exports = class ExitStack { 6 | constructor() { 7 | this._disposables = []; 8 | 9 | const dispose = () => { 10 | const disposable = this._disposables.pop(); 11 | return disposable !== undefined 12 | ? resolve(disposable.dispose()).then(dispose) 13 | : Promise.resolve(); 14 | }; 15 | return { dispose, value: this }; 16 | } 17 | 18 | enter(disposable) { 19 | if (!isDisposable(disposable)) { 20 | throw new TypeError("not a disposable"); 21 | } 22 | this._disposables.push(disposable); 23 | return disposable.value; 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /src/_evalDisposable.js: -------------------------------------------------------------------------------- 1 | const pTry = require("./try"); 2 | 3 | module.exports = (v) => 4 | typeof v === "function" ? pTry(v) : Promise.resolve(v); 5 | -------------------------------------------------------------------------------- /src/_finally.js: -------------------------------------------------------------------------------- 1 | // Usage: pFinally(promise, cb) 2 | const pFinally = (p, cb) => p.then(cb, cb).then(() => p); 3 | module.exports = pFinally; 4 | -------------------------------------------------------------------------------- /src/_identity.js: -------------------------------------------------------------------------------- 1 | module.exports = (value) => value; 2 | -------------------------------------------------------------------------------- /src/_isDisposable.js: -------------------------------------------------------------------------------- 1 | module.exports = (v) => v != null && typeof v.dispose === "function"; 2 | -------------------------------------------------------------------------------- /src/_isProgrammerError.js: -------------------------------------------------------------------------------- 1 | module.exports = (reason) => 2 | reason instanceof ReferenceError || 3 | reason instanceof SyntaxError || 4 | reason instanceof TypeError; 5 | -------------------------------------------------------------------------------- /src/_makeEventAdder.js: -------------------------------------------------------------------------------- 1 | const noop = require("./_noop"); 2 | const once = require("./_once"); 3 | 4 | module.exports = ($cancelToken, emitter, arrayArg) => { 5 | const add = emitter.addEventListener || emitter.addListener || emitter.on; 6 | if (add === undefined) { 7 | throw new Error("cannot register event listener"); 8 | } 9 | 10 | const remove = 11 | emitter.removeEventListener || emitter.removeListener || emitter.off; 12 | 13 | const eventsAndListeners = []; 14 | 15 | let clean = noop; 16 | if (remove !== undefined) { 17 | clean = once(() => { 18 | for (let i = 0, n = eventsAndListeners.length; i < n; i += 2) { 19 | remove.call(emitter, eventsAndListeners[i], eventsAndListeners[i + 1]); 20 | } 21 | }); 22 | $cancelToken.promise.then(clean); 23 | } 24 | 25 | return arrayArg 26 | ? (eventName, cb) => { 27 | function listener() { 28 | clean(); 29 | const event = Array.prototype.slice.call(arguments); 30 | event.args = event; 31 | event.event = event.name = eventName; 32 | cb(event); 33 | } 34 | eventsAndListeners.push(eventName, listener); 35 | add.call(emitter, eventName, listener); 36 | } 37 | : (event, cb) => { 38 | const listener = (arg) => { 39 | clean(); 40 | cb(arg); 41 | }; 42 | eventsAndListeners.push(event, listener); 43 | add.call(emitter, event, listener); 44 | }; 45 | }; 46 | -------------------------------------------------------------------------------- /src/_matchError.js: -------------------------------------------------------------------------------- 1 | const isProgrammerError = require("./_isProgrammerError"); 2 | 3 | module.exports = function matchError(predicate, error) { 4 | if (predicate === undefined) { 5 | return !isProgrammerError(error); 6 | } 7 | 8 | const type = typeof predicate; 9 | 10 | if (type === "boolean") { 11 | return predicate; 12 | } 13 | 14 | if (type === "function") { 15 | return predicate === Error || predicate.prototype instanceof Error 16 | ? error instanceof predicate 17 | : predicate(error); 18 | } 19 | 20 | if (Array.isArray(predicate)) { 21 | const n = predicate.length; 22 | for (let i = 0; i < n; ++i) { 23 | if (matchError(predicate[i], error)) { 24 | return true; 25 | } 26 | } 27 | return false; 28 | } 29 | 30 | if (error != null && type === "object") { 31 | for (const key in predicate) { 32 | if ( 33 | hasOwnProperty.call(predicate, key) && 34 | error[key] !== predicate[key] 35 | ) { 36 | return false; 37 | } 38 | } 39 | return true; 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /src/_noop.js: -------------------------------------------------------------------------------- 1 | module.exports = Function.prototype; 2 | -------------------------------------------------------------------------------- /src/_once.js: -------------------------------------------------------------------------------- 1 | module.exports = (fn) => { 2 | let result; 3 | return function () { 4 | if (fn !== undefined) { 5 | result = fn.apply(this, arguments); 6 | fn = undefined; 7 | } 8 | return result; 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /src/_resolve.js: -------------------------------------------------------------------------------- 1 | const isPromise = require("./isPromise"); 2 | module.exports = (value) => (isPromise(value) ? value : Promise.resolve(value)); 3 | -------------------------------------------------------------------------------- /src/_setFunctionNameAndLength.js: -------------------------------------------------------------------------------- 1 | module.exports = (() => { 2 | const _defineProperties = Object.defineProperties; 3 | 4 | try { 5 | const f = _defineProperties(function () {}, { 6 | length: { value: 2 }, 7 | name: { value: "foo" }, 8 | }); 9 | 10 | if (f.length === 2 && f.name === "foo") { 11 | return (fn, name, length) => 12 | _defineProperties(fn, { 13 | length: { 14 | configurable: true, 15 | value: length > 0 ? length : 0, 16 | }, 17 | name: { 18 | configurable: true, 19 | value: name, 20 | }, 21 | }); 22 | } 23 | } catch (_) {} 24 | 25 | return require("./_identity"); 26 | })(); 27 | -------------------------------------------------------------------------------- /src/_symbols.js: -------------------------------------------------------------------------------- 1 | const getSymbol = 2 | typeof Symbol === "function" 3 | ? (name) => { 4 | const symbol = Symbol[name]; 5 | return symbol !== undefined ? symbol : `@@${name}`; 6 | } 7 | : (name) => `@@${name}`; 8 | 9 | exports.$$iterator = getSymbol("iterator"); 10 | exports.$$toStringTag = getSymbol("toStringTag"); 11 | -------------------------------------------------------------------------------- /src/_utils.js: -------------------------------------------------------------------------------- 1 | if ( 2 | typeof Promise !== "function" || 3 | typeof Promise.reject !== "function" || 4 | typeof Promise.resolve !== "function" 5 | ) { 6 | throw new Error( 7 | "a standard Promise implementation is required (https://github.com/JsCommunity/promise-toolbox#usage)" 8 | ); 9 | } 10 | 11 | const isPromise = require("./isPromise"); 12 | const { $$iterator } = require("./_symbols"); 13 | 14 | const forArray = (exports.forArray = (array, iteratee) => { 15 | const { length } = array; 16 | for (let i = 0; i < length; ++i) { 17 | iteratee(array[i], i, array); 18 | } 19 | }); 20 | 21 | exports.forIn = (object, iteratee) => { 22 | for (const key in object) { 23 | iteratee(object[key], key, object); 24 | } 25 | }; 26 | 27 | const forIterable = (exports.forIterable = (iterable, iteratee) => { 28 | const iterator = iterable[$$iterator](); 29 | 30 | let current; 31 | while (!(current = iterator.next()).done) { 32 | iteratee(current.value, undefined, iterable); 33 | } 34 | }); 35 | 36 | const { hasOwnProperty } = Object.prototype; 37 | const forOwn = (exports.forOwn = (object, iteratee) => { 38 | for (const key in object) { 39 | if (hasOwnProperty.call(object, key)) { 40 | iteratee(object[key], key, object); 41 | } 42 | } 43 | }); 44 | 45 | const isIterable = (value) => 46 | value != null && typeof value[$$iterator] === "function"; 47 | 48 | const forEach = (exports.forEach = (collection, iteratee) => 49 | Array.isArray(collection) 50 | ? forArray(collection, iteratee) 51 | : isIterable(collection) 52 | ? forIterable(collection, iteratee) 53 | : isArrayLike(collection) 54 | ? forArray(collection, iteratee) 55 | : forOwn(collection, iteratee)); 56 | 57 | const isLength = (value) => 58 | typeof value === "number" && 59 | value >= 0 && 60 | value < Infinity && 61 | Math.floor(value) === value; 62 | 63 | const isArrayLike = (exports.isArrayLike = (value) => 64 | typeof value !== "function" && value != null && isLength(value.length)); 65 | 66 | exports.makeAsyncIterator = (iterator) => { 67 | const asyncIterator = (collection, iteratee) => { 68 | if (isPromise(collection)) { 69 | return collection.then((collection) => 70 | asyncIterator(collection, iteratee) 71 | ); 72 | } 73 | 74 | let mainPromise = Promise.resolve(); 75 | 76 | iterator(collection, (value, key) => { 77 | mainPromise = isPromise(value) 78 | ? mainPromise.then(() => 79 | value.then((value) => iteratee(value, key, collection)) 80 | ) 81 | : mainPromise.then(() => iteratee(value, key, collection)); 82 | }); 83 | 84 | return mainPromise; 85 | }; 86 | return asyncIterator; 87 | }; 88 | 89 | exports.map = (collection, iteratee) => { 90 | const result = []; 91 | forEach(collection, (item, key, collection) => { 92 | result.push(iteratee(item, key, collection)); 93 | }); 94 | return result; 95 | }; 96 | 97 | exports.mapAuto = (collection, iteratee) => { 98 | const result = isArrayLike(collection) 99 | ? new Array(collection.length) 100 | : Object.create(null); 101 | if (iteratee !== undefined) { 102 | forEach(collection, (item, key, collection) => { 103 | result[key] = iteratee(item, key, collection); 104 | }); 105 | } 106 | return result; 107 | }; 108 | -------------------------------------------------------------------------------- /src/asCallback.js: -------------------------------------------------------------------------------- 1 | // Usage: promise::asCallback(cb) 2 | module.exports = function asCallback(cb) { 3 | if (typeof cb === "function") { 4 | this.then((value) => cb(undefined, value), cb); 5 | } 6 | 7 | return this; 8 | }; 9 | -------------------------------------------------------------------------------- /src/asyncFn.js: -------------------------------------------------------------------------------- 1 | const identity = require("./_identity"); 2 | const isPromise = require("./isPromise"); 3 | const toPromise = require("./_resolve"); 4 | 5 | const noop = Function.prototype; 6 | 7 | function step(key, value) { 8 | let cursor; 9 | try { 10 | cursor = this._iterator[key](value); 11 | } catch (error) { 12 | this.finally(); 13 | return this._reject(error); 14 | } 15 | value = cursor.value; 16 | if (cursor.done) { 17 | this.finally(); 18 | this._resolve(value); 19 | } else { 20 | this.toPromise(value).then(this.next, this._throw); 21 | } 22 | } 23 | 24 | function AsyncFn(iterator, resolve, reject) { 25 | this._iterator = iterator; 26 | this._reject = reject; 27 | this._resolve = resolve; 28 | this._throw = step.bind(this, "throw"); 29 | this.next = step.bind(this, "next"); 30 | } 31 | 32 | AsyncFn.prototype.finally = noop; 33 | AsyncFn.prototype.toPromise = toPromise; 34 | 35 | const asyncFn = (generator) => 36 | function () { 37 | return new Promise((resolve, reject) => 38 | new AsyncFn(generator.apply(this, arguments), resolve, reject).next() 39 | ); 40 | }; 41 | 42 | function CancelabledAsyncFn(cancelToken) { 43 | AsyncFn.apply(this, [].slice.call(arguments, 1)); 44 | 45 | this._cancelToken = cancelToken; 46 | this._onCancel = noop; 47 | 48 | this.finally = cancelToken.addHandler((reason) => { 49 | this._onCancel(reason); 50 | return new Promise((resolve) => { 51 | this.finally = resolve; 52 | }); 53 | }); 54 | } 55 | Object.setPrototypeOf( 56 | CancelabledAsyncFn.prototype, 57 | Object.getPrototypeOf(AsyncFn.prototype) 58 | ).toPromise = function (value) { 59 | if (Array.isArray(value)) { 60 | return toPromise(value[0]); 61 | } 62 | 63 | const cancelToken = this._cancelToken; 64 | if (cancelToken.requested) { 65 | return Promise.reject(cancelToken.reason); 66 | } 67 | 68 | // TODO: add cancel handler to rest of the function 69 | return isPromise(value) 70 | ? new Promise((resolve, reject) => { 71 | value.then(resolve, reject); 72 | this._onCancel = reject; 73 | }) 74 | : Promise.resolve(value); 75 | }; 76 | 77 | asyncFn.cancelable = (generator, getCancelToken = identity) => 78 | function () { 79 | const cancelToken = getCancelToken.apply(this, arguments); 80 | if (cancelToken.requested) { 81 | return Promise.reject(cancelToken.reason); 82 | } 83 | return new Promise((resolve, reject) => { 84 | new CancelabledAsyncFn( 85 | cancelToken, 86 | generator.apply(this, arguments), 87 | resolve, 88 | reject 89 | ).next(); 90 | }); 91 | }; 92 | module.exports = asyncFn; 93 | 94 | // TODO: asyncFn.timeout? 95 | -------------------------------------------------------------------------------- /src/asyncFn.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | const asyncFn = require("./asyncFn"); 4 | const CancelToken = require("./CancelToken"); 5 | 6 | describe("asyncFn", () => { 7 | it("forwards this and args", async () => { 8 | const thisArg = {}; 9 | const args = ["foo", "bar"]; 10 | const f = asyncFn(function* () { 11 | expect(this).toBe(thisArg); 12 | expect(Array.from(arguments)).toEqual(args); 13 | return (yield Promise.resolve(1)) + 2; 14 | }); 15 | await f.apply(thisArg, args); 16 | }); 17 | 18 | it("makes promise resolution available via yield", async () => { 19 | const f = asyncFn(function* () { 20 | return (yield Promise.resolve(1)) + 2; 21 | }); 22 | expect(await f()).toBe(3); 23 | }); 24 | 25 | it("makes promise rejection available via yield", async () => { 26 | const f = asyncFn(function* (value) { 27 | try { 28 | yield Promise.reject(value); 29 | } catch (error) { 30 | return error; 31 | } 32 | }); 33 | expect(await f("foo")).toBe("foo"); 34 | }); 35 | }); 36 | 37 | describe("asyncFn.cancelable", () => { 38 | it("works", async () => { 39 | const { cancel, token } = CancelToken.source(); 40 | let canceled = false; 41 | 42 | const expectedThis = {}; 43 | const expectedArgs = [token, Math.random(), Math.random()]; 44 | const expectedResult = {}; 45 | const fn = asyncFn.cancelable(function* () { 46 | expect(this).toBe(expectedThis); 47 | expect(Array.from(arguments)).toEqual(expectedArgs); 48 | 49 | { 50 | const expectedValue = {}; 51 | expect(yield Promise.resolve(expectedValue)).toBe(expectedValue); 52 | } 53 | { 54 | const expectedError = {}; 55 | try { 56 | yield Promise.reject(expectedError); 57 | throw new Error(); 58 | } catch (error) { 59 | expect(error).toBe(expectedError); 60 | } 61 | } 62 | 63 | cancel().then(() => { 64 | canceled = true; 65 | }); 66 | 67 | try { 68 | yield Promise.resolve({}); 69 | } catch (error) { 70 | expect(error).toBe(token.reason); 71 | } 72 | 73 | { 74 | const expectedValue = {}; 75 | expect(yield [Promise.resolve(expectedValue)]).toBe(expectedValue); 76 | } 77 | 78 | yield [new Promise((resolve) => setImmediate(resolve))]; 79 | expect(canceled).toBe(false); 80 | 81 | return expectedResult; 82 | }); 83 | 84 | expect(await fn.apply(expectedThis, expectedArgs)).toBe(expectedResult); 85 | 86 | await new Promise((resolve) => setImmediate(resolve)); 87 | expect(canceled).toBe(true); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /src/cancelable.js: -------------------------------------------------------------------------------- 1 | const setFunctionNameAndLength = require("./_setFunctionNameAndLength"); 2 | const { isCancelToken, source } = require("./CancelToken"); 3 | 4 | /** 5 | * Usage: 6 | * 7 | * @cancelable 8 | * async fn (cancelToken, other, args) { 9 | * if (!cancelToken.requested) { 10 | * doStuff() 11 | * } 12 | * 13 | * cancelToken.throwIfRequested() 14 | * 15 | * doSomeMoreStuff() 16 | * 17 | * cancelToken.promise.then(() => { 18 | * // Do stuff if canceled. 19 | * }) 20 | * 21 | * // do other stuff. 22 | * } 23 | * 24 | * @deprecated explicitely pass a cancel token or an abort signal instead 25 | */ 26 | const cancelable = (target, name, descriptor) => { 27 | const fn = descriptor !== undefined ? descriptor.value : target; 28 | 29 | const wrapper = setFunctionNameAndLength( 30 | function cancelableWrapper() { 31 | const { length } = arguments; 32 | if (length !== 0 && isCancelToken(arguments[0])) { 33 | return fn.apply(this, arguments); 34 | } 35 | 36 | const { cancel, token } = source(); 37 | const args = new Array(length + 1); 38 | args[0] = token; 39 | for (let i = 0; i < length; ++i) { 40 | args[i + 1] = arguments[i]; 41 | } 42 | 43 | const promise = fn.apply(this, args); 44 | promise.cancel = cancel; 45 | 46 | return promise; 47 | }, 48 | fn.name, 49 | fn.length - 1 50 | ); 51 | 52 | if (descriptor !== undefined) { 53 | descriptor.value = wrapper; 54 | return descriptor; 55 | } 56 | 57 | return wrapper; 58 | }; 59 | module.exports = cancelable; 60 | -------------------------------------------------------------------------------- /src/cancelable.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | const cancelable = require("./cancelable"); 4 | const CancelToken = require("./CancelToken"); 5 | const noop = require("./_noop"); 6 | 7 | describe("@cancelable", () => { 8 | it("forwards params if a cancel token is passed", () => { 9 | const token = new CancelToken(noop); 10 | const spy = jest.fn(() => Promise.resolve()); 11 | 12 | cancelable(spy)(token, "foo", "bar"); 13 | expect(spy.mock.calls).toEqual([[token, "foo", "bar"]]); 14 | }); 15 | 16 | it("injects a cancel token and add the cancel method on the returned promise if none is passed", () => { 17 | const spy = jest.fn(() => Promise.resolve()); 18 | 19 | const promise = cancelable(spy)("foo", "bar"); 20 | expect(spy.mock.calls).toEqual([ 21 | [ 22 | { 23 | asymmetricMatch: (actual) => CancelToken.isCancelToken(actual), 24 | }, 25 | "foo", 26 | "bar", 27 | ], 28 | ]); 29 | const token = spy.mock.calls[0][0]; 30 | expect(token.requested).toBe(false); 31 | promise.cancel(); 32 | expect(token.requested).toBe(true); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/catch.js: -------------------------------------------------------------------------------- 1 | const matchError = require("./_matchError"); 2 | 3 | function handler(predicates, cb, reason) { 4 | return matchError(predicates, reason) ? cb(reason) : this; 5 | } 6 | 7 | // Similar to `Promise#catch()` but: 8 | // - support predicates 9 | // - do not catch `ReferenceError`, `SyntaxError` or `TypeError` 10 | // unless they match a predicate because they are usually programmer 11 | // errors and should be handled separately. 12 | module.exports = function pCatch() { 13 | let n = arguments.length; 14 | 15 | let cb; 16 | if (n === 0 || typeof (cb = arguments[--n]) !== "function") { 17 | return this; 18 | } 19 | 20 | return this.then( 21 | undefined, 22 | handler.bind( 23 | this, 24 | n === 0 25 | ? undefined 26 | : n === 1 27 | ? arguments[0] 28 | : Array.prototype.slice.call(arguments, 0, n), 29 | cb 30 | ) 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /src/catch.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | const makeError = require("make-error"); 4 | 5 | const pCatch = require("./catch"); 6 | const identity = require("./_identity"); 7 | const { reject } = require("./fixtures"); 8 | 9 | describe("catch", () => { 10 | it("catches errors matching a predicate", async () => { 11 | const predicate = (reason) => reason === "foo"; 12 | 13 | expect(await reject("foo")::pCatch(predicate, identity)).toBe("foo"); 14 | 15 | await expect(reject("bar")::pCatch(predicate, identity)).rejects.toBe( 16 | "bar" 17 | ); 18 | }); 19 | 20 | it("catches errors matching a class", async () => { 21 | const CustomError1 = makeError("CustomError1"); 22 | const CustomError2 = makeError("CustomError2"); 23 | 24 | const error = new CustomError1(); 25 | 26 | // The class itself. 27 | expect(await Promise.reject(error)::pCatch(CustomError1, identity)).toBe( 28 | error 29 | ); 30 | 31 | // A parent. 32 | expect(await Promise.reject(error)::pCatch(Error, identity)).toBe(error); 33 | 34 | // Another class. 35 | await expect( 36 | Promise.reject(error)::pCatch(CustomError2, identity) 37 | ).rejects.toBe(error); 38 | }); 39 | 40 | it("catches errors matching an object pattern", async () => { 41 | const predicate = { foo: 0 }; 42 | 43 | expect(await reject({ foo: 0 })::pCatch(predicate, identity)).toEqual({ 44 | foo: 0, 45 | }); 46 | 47 | await expect( 48 | reject({ foo: 1 })::pCatch(predicate, identity) 49 | ).rejects.toEqual({ foo: 1 }); 50 | 51 | await expect( 52 | reject({ bar: 0 })::pCatch(predicate, identity) 53 | ).rejects.toEqual({ bar: 0 }); 54 | }); 55 | 56 | it("does not catch programmer errors", async () => { 57 | await expect( 58 | Promise.reject(new ReferenceError(""))::pCatch(identity) 59 | ).rejects.toBeInstanceOf(ReferenceError); 60 | await expect( 61 | Promise.reject(new SyntaxError(""))::pCatch(identity) 62 | ).rejects.toBeInstanceOf(SyntaxError); 63 | await expect( 64 | Promise.reject(new TypeError(""))::pCatch(identity) 65 | ).rejects.toBeInstanceOf(TypeError); 66 | 67 | // Unless matches by a predicate. 68 | expect( 69 | await Promise.reject(new TypeError(""))::pCatch(TypeError, identity) 70 | ).toBeInstanceOf(TypeError); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /src/defer.js: -------------------------------------------------------------------------------- 1 | // Discouraged but sometimes necessary way to create a promise. 2 | const defer = () => { 3 | let resolve, reject; 4 | // eslint-disable-next-line promise/param-names 5 | const promise = new Promise((resolve_, reject_) => { 6 | resolve = resolve_; 7 | reject = reject_; 8 | }); 9 | 10 | return { 11 | promise, 12 | reject, 13 | resolve, 14 | }; 15 | }; 16 | module.exports = defer; 17 | -------------------------------------------------------------------------------- /src/delay.js: -------------------------------------------------------------------------------- 1 | const isPromise = require("./isPromise"); 2 | 3 | // Usage: promise::delay(ms, [value]) 4 | module.exports = function delay(ms) { 5 | const value = arguments.length === 2 ? arguments[1] : this; 6 | 7 | if (isPromise(value)) { 8 | return value.then( 9 | (value) => 10 | new Promise((resolve) => { 11 | setTimeout(resolve, ms, value); 12 | }) 13 | ); 14 | } 15 | 16 | let handle; 17 | const p = new Promise((resolve) => { 18 | handle = setTimeout(resolve, ms, value); 19 | }); 20 | p.unref = () => { 21 | if (handle != null && typeof handle.unref === "function") { 22 | handle.unref(); 23 | } 24 | return p; 25 | }; 26 | return p; 27 | }; 28 | -------------------------------------------------------------------------------- /src/finally.js: -------------------------------------------------------------------------------- 1 | // Ponyfill for Promise.finally(cb) 2 | // 3 | // Usage: promise::finally(cb) 4 | module.exports = function pFinally(cb) { 5 | return this.then(cb, cb).then(() => this); 6 | }; 7 | -------------------------------------------------------------------------------- /src/finally.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | const lastly = require("./finally"); 4 | 5 | describe("finally()", () => { 6 | it("calls a callback on resolution", async () => { 7 | const value = {}; 8 | const spy = jest.fn(); 9 | 10 | expect(await Promise.resolve(value)::lastly(spy)).toBe(value); 11 | 12 | expect(spy).toHaveBeenCalledTimes(1); 13 | }); 14 | 15 | it("calls a callback on rejection", async () => { 16 | const reason = {}; 17 | const spy = jest.fn(); 18 | 19 | await expect(Promise.reject(reason)::lastly(spy)).rejects.toBe(reason); 20 | 21 | expect(spy).toHaveBeenCalledTimes(1); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/fixtures.js: -------------------------------------------------------------------------------- 1 | exports.hideLiteralErrorFromLinter = (literal) => literal; 2 | exports.reject = (reason) => Promise.reject(reason); 3 | exports.throwArg = (value) => { 4 | throw value; 5 | }; 6 | -------------------------------------------------------------------------------- /src/forArray.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./makeAsyncIterator")(require("./_utils").forArray); 2 | -------------------------------------------------------------------------------- /src/forEach.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./makeAsyncIterator")(require("./_utils").forEach); 2 | -------------------------------------------------------------------------------- /src/forEach.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | const forEach = require("./forEach"); 4 | 5 | describe("forEach()", () => { 6 | it("iterates over an array of promises", async () => { 7 | const spy = jest.fn(); 8 | 9 | const array = [Promise.resolve("foo"), Promise.resolve("bar"), "baz"]; 10 | 11 | expect(await array::forEach(spy)).not.toBeDefined(); 12 | expect(await spy.mock.calls).toEqual([ 13 | ["foo", 0, array], 14 | ["bar", 1, array], 15 | ["baz", 2, array], 16 | ]); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/forIn.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./makeAsyncIterator")(require("./_utils").forIn); 2 | -------------------------------------------------------------------------------- /src/forIterable.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./makeAsyncIterator")( 2 | require("./_utils").forIterable 3 | ); 4 | -------------------------------------------------------------------------------- /src/forOwn.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./makeAsyncIterator")(require("./_utils").forOwn); 2 | -------------------------------------------------------------------------------- /src/fromCallback.js: -------------------------------------------------------------------------------- 1 | function resolver(fn, args, resolve, reject) { 2 | args.push((error, result) => 3 | error != null && error !== false ? reject(error) : resolve(result) 4 | ); 5 | fn.apply(this, args); 6 | } 7 | 8 | // Usage: 9 | // 10 | // fromCallback(fs.readFile, 'foo.txt') 11 | // .then(content => { 12 | // console.log(content) 13 | // }) 14 | // 15 | // fromCallback.call(obj, 'method', 'foo.txt') 16 | // .then(content => { 17 | // console.log(content) 18 | // }) 19 | module.exports = function fromCallback(fn, ...args) { 20 | return new Promise( 21 | resolver.bind(this, typeof fn === "function" ? fn : this[fn], args) 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /src/fromCallback.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | const fromCallback = require("./fromCallback"); 4 | const { hideLiteralErrorFromLinter } = require("./fixtures"); 5 | 6 | describe("fromCallback()", () => { 7 | it("creates a promise which resolves with value passed to the callback", async () => { 8 | expect(await fromCallback((cb) => cb(undefined, "foo"))).toBe("foo"); 9 | }); 10 | 11 | it("creates a promise which rejects with reason passed to the callback", async () => { 12 | await expect( 13 | fromCallback((cb) => cb(hideLiteralErrorFromLinter("bar"))) 14 | ).rejects.toBe("bar"); 15 | }); 16 | 17 | it("passes context and arguments", async () => { 18 | const context = {}; 19 | const args = ["bar", "baz"]; 20 | 21 | expect( 22 | await fromCallback.call( 23 | context, 24 | function (...args_) { 25 | const cb = args_.pop(); 26 | 27 | expect(this).toBe(context); 28 | expect(args_).toEqual(args); 29 | 30 | cb(null, "foo"); 31 | }, 32 | ...args 33 | ) 34 | ).toBe("foo"); 35 | }); 36 | 37 | it("can call a method by its name", async () => { 38 | const obj = { 39 | method(cb) { 40 | expect(this).toBe(obj); 41 | cb(null, "foo"); 42 | }, 43 | }; 44 | 45 | expect(await fromCallback.call(obj, "method")).toBe("foo"); 46 | }); 47 | 48 | it("resolves if `error` is `false`", async () => { 49 | expect( 50 | await fromCallback((cb) => { 51 | // eslint-disable-next-line node/no-callback-literal 52 | cb(false, "foo"); 53 | }) 54 | ).toBe("foo"); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /src/fromEvent.js: -------------------------------------------------------------------------------- 1 | const cancelable = require("./cancelable"); 2 | const makeEventAdder = require("./_makeEventAdder"); 3 | 4 | const fromEvent = cancelable( 5 | ($cancelToken, emitter, event, opts = {}) => 6 | new Promise((resolve, reject) => { 7 | const add = makeEventAdder($cancelToken, emitter, opts.array); 8 | add(event, resolve); 9 | if (!opts.ignoreErrors) { 10 | const { error = "error" } = opts; 11 | if (error !== event) { 12 | add(error, reject); 13 | } 14 | } 15 | }) 16 | ); 17 | module.exports = fromEvent; 18 | -------------------------------------------------------------------------------- /src/fromEvent.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | const { EventEmitter } = require("events"); 4 | 5 | const fromEvent = require("./fromEvent"); 6 | const noop = require("./_noop"); 7 | 8 | const arg1 = "arg1"; 9 | const arg2 = "arg2"; 10 | const emitter = new EventEmitter(); 11 | 12 | describe("fromEvent()", () => { 13 | it("waits for an event", () => { 14 | const promise = fromEvent(emitter, "foo"); 15 | 16 | emitter.emit("foo"); 17 | 18 | return promise; 19 | }); 20 | 21 | // ----------------------------------------------------------------- 22 | 23 | it("forwards first event arg", () => { 24 | const promise = fromEvent(emitter, "foo"); 25 | emitter.emit("foo", arg1, arg2); 26 | 27 | return promise.then((value) => { 28 | expect(value).toBe(arg1); 29 | }); 30 | }); 31 | 32 | // ----------------------------------------------------------------- 33 | 34 | describe("array option", () => { 35 | it("forwards all args as an array", () => { 36 | const promise = fromEvent(emitter, "foo", { 37 | array: true, 38 | }); 39 | emitter.emit("foo", arg1, arg2); 40 | 41 | return promise.then((value) => { 42 | expect(value.event).toBe("foo"); 43 | expect(value.slice()).toEqual([arg1, arg2]); 44 | }); 45 | }); 46 | }); 47 | 48 | // ----------------------------------------------------------------- 49 | 50 | it("resolves if event is error event", () => { 51 | const promise = fromEvent(emitter, "error"); 52 | emitter.emit("error"); 53 | return promise; 54 | }); 55 | 56 | // ----------------------------------------------------------------- 57 | 58 | it("handles error event", () => { 59 | const error = new Error(); 60 | 61 | const promise = fromEvent(emitter, "foo"); 62 | emitter.emit("error", error); 63 | 64 | return expect(promise).rejects.toBe(error); 65 | }); 66 | 67 | // ----------------------------------------------------------------- 68 | 69 | describe("error option", () => { 70 | it("handles a custom error event", () => { 71 | const error = new Error(); 72 | 73 | const promise = fromEvent(emitter, "foo", { 74 | error: "test-error", 75 | }); 76 | emitter.emit("test-error", error); 77 | 78 | return expect(promise).rejects.toBe(error); 79 | }); 80 | }); 81 | 82 | // ----------------------------------------------------------------- 83 | 84 | describe("ignoreErrors option", () => { 85 | it("ignores error events", () => { 86 | const error = new Error(); 87 | 88 | // Node requires at least one error listener. 89 | emitter.once("error", noop); 90 | 91 | const promise = fromEvent(emitter, "foo", { 92 | ignoreErrors: true, 93 | }); 94 | emitter.emit("error", error); 95 | emitter.emit("foo", arg1); 96 | 97 | return promise.then((value) => { 98 | expect(value).toBe(arg1); 99 | }); 100 | }); 101 | }); 102 | 103 | // ----------------------------------------------------------------- 104 | 105 | it("removes listeners after event", () => { 106 | const promise = fromEvent(emitter, "foo"); 107 | emitter.emit("foo"); 108 | 109 | return promise.then(() => { 110 | expect(emitter.listeners("foo")).toEqual([]); 111 | expect(emitter.listeners("error")).toEqual([]); 112 | }); 113 | }); 114 | 115 | // ----------------------------------------------------------------- 116 | 117 | it("removes listeners after error", () => { 118 | const promise = fromEvent(emitter, "foo"); 119 | emitter.emit("error"); 120 | 121 | return promise.catch(() => { 122 | expect(emitter.listeners("foo")).toEqual([]); 123 | expect(emitter.listeners("error")).toEqual([]); 124 | }); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /src/fromEvents.js: -------------------------------------------------------------------------------- 1 | const cancelable = require("./cancelable"); 2 | const makeEventAdder = require("./_makeEventAdder"); 3 | const { forArray } = require("./_utils"); 4 | 5 | const fromEvents = cancelable( 6 | ($cancelToken, emitter, successEvents, errorEvents = ["error"]) => 7 | new Promise((resolve, reject) => { 8 | const add = makeEventAdder($cancelToken, emitter, true); 9 | forArray(successEvents, (event) => add(event, resolve)); 10 | forArray(errorEvents, (event) => add(event, reject)); 11 | }) 12 | ); 13 | module.exports = fromEvents; 14 | -------------------------------------------------------------------------------- /src/fromEvents.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | const { EventEmitter } = require("events"); 4 | 5 | const fromEvents = require("./fromEvents"); 6 | 7 | const arg1 = "arg1"; 8 | const arg2 = "arg2"; 9 | const emitter = new EventEmitter(); 10 | 11 | describe("fromEvents()", () => { 12 | it("resolves if one of the success events is emitted", () => { 13 | const promise = fromEvents(emitter, ["foo", "bar"]); 14 | emitter.emit("foo", arg1, arg2); 15 | 16 | return promise.then((event) => { 17 | expect(event.name).toBe("foo"); 18 | expect(event.args.slice()).toEqual([arg1, arg2]); 19 | 20 | // legacy API 21 | expect(event.event).toBe("foo"); 22 | expect(event.slice()).toEqual([arg1, arg2]); 23 | }); 24 | }); 25 | 26 | // ----------------------------------------------------------------- 27 | 28 | it("rejects if one of the error events is emitted", () => { 29 | const promise = fromEvents(emitter, [], ["foo", "bar"]); 30 | emitter.emit("bar", arg1); 31 | 32 | return promise.catch((event) => { 33 | expect(event.name).toBe("bar"); 34 | expect(event.args.slice()).toEqual([arg1]); 35 | 36 | // legacy API 37 | expect(event.event).toBe("bar"); 38 | expect(event.slice()).toEqual([arg1]); 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/ignoreErrors.js: -------------------------------------------------------------------------------- 1 | const isProgrammerError = require("./_isProgrammerError"); 2 | 3 | const cb = (error) => { 4 | if (isProgrammerError(error)) { 5 | throw error; 6 | } 7 | }; 8 | 9 | module.exports = function ignoreErrors() { 10 | return this.then(undefined, cb); 11 | }; 12 | -------------------------------------------------------------------------------- /src/ignoreErrors.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | const ignoreErrors = require("./ignoreErrors"); 4 | const { reject } = require("./fixtures"); 5 | 6 | describe("ignoreErrors()", () => { 7 | it("swallows errors", () => { 8 | return reject("foo")::ignoreErrors(); 9 | }); 10 | 11 | it("does not swallow programmer errors", async () => { 12 | expect( 13 | Promise.reject(new ReferenceError(""))::ignoreErrors() 14 | ).rejects.toBeInstanceOf(ReferenceError); 15 | expect( 16 | Promise.reject(new SyntaxError(""))::ignoreErrors() 17 | ).rejects.toBeInstanceOf(SyntaxError); 18 | expect( 19 | Promise.reject(new TypeError(""))::ignoreErrors() 20 | ).rejects.toBeInstanceOf(TypeError); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | exports.pAsCallback = exports.asCallback = require("./asCallback"); 2 | exports.pAsyncFn = exports.asyncFn = require("./asyncFn"); 3 | exports.pCancel = exports.Cancel = require("./Cancel"); 4 | exports.pCancelable = exports.cancelable = require("./cancelable"); 5 | exports.pCancelToken = exports.CancelToken = require("./CancelToken"); 6 | exports.pCatch = exports.catch = require("./catch"); 7 | exports.pDefer = exports.defer = require("./defer"); 8 | exports.pDelay = exports.delay = require("./delay"); 9 | exports.pDisposable = exports.Disposable = require("./Disposable"); 10 | exports.pFinally = exports.finally = require("./finally"); 11 | exports.pForArray = exports.forArray = require("./forArray"); 12 | exports.pForEach = exports.forEach = require("./forEach"); 13 | exports.pForIn = exports.forIn = require("./forIn"); 14 | exports.pForIterable = exports.forIterable = require("./forIterable"); 15 | exports.pForOwn = exports.forOwn = require("./forOwn"); 16 | exports.pFromCallback = exports.fromCallback = require("./fromCallback"); 17 | exports.pFromEvent = exports.fromEvent = require("./fromEvent"); 18 | exports.pFromEvents = exports.fromEvents = require("./fromEvents"); 19 | exports.pIgnoreErrors = exports.ignoreErrors = require("./ignoreErrors"); 20 | exports.pIsPromise = exports.isPromise = require("./isPromise"); 21 | exports.pMakeAsyncIterator = 22 | exports.makeAsyncIterator = require("./makeAsyncIterator"); 23 | exports.pNodeify = exports.nodeify = require("./nodeify"); 24 | exports.pPipe = exports.pipe = require("./pipe"); 25 | exports.pPromisify = exports.promisify = require("./promisify"); 26 | exports.pPromisifyAll = exports.promisifyAll = require("./promisifyAll"); 27 | exports.pReflect = exports.reflect = require("./reflect"); 28 | exports.pRetry = exports.retry = require("./retry"); 29 | exports.pSome = exports.some = require("./some"); 30 | exports.pSuppressUnhandledRejections = 31 | exports.suppressUnhandledRejections = require("./suppressUnhandledRejections"); 32 | exports.pTap = exports.tap = require("./tap"); 33 | exports.pTapCatch = exports.tapCatch = require("./tapCatch"); 34 | exports.pTimeout = exports.timeout = require("./timeout"); 35 | exports.pTimeoutError = exports.TimeoutError = require("./TimeoutError"); 36 | exports.pTry = exports.try = require("./try"); 37 | exports.pUnpromisify = exports.unpromisify = require("./unpromisify"); 38 | exports.pWrapApply = exports.wrapApply = require("./wrapApply"); 39 | exports.pWrapCall = exports.wrapCall = require("./wrapCall"); 40 | -------------------------------------------------------------------------------- /src/isPromise.js: -------------------------------------------------------------------------------- 1 | const isPromise = (value) => value != null && typeof value.then === "function"; 2 | module.exports = isPromise; 3 | -------------------------------------------------------------------------------- /src/makeAsyncIterator.js: -------------------------------------------------------------------------------- 1 | const noop = require("./_noop"); 2 | const { makeAsyncIterator } = require("./_utils"); 3 | 4 | const makeAsyncIteratorWrapper = (iterator) => { 5 | const asyncIterator = makeAsyncIterator(iterator); 6 | 7 | return function asyncIteratorWrapper(iteratee) { 8 | return asyncIterator(this, iteratee).then(noop); 9 | }; 10 | }; 11 | 12 | module.exports = makeAsyncIteratorWrapper; 13 | -------------------------------------------------------------------------------- /src/nodeify.js: -------------------------------------------------------------------------------- 1 | const setFunctionNameAndLength = require("./_setFunctionNameAndLength"); 2 | const wrapApply = require("./wrapApply"); 3 | 4 | const { slice } = Array.prototype; 5 | 6 | const nodeify = (fn) => 7 | setFunctionNameAndLength( 8 | function () { 9 | const last = arguments.length - 1; 10 | let cb; 11 | if (last < 0 || typeof (cb = arguments[last]) !== "function") { 12 | throw new TypeError("missing callback"); 13 | } 14 | const args = slice.call(arguments, 0, last); 15 | wrapApply(fn, args).then((value) => cb(undefined, value), cb); 16 | }, 17 | fn.name, 18 | fn.length + 1 19 | ); 20 | 21 | module.exports = nodeify; 22 | -------------------------------------------------------------------------------- /src/nodeify.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | const nodeify = require("./nodeify"); 4 | 5 | describe("nodeify()", () => { 6 | it("handles resolved promises", (done) => { 7 | nodeify(() => Promise.resolve("foo"))((err, res) => { 8 | expect(err).toBe(undefined); 9 | expect(res).toBe("foo"); 10 | done(); 11 | }); 12 | }); 13 | 14 | it("handles rejected promises", (done) => { 15 | const err = new Error(); 16 | nodeify(() => Promise.reject(err))((err, res) => { 17 | expect(err).toBe(err); 18 | expect(res).toBe(undefined); 19 | done(); 20 | }); 21 | }); 22 | 23 | it("handles sync calls values", (done) => { 24 | nodeify(() => "foo")((err, res) => { 25 | expect(err).toBe(undefined); 26 | expect(res).toBe("foo"); 27 | done(); 28 | }); 29 | }); 30 | 31 | it("handles thrown errors", (done) => { 32 | const error = new Error(); 33 | nodeify(() => { 34 | throw error; 35 | })((err, res) => { 36 | expect(err).toBe(err); 37 | expect(res).toBe(undefined); 38 | done(); 39 | }); 40 | }); 41 | 42 | it("returns a function with the same name", () => { 43 | function foo() {} 44 | expect(nodeify(foo).name).toBe(foo.name); 45 | }); 46 | 47 | it("returns a function with one more param", () => { 48 | expect(nodeify(function (a, b) {}).length).toBe(3); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/pipe.js: -------------------------------------------------------------------------------- 1 | const { 2 | isArray, 3 | prototype: { slice }, 4 | } = Array; 5 | 6 | const chain = (promise, fn) => promise.then(fn); 7 | 8 | module.exports = function pPipe(fns) { 9 | if (!isArray(fns)) { 10 | fns = slice.call(arguments); 11 | } 12 | 13 | if (typeof fns[0] !== "function") { 14 | fns[0] = Promise.resolve(fns[0]); 15 | return fns.reduce(chain); 16 | } 17 | 18 | return (arg) => fns.reduce(chain, Promise.resolve(arg)); 19 | }; 20 | -------------------------------------------------------------------------------- /src/promisify.js: -------------------------------------------------------------------------------- 1 | const setFunctionNameAndLength = require("./_setFunctionNameAndLength"); 2 | 3 | // Usage: promisify(fn, [ context ]) 4 | const promisify = (fn, context) => 5 | setFunctionNameAndLength( 6 | function () { 7 | const { length } = arguments; 8 | const args = new Array(length + 1); 9 | for (let i = 0; i < length; ++i) { 10 | args[i] = arguments[i]; 11 | } 12 | 13 | return new Promise((resolve, reject) => { 14 | args[length] = (error, result) => 15 | error != null && error !== false ? reject(error) : resolve(result); 16 | 17 | fn.apply(context === undefined ? this : context, args); 18 | }); 19 | }, 20 | fn.name, 21 | fn.length - 1 22 | ); 23 | module.exports = promisify; 24 | -------------------------------------------------------------------------------- /src/promisify.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | const promisify = require("./promisify"); 4 | 5 | describe("promisify()", () => { 6 | it("handle callback results", async () => { 7 | const value = {}; 8 | expect( 9 | await promisify(function (cb) { 10 | cb(null, value); 11 | })() 12 | ).toBe(value); 13 | }); 14 | 15 | it("resolves if `error` is `false`", async () => { 16 | const value = {}; 17 | expect( 18 | await promisify((cb) => { 19 | // eslint-disable-next-line node/no-callback-literal 20 | cb(false, value); 21 | })() 22 | ).toBe(value); 23 | }); 24 | 25 | it("handle callback errors", async () => { 26 | const error = new Error(); 27 | await expect( 28 | promisify(function (cb) { 29 | cb(error); 30 | })() 31 | ).rejects.toThrowError(error); 32 | }); 33 | 34 | it("handle thrown values", async () => { 35 | const error = new Error(); 36 | await expect( 37 | promisify(function () { 38 | throw error; 39 | })() 40 | ).rejects.toThrowError(error); 41 | }); 42 | 43 | it("forwards context and arguments", () => { 44 | const thisArg = {}; 45 | const args = [{}, {}]; 46 | promisify(function () { 47 | expect(this).toBe(thisArg); 48 | expect([].slice.call(arguments, 0, -1)).toEqual(args); 49 | }).apply(thisArg, args); 50 | }); 51 | 52 | it("returns a function with the same name", () => { 53 | function foo() {} 54 | expect(promisify(foo).name).toBe(foo.name); 55 | }); 56 | 57 | it("returns a function with one less param", () => { 58 | expect(promisify(function (a, b) {}).length).toBe(1); 59 | 60 | // special case if fn has no param 61 | expect(promisify(function () {}).length).toBe(0); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /src/promisifyAll.js: -------------------------------------------------------------------------------- 1 | const promisify = require("./promisify"); 2 | const { forIn } = require("./_utils"); 3 | 4 | const DEFAULT_MAPPER = (_, name) => 5 | !(name.endsWith("Sync") || name.endsWith("Async")) && name; 6 | 7 | // Usage: promisifyAll(obj, [ opts ]) 8 | const promisifyAll = ( 9 | obj, 10 | { mapper = DEFAULT_MAPPER, target = {}, context = obj } = {} 11 | ) => { 12 | forIn(obj, (value, name) => { 13 | let newName; 14 | if (typeof value === "function" && (newName = mapper(value, name, obj))) { 15 | target[newName] = promisify(value, context); 16 | } 17 | }); 18 | 19 | return target; 20 | }; 21 | module.exports = promisifyAll; 22 | -------------------------------------------------------------------------------- /src/promisifyAll.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | const promisifyAll = require("./promisifyAll"); 4 | 5 | describe("promisifyAll()", () => { 6 | it("returns a new object", () => { 7 | const o = {}; 8 | const r = promisifyAll(o); 9 | 10 | expect(typeof r).toBe("object"); 11 | expect(r).not.toBe(o); 12 | }); 13 | 14 | it("creates promisified version of all functions bound to the original object", async () => { 15 | const o = { 16 | foo(cb) { 17 | cb(undefined, this); 18 | }, 19 | }; 20 | const r = promisifyAll(o); 21 | 22 | expect(await r.foo()).toBe(o); 23 | }); 24 | 25 | it("ignores functions ending with Sync or Async", () => { 26 | const o = { 27 | fooAsync() {}, 28 | fooSync() {}, 29 | }; 30 | const r = o::promisifyAll(); 31 | 32 | expect(r.foo).not.toBeDefined(); 33 | expect(r.fooASync).not.toBeDefined(); 34 | expect(r.fooSync).not.toBeDefined(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/reflect.js: -------------------------------------------------------------------------------- 1 | const FN_FALSE = () => false; 2 | const FN_TRUE = () => true; 3 | 4 | const onFulfilled = ((__proto__) => (value) => ({ 5 | __proto__: __proto__, 6 | value: () => value, 7 | }))({ 8 | isFulfilled: FN_TRUE, 9 | isPending: FN_FALSE, 10 | isRejected: FN_FALSE, 11 | reason: () => { 12 | throw new Error("no reason, the promise has resolved"); 13 | }, 14 | }); 15 | 16 | const onRejected = ((__proto__) => (reason) => ({ 17 | __proto__: __proto__, 18 | reason: () => reason, 19 | }))({ 20 | isFulfilled: FN_FALSE, 21 | isPending: FN_FALSE, 22 | isRejected: FN_TRUE, 23 | value: () => { 24 | throw new Error("no value, the promise has rejected"); 25 | }, 26 | }); 27 | 28 | // Returns a promise that is always successful when this promise is 29 | // settled. Its fulfillment value is an object that implements the 30 | // PromiseInspection interface and reflects the resolution this 31 | // promise. 32 | // 33 | // Usage: promise::reflect() 34 | module.exports = function () { 35 | return this.then(onFulfilled, onRejected); 36 | }; 37 | -------------------------------------------------------------------------------- /src/retry.js: -------------------------------------------------------------------------------- 1 | const matchError = require("./_matchError"); 2 | const noop = require("./_noop"); 3 | const setFunctionNameAndLength = require("./_setFunctionNameAndLength"); 4 | 5 | function retry( 6 | fn, 7 | { delay, delays, onRetry = noop, retries, tries, when } = {}, 8 | args 9 | ) { 10 | let shouldRetry; 11 | if (delays !== undefined) { 12 | if (delay !== undefined || tries !== undefined || retries !== undefined) { 13 | throw new TypeError( 14 | "delays is incompatible with delay, tries and retries" 15 | ); 16 | } 17 | 18 | const iterator = delays[Symbol.iterator](); 19 | shouldRetry = () => { 20 | const { done, value } = iterator.next(); 21 | if (done) { 22 | return false; 23 | } 24 | delay = value; 25 | return true; 26 | }; 27 | } else { 28 | if (tries === undefined) { 29 | tries = retries !== undefined ? retries + 1 : 10; 30 | } else if (retries !== undefined) { 31 | throw new TypeError("retries and tries options are mutually exclusive"); 32 | } 33 | 34 | if (delay === undefined) { 35 | delay = 1e3; 36 | } 37 | 38 | shouldRetry = () => --tries !== 0; 39 | } 40 | 41 | when = matchError.bind(undefined, when); 42 | 43 | let attemptNumber = 0; 44 | const sleepResolver = (resolve) => setTimeout(resolve, delay); 45 | const sleep = () => new Promise(sleepResolver); 46 | const onError = (error) => { 47 | if (error instanceof ErrorContainer) { 48 | throw error.error; 49 | } 50 | if (when(error) && shouldRetry()) { 51 | let promise = Promise.resolve( 52 | onRetry.call( 53 | { 54 | arguments: args, 55 | attemptNumber: attemptNumber++, 56 | delay, 57 | fn, 58 | this: this, 59 | }, 60 | error 61 | ) 62 | ); 63 | if (delay !== 0) { 64 | promise = promise.then(sleep); 65 | } 66 | return promise.then(loop); 67 | } 68 | throw error; 69 | }; 70 | const loopResolver = (resolve) => resolve(fn.apply(this, args)); 71 | const loop = () => new Promise(loopResolver).catch(onError); 72 | 73 | return loop(); 74 | } 75 | module.exports = retry; 76 | 77 | function ErrorContainer(error) { 78 | this.error = error; 79 | } 80 | 81 | retry.bail = function retryBail(error) { 82 | throw new ErrorContainer(error); 83 | }; 84 | 85 | retry.wrap = function retryWrap(fn, options) { 86 | const getOptions = typeof options !== "function" ? () => options : options; 87 | return setFunctionNameAndLength( 88 | function () { 89 | return retry.call( 90 | this, 91 | fn, 92 | getOptions.apply(this, arguments), 93 | Array.from(arguments) 94 | ); 95 | }, 96 | fn.name, 97 | fn.length 98 | ); 99 | }; 100 | -------------------------------------------------------------------------------- /src/retry.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | const noop = require("./_noop"); 4 | const retry = require("./retry"); 5 | const { forOwn } = require("./_utils"); 6 | 7 | const microtasks = () => new Promise(setImmediate); 8 | 9 | describe("retry()", () => { 10 | it("retries until the function succeeds", async () => { 11 | let i = 0; 12 | expect( 13 | await retry( 14 | () => { 15 | if (++i < 3) { 16 | throw new Error(); 17 | } 18 | return "foo"; 19 | }, 20 | { delay: 0 } 21 | ) 22 | ).toBe("foo"); 23 | expect(i).toBe(3); 24 | }); 25 | 26 | it("returns the last error", async () => { 27 | let tries = 5; 28 | const e = new Error(); 29 | await expect( 30 | retry( 31 | () => { 32 | throw --tries > 0 ? new Error() : e; 33 | }, 34 | { delay: 0, tries } 35 | ) 36 | ).rejects.toBe(e); 37 | }); 38 | [ReferenceError, TypeError].forEach((ErrorType) => { 39 | it(`does not retry if a ${ErrorType.name} is thrown`, async () => { 40 | let i = 0; 41 | await expect( 42 | retry(() => { 43 | ++i; 44 | throw new ErrorType(); 45 | }) 46 | ).rejects.toBeInstanceOf(ErrorType); 47 | expect(i).toBe(1); 48 | }); 49 | }); 50 | 51 | it("does not retry if `retry.bail` callback is called", async () => { 52 | const e = new Error(); 53 | let i = 0; 54 | await expect( 55 | retry(() => { 56 | ++i; 57 | retry.bail(e); 58 | }) 59 | ).rejects.toBe(e); 60 | expect(i).toBe(1); 61 | }); 62 | 63 | it("forwards this and arguments", async () => { 64 | expect.assertions(2); 65 | 66 | const expectedThis = {}; 67 | const expectedArgs = [Math.random(), Math.random()]; 68 | await retry.call( 69 | expectedThis, 70 | function (...args) { 71 | expect(this).toBe(expectedThis); 72 | expect(args).toEqual(expectedArgs); 73 | }, 74 | { retries: 0 }, 75 | expectedArgs 76 | ); 77 | }); 78 | 79 | describe("`delays` option", () => { 80 | it("works", async () => { 81 | jest.useFakeTimers(); 82 | 83 | const expected = new Error(); 84 | const fn = jest.fn(() => Promise.reject(expected)); 85 | let actual; 86 | retry(fn, { 87 | delays: (function* () { 88 | yield 10; 89 | yield 20; 90 | })(), 91 | }).catch((error) => { 92 | actual = error; 93 | }); 94 | await microtasks(); 95 | 96 | expect(fn).toHaveBeenCalledTimes(1); 97 | 98 | // --- 99 | 100 | jest.advanceTimersByTime(9); 101 | await microtasks(); 102 | 103 | expect(fn).toHaveBeenCalledTimes(1); 104 | 105 | jest.advanceTimersByTime(1); 106 | await microtasks(); 107 | 108 | expect(fn).toHaveBeenCalledTimes(2); 109 | 110 | // --- 111 | 112 | jest.advanceTimersByTime(19); 113 | await microtasks(); 114 | 115 | expect(fn).toHaveBeenCalledTimes(2); 116 | expect(actual).toBe(undefined); 117 | 118 | jest.advanceTimersByTime(1); 119 | await microtasks(); 120 | 121 | expect(fn).toHaveBeenCalledTimes(3); 122 | expect(actual).toBe(expected); 123 | }); 124 | }); 125 | 126 | describe("`tries` and `retries` options", () => { 127 | it("are mutually exclusive", () => { 128 | expect(() => 129 | retry(() => {}, { 130 | tries: 3, 131 | retries: 4, 132 | }) 133 | ).toThrow( 134 | new RangeError("retries and tries options are mutually exclusive") 135 | ); 136 | }); 137 | }); 138 | 139 | describe("`when` option", () => { 140 | forOwn( 141 | { 142 | "with function predicate": (_) => _.message === "foo", 143 | "with object predicate": { message: "foo" }, 144 | }, 145 | (when, title) => 146 | describe(title, () => { 147 | it("retries when error matches", async () => { 148 | let i = 0; 149 | await retry( 150 | () => { 151 | ++i; 152 | throw new Error("foo"); 153 | }, 154 | { delay: 0, when, tries: 2 } 155 | ).catch(Function.prototype); 156 | expect(i).toBe(2); 157 | }); 158 | 159 | it("does not retry when error does not match", async () => { 160 | let i = 0; 161 | await retry( 162 | () => { 163 | ++i; 164 | throw new Error("bar"); 165 | }, 166 | { delay: 0, when, tries: 2 } 167 | ).catch(Function.prototype); 168 | expect(i).toBe(1); 169 | }); 170 | }) 171 | ); 172 | }); 173 | 174 | describe("`onRetry` option", () => { 175 | it("is called with the error before retry is scheduled", async () => { 176 | let expectedError = new Error(); 177 | const expectedArgs = [Math.random(), Math.random()]; 178 | const expectedDelay = 0; 179 | const expectedFn = () => { 180 | if (expectedError) { 181 | throw expectedError; 182 | } 183 | return "foo"; 184 | }; 185 | const expectedThis = {}; 186 | expect( 187 | await retry.call( 188 | expectedThis, 189 | expectedFn, 190 | { 191 | delay: expectedDelay, 192 | onRetry(e) { 193 | expect(e).toBe(expectedError); 194 | expect(this.arguments).toEqual(expectedArgs); 195 | expect(this.attemptNumber).toBe(0); 196 | expect(this.delay).toBe(expectedDelay); 197 | expect(this.this).toBe(expectedThis); 198 | 199 | expectedError = null; 200 | }, 201 | }, 202 | expectedArgs 203 | ) 204 | ).toBe("foo"); 205 | }); 206 | }); 207 | 208 | describe(".wrap()", () => { 209 | it("creates a retrying function", async () => { 210 | const expectedThis = {}; 211 | const expectedArgs = [Math.random(), Math.random()]; 212 | const expectedResult = {}; 213 | 214 | const fn = function foo(bar, baz) { 215 | expect(this).toBe(expectedThis); 216 | expect(Array.from(arguments)).toEqual(expectedArgs); 217 | 218 | return expectedResult; 219 | }; 220 | const retryingFn = retry.wrap(fn, { retries: 0 }); 221 | 222 | expect(retryingFn.name).toBe(fn.name); 223 | expect(retryingFn.length).toBe(fn.length); 224 | 225 | expect(await retryingFn.apply(expectedThis, expectedArgs)).toBe( 226 | expectedResult 227 | ); 228 | }); 229 | 230 | it("options can be a function", () => { 231 | expect.assertions(2); 232 | 233 | const expectedThis = {}; 234 | const expectedArgs = [Math.random(), Math.random()]; 235 | 236 | retry 237 | .wrap(noop, function getOptions() { 238 | expect(this).toBe(expectedThis); 239 | expect(Array.from(arguments)).toEqual(expectedArgs); 240 | }) 241 | .apply(expectedThis, expectedArgs); 242 | }); 243 | }); 244 | }); 245 | -------------------------------------------------------------------------------- /src/some.js: -------------------------------------------------------------------------------- 1 | const resolve = require("./_resolve"); 2 | const { forEach } = require("./_utils"); 3 | 4 | const _some = (promises, count) => 5 | new Promise((resolve, reject) => { 6 | let values = []; 7 | let errors = []; 8 | 9 | const onFulfillment = (value) => { 10 | if (!values) { 11 | return; 12 | } 13 | 14 | values.push(value); 15 | if (--count === 0) { 16 | resolve(values); 17 | values = errors = undefined; 18 | } 19 | }; 20 | 21 | let acceptableErrors = -count; 22 | const onRejection = (reason) => { 23 | if (!values) { 24 | return; 25 | } 26 | 27 | errors.push(reason); 28 | if (--acceptableErrors === 0) { 29 | reject(errors); 30 | values = errors = undefined; 31 | } 32 | }; 33 | 34 | forEach(promises, (promise) => { 35 | ++acceptableErrors; 36 | resolve(promise).then(onFulfillment, onRejection); 37 | }); 38 | }); 39 | 40 | // Usage: promises::some(count) 41 | module.exports = function some(count) { 42 | return resolve(this).then((promises) => _some(promises, count)); 43 | }; 44 | -------------------------------------------------------------------------------- /src/suppressUnhandledRejections.js: -------------------------------------------------------------------------------- 1 | const noop = require("./_noop"); 2 | 3 | // when the rejection is handled later 4 | module.exports = function suppressUnhandledRejections() { 5 | const native = this.suppressUnhandledRejections; 6 | if (typeof native === "function") { 7 | native.call(this); 8 | } else { 9 | this.then(undefined, noop); 10 | } 11 | return this; 12 | }; 13 | -------------------------------------------------------------------------------- /src/tap.js: -------------------------------------------------------------------------------- 1 | module.exports = function tap(onFulfilled, onRejected) { 2 | return this.then(onFulfilled, onRejected).then(() => this); 3 | }; 4 | -------------------------------------------------------------------------------- /src/tap.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | const noop = require("./_noop"); 4 | const tap = require("./tap"); 5 | const { reject } = require("./fixtures"); 6 | 7 | describe("tap(cb)", () => { 8 | it("call cb with the resolved value", () => 9 | new Promise((resolve) => { 10 | Promise.resolve("value")::tap((value) => { 11 | expect(value).toBe("value"); 12 | resolve(); 13 | }); 14 | })); 15 | 16 | it("does not call cb if the promise is rejected", async () => { 17 | await expect( 18 | reject("reason")::tap(() => reject("other reason")) 19 | ).rejects.toBe("reason"); 20 | }); 21 | 22 | it("forwards the resolved value", async () => { 23 | expect(await Promise.resolve("value")::tap(() => "other value")).toBe( 24 | "value" 25 | ); 26 | }); 27 | 28 | it("rejects if cb rejects", async () => { 29 | await expect( 30 | Promise.resolve("value")::tap(() => reject("reason")) 31 | ).rejects.toBe("reason"); 32 | }); 33 | }); 34 | 35 | describe("tap(undefined, cb)", () => { 36 | it("call cb with the rejected reason", () => 37 | new Promise((resolve) => { 38 | reject("reason") 39 | ::tap(undefined, (reason) => { 40 | expect(reason).toBe("reason"); 41 | resolve(); 42 | }) 43 | .catch(noop); // prevents the unhandled rejection warning 44 | })); 45 | 46 | it("does not call cb if the promise is resolved", async () => { 47 | expect( 48 | await Promise.resolve("value")::tap(undefined, () => 49 | reject("other reason") 50 | ) 51 | ).toBe("value"); 52 | }); 53 | 54 | it("forwards the rejected reason", async () => { 55 | await expect(reject("reason")::tap(undefined, () => "value")).rejects.toBe( 56 | "reason" 57 | ); 58 | }); 59 | 60 | it("rejects if cb rejects", async () => { 61 | await expect( 62 | reject("reason")::tap(undefined, () => reject("other reason")) 63 | ).rejects.toBe("other reason"); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /src/tapCatch.js: -------------------------------------------------------------------------------- 1 | module.exports = function tapCatch(cb) { 2 | return this.then(undefined, cb).then(() => this); 3 | }; 4 | -------------------------------------------------------------------------------- /src/timeout.js: -------------------------------------------------------------------------------- 1 | const TimeoutError = require("./TimeoutError"); 2 | 3 | // Usage: 4 | // - promise::timeout(ms) 5 | // - promise::timeout(ms, rejectionValue) 6 | // - promise::timeout(ms, cb) 7 | // 8 | // 0 is a special value that disable the timeout 9 | module.exports = function timeout(ms, onReject) { 10 | if (ms === 0) { 11 | return this; 12 | } 13 | 14 | if (onReject === undefined) { 15 | onReject = new TimeoutError(); 16 | } 17 | 18 | return new Promise((resolve, reject) => { 19 | let handle = setTimeout(() => { 20 | handle = undefined; 21 | 22 | if (typeof this.cancel === "function") { 23 | this.cancel(); 24 | } 25 | 26 | if (typeof onReject === "function") { 27 | try { 28 | resolve(onReject()); 29 | } catch (error) { 30 | reject(error); 31 | } 32 | } else { 33 | reject(onReject); 34 | } 35 | }, ms); 36 | 37 | this.then( 38 | (value) => { 39 | handle !== undefined && clearTimeout(handle); 40 | resolve(value); 41 | }, 42 | (reason) => { 43 | handle !== undefined && clearTimeout(handle); 44 | reject(reason); 45 | } 46 | ); 47 | }); 48 | }; 49 | -------------------------------------------------------------------------------- /src/timeout.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | const noop = require("./_noop"); 4 | const timeout = require("./timeout"); 5 | const TimeoutError = require("./TimeoutError"); 6 | const { reject } = require("./fixtures"); 7 | 8 | describe("timeout()", () => { 9 | const neverSettle = new Promise(noop); 10 | 11 | it("rejects a promise if not settled after a delay", async () => { 12 | await expect(neverSettle::timeout(10)).rejects.toBeInstanceOf(TimeoutError); 13 | }); 14 | 15 | it("call the callback if not settled after a delay", async () => { 16 | expect(await neverSettle::timeout(10, () => "bar")).toBe("bar"); 17 | }); 18 | 19 | it("forwards the settlement if settled before a delay", async () => { 20 | expect(await Promise.resolve("value")::timeout(10)).toBe("value"); 21 | 22 | await expect(reject("reason")::timeout(10)).rejects.toBe("reason"); 23 | }); 24 | 25 | it("rejects if cb throws synchronously", async () => { 26 | await expect( 27 | neverSettle::timeout(10, () => { 28 | throw "reason"; // eslint-disable-line no-throw-literal 29 | }) 30 | ).rejects.toBe("reason"); 31 | }); 32 | 33 | it("thrown error has correct stack trace", async () => { 34 | let error; 35 | try { 36 | error = new Error(); 37 | await neverSettle::timeout(10); 38 | } catch (timeoutError) { 39 | expect(timeoutError.stack.split("\n").slice(3, 10)).toEqual( 40 | error.stack.split("\n").slice(2, 9) 41 | ); 42 | } 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/try.js: -------------------------------------------------------------------------------- 1 | const resolve = require("./_resolve"); 2 | 3 | module.exports = function pTry(fn) { 4 | try { 5 | return resolve(fn()); 6 | } catch (error) { 7 | return Promise.reject(error); 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /src/try.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | const pTry = require("./try"); 4 | const { throwArg } = require("./fixtures"); 5 | 6 | describe("try()", () => { 7 | it("wraps returned value in promise", () => { 8 | return pTry(() => "foo").then((value) => { 9 | expect(value).toBe("foo"); 10 | }); 11 | }); 12 | 13 | it("wraps thrown exception in promise", () => { 14 | return expect(pTry(() => throwArg("foo"))).rejects.toBe("foo"); 15 | }); 16 | 17 | it("calls the callback synchronously", () => { 18 | const spy = jest.fn(); 19 | pTry(spy); 20 | 21 | expect(spy).toHaveBeenCalled(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/unpromisify.js: -------------------------------------------------------------------------------- 1 | const setFunctionNameAndLength = require("./_setFunctionNameAndLength"); 2 | 3 | // Note: synchronous exception are not caught. 4 | // 5 | // Usage: fn::unpromisify() 6 | module.exports = function unpromisify() { 7 | const fn = this; 8 | return setFunctionNameAndLength( 9 | function () { 10 | const n = arguments.length - 1; 11 | let cb; 12 | if (n < 0 || typeof (cb = arguments[n]) !== "function") { 13 | throw new Error("missing callback"); 14 | } 15 | 16 | const args = new Array(n); 17 | for (let i = 0; i < n; ++i) { 18 | args[i] = arguments[i]; 19 | } 20 | 21 | fn.apply(this, args).then( 22 | (result) => cb(undefined, result), 23 | (reason) => cb(reason) 24 | ); 25 | }, 26 | fn.name, 27 | fn.length + 1 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /src/unpromisify.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | const noop = require("./_noop"); 4 | const unpromisify = require("./unpromisify"); 5 | const { reject } = require("./fixtures"); 6 | 7 | describe("unpromisify()", () => { 8 | it("forwards the result", (done) => { 9 | const fn = unpromisify.call(() => Promise.resolve("foo")); 10 | 11 | fn((error, result) => { 12 | expect(error).toBe(undefined); 13 | expect(result).toBe("foo"); 14 | 15 | done(); 16 | }); 17 | }); 18 | 19 | it("forwards the error", (done) => { 20 | const fn = unpromisify.call(() => reject("foo")); 21 | 22 | fn((error) => { 23 | expect(error).toBe("foo"); 24 | 25 | done(); 26 | }); 27 | }); 28 | 29 | it("does not catch sync exceptions", () => { 30 | const fn = unpromisify.call(() => { 31 | throw new Error("foo"); 32 | }); 33 | expect(() => fn(noop)).toThrow("foo"); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/wrapApply.js: -------------------------------------------------------------------------------- 1 | const resolve = require("./_resolve"); 2 | 3 | const wrapApply = (fn, args, thisArg) => { 4 | try { 5 | return resolve(fn.apply(thisArg, args)); 6 | } catch (error) { 7 | return Promise.reject(error); 8 | } 9 | }; 10 | module.exports = wrapApply; 11 | -------------------------------------------------------------------------------- /src/wrapCall.js: -------------------------------------------------------------------------------- 1 | const resolve = require("./_resolve"); 2 | 3 | const wrapCall = (fn, arg, thisArg) => { 4 | try { 5 | return resolve(fn.call(thisArg, arg)); 6 | } catch (error) { 7 | return Promise.reject(error); 8 | } 9 | }; 10 | module.exports = wrapCall; 11 | -------------------------------------------------------------------------------- /src/wrapping.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | const wrapApply = require("./wrapApply"); 4 | const wrapCall = require("./wrapCall"); 5 | const { throwArg } = require("./fixtures"); 6 | 7 | describe("wrapApply() & wrapCall()", () => { 8 | it("calls a function passing args and thisArg", () => { 9 | const args = ["foo", "bar", "baz"]; 10 | const thisArg = {}; 11 | const spy = jest.fn(); 12 | 13 | wrapApply(spy, args, thisArg); 14 | wrapCall(spy, args, thisArg); 15 | 16 | expect(spy.mock.calls).toEqual([args, [args]]); 17 | expect(spy.mock.instances[0]).toBe(thisArg); 18 | expect(spy.mock.instances[1]).toBe(thisArg); 19 | }); 20 | 21 | it("forwards any returned promise", () => { 22 | const p = Promise.resolve(); 23 | const fn = () => p; 24 | 25 | expect(wrapApply(fn)).toBe(p); 26 | expect(wrapCall(fn)).toBe(p); 27 | }); 28 | 29 | it("wraps sync returned value", () => { 30 | const value = {}; 31 | const fn = () => value; 32 | 33 | return Promise.all([ 34 | wrapApply(fn).then((result) => { 35 | expect(result).toBe(value); 36 | }), 37 | wrapCall(fn).then((result) => { 38 | expect(result).toBe(value); 39 | }), 40 | ]); 41 | }); 42 | 43 | it("wraps sync exceptions", () => { 44 | const value = {}; 45 | const fn = () => throwArg(value); 46 | 47 | return Promise.all([ 48 | expect(wrapApply(fn)).rejects.toBe(value), 49 | expect(wrapCall(fn)).rejects.toBe(value), 50 | ]); 51 | }); 52 | }); 53 | --------------------------------------------------------------------------------