├── .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 | [](https://npmjs.org/package/promise-toolbox) [](https://travis-ci.org/JsCommunity/promise-toolbox) [](https://packagephobia.now.sh/result?p=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 |
--------------------------------------------------------------------------------