From db3c0bd63ec4997d476060a372185e052ebc1686 Mon Sep 17 00:00:00 2001 From: Sami Turcotte Date: Mon, 26 Nov 2018 01:18:01 -0500 Subject: [PATCH] Add every() and delay() methods back --- README.md | 52 ++++++++++++++++++++++++++++++++------ package.json | 4 +-- src/utils.spec.ts | 55 +++++++++++++++++++++++++++++++++++++++-- src/utils.ts | 63 ++++++++++++++++++++++++++++++++++++++++++++--- yarn.lock | 22 ++++++++++++++--- 5 files changed, 177 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index ac808c3..0d4cf33 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # mysah -Promise, Stream and EventEmitter utils for Node.js +**Promise, Stream and EventEmitter utils for Node.js** ## Installation @@ -10,8 +10,10 @@ yarn add mysah ## Basic Usage +The following snippet demonstrates most of mysah's current features. More will come! + ```js -const { once, sleep, stream } = require("mysah"); +const { sleep, once, delay, every, stream } = require("mysah"); async function main() { const collector = stream @@ -22,8 +24,16 @@ async function main() { .pipe(stream.collect({ objectMode: true })); const collected = await once(collector, "data"); - console.log(collected); // [ 'a', 'b', 'c', 'd', 'e' ] - await sleep(1000); // Resolve after one second + await sleep(1000); // undefined (after one second) + await delay(collected, 1000); // [ 'a', 'b', 'c', 'd', 'e' ] (after another second) + await every( + [Promise.resolve("ab"), delay("cd", 1000)], + c => c.length === 2 + ); // true (after another second) + await every( + [Promise.resolve("ab"), delay("cd", 1000)], + c => c.length === 1 + ); // false (instantly) } main(); ``` @@ -65,14 +75,40 @@ export declare function concat( /** * Resolve after the given delay in milliseconds * - * @param ms - The number of milliseconds to wait + * @param ms The number of milliseconds to wait */ export declare function sleep(ms: number): Promise<{}>; + +/** + * Resolve a value after the given delay in milliseconds + * + * @param value Value to resolve + * @param ms Number of milliseconds to wait + */ +export declare function delay(value: T, ms: number): Promise; + /** * Resolve once the given event emitter emits the specified event * - * @param emitter - The event emitter to watch - * @param event - The event to watch + * @param emitter Event emitter to watch + * @param event Event to watch */ -export declare function once(emitter: NodeJS.EventEmitter, event: string): Promise; +export declare function once( + emitter: NodeJS.EventEmitter, + event: string, +): Promise; + +/** + * Resolve to false as soon as any of the promises has resolved to a value for which the predicate is + * falsey, or resolve to true when all of the promises have resolved to a value for which the predicate is + * thruthy, or rejects with the reason of the first promise rejection + * + * @param promises Promises whose resolved values will be tested by the predicate + * @param predicate Predicate to apply + * @returns Promise indicating whether the predicate holds for all resolved promise values + */ +export declare function every( + promises: Array>, + predicate: (value: T) => boolean, +): Promise; ``` diff --git a/package.json b/package.json index 5768e6d..455b957 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mysah", - "version": "0.3.4", + "version": "0.3.5", "description": "Promise, Stream and EventEmitter utils for Node.js", "keywords": [ "promise", @@ -34,7 +34,7 @@ "@types/node": "^10.12.10", "ava": "^1.0.0-rc.2", "chai": "^4.2.0", - "mysah": "^0.3.3", + "mysah": "0.2", "prettier": "^1.14.3", "ts-node": "^7.0.1", "tslint": "^5.11.0", diff --git a/src/utils.spec.ts b/src/utils.spec.ts index 5bd6bea..9146126 100644 --- a/src/utils.spec.ts +++ b/src/utils.spec.ts @@ -1,7 +1,7 @@ import test from "ava"; import { expect } from "chai"; import { EventEmitter } from "events"; -import { once, sleep } from "./utils"; +import { once, sleep, delay, every } from "./utils"; const TimingErrorMarginMs = 50; @@ -14,6 +14,16 @@ test("sleep() resolves after the specified delay in milliseconds", async t => { expect(after - before).closeTo(200, TimingErrorMarginMs); }); +test("delay() resolves a value after the specified delay in milliseconds", async t => { + const before = Date.now(); + const value = await delay("abc", 200); + const after = Date.now(); + + expect(value).equal("abc"); + expect(after - before).gte(200); + expect(after - before).closeTo(200, TimingErrorMarginMs); +}); + test("once() resolves only after the specified event is emitted", async t => { const emitter = new EventEmitter(); const before = Date.now(); @@ -23,7 +33,48 @@ test("once() resolves only after the specified event is emitted", async t => { const result = await once(emitter, "done"); const after = Date.now(); - expect(result).to.equal("some-result"); + expect(result).equal("some-result"); expect(after - before).gte(200); expect(after - before).closeTo(200, TimingErrorMarginMs); }); + +test("every() resolves to true when the predicate holds true for all resolved values", async t => { + const before = Date.now(); + const result = await every( + [Promise.resolve("a"), Promise.resolve("b"), delay("c", 200)], + () => true, + ); + const after = Date.now(); + expect(result).equal(true); + expect(after - before).gte(200); + expect(after - before).closeTo(200, TimingErrorMarginMs); +}); + +test("every() resolves to false as soon as the predicate does not hold for some resolved value", async t => { + const before = Date.now(); + const result = await every( + [Promise.resolve("a"), Promise.resolve("bb"), delay("c", 200)], + (value: string) => value.length === 1, + ); + const after = Date.now(); + expect(result).equal(false); + expect(after - before).lt(200); + expect(after - before).closeTo(0, TimingErrorMarginMs); +}); + +test("every() rejects with the reason as soon as any of the promises rejects", async t => { + const before = Date.now(); + await t.throwsAsync(() => + every( + [ + Promise.resolve("a"), + Promise.reject(new Error("Expected")), + delay("c", 200), + ], + () => true, + ), + ); + const after = Date.now(); + expect(after - before).lt(200); + expect(after - before).closeTo(0, TimingErrorMarginMs); +}); diff --git a/src/utils.ts b/src/utils.ts index 7745e86..82b3f92 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,7 +1,7 @@ /** * Resolve after the given delay in milliseconds * - * @param ms - The number of milliseconds to wait + * @param ms The number of milliseconds to wait */ export function sleep(ms: number) { return new Promise(resolve => { @@ -9,11 +9,23 @@ export function sleep(ms: number) { }); } +/** + * Resolve a value after the given delay in milliseconds + * + * @param value Value to resolve + * @param ms Number of milliseconds to wait + */ +export function delay(value: T, ms: number): Promise { + return new Promise(resolve => { + setTimeout(() => resolve(value), ms); + }); +} + /** * Resolve once the given event emitter emits the specified event * - * @param emitter - The event emitter to watch - * @param event - The event to watch + * @param emitter Event emitter to watch + * @param event Event to watch */ export function once( emitter: NodeJS.EventEmitter, @@ -25,3 +37,48 @@ export function once( }); }); } + +/** + * Resolve to false as soon as any of the promises has resolved to a value for which the predicate is + * falsey, or resolve to true when all of the promises have resolved to a value for which the predicate is + * thruthy, or rejects with the reason of the first promise rejection + * + * @param promises Promises whose resolved values will be tested by the predicate + * @param predicate Predicate to apply + * @returns Promise indicating whether the predicate holds for all resolved promise values + */ +export function every( + promises: Array>, + predicate: (value: T) => boolean, +): Promise { + if (promises.length > 0) { + return new Promise((resolve, reject) => { + let resolvedCount = 0; + let done = false; + promises.forEach(promise => { + promise + .then(value => { + resolvedCount++; + if (!done) { + const predicateValue = predicate(value); + if (!predicateValue) { + resolve(false); + done = true; + } else if (resolvedCount === promises.length) { + resolve(predicateValue); + done = true; + } + } + }) + .catch(err => { + if (!done) { + reject(err); + done = true; + } + }); + }); + }); + } else { + return Promise.resolve(true); + } +} diff --git a/yarn.lock b/yarn.lock index 2a63122..a156d58 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1142,6 +1142,18 @@ es6-error@^4.0.1: resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d" integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg== +es6-promise@^4.0.3: + version "4.2.5" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.5.tgz#da6d0d5692efb461e082c14817fe2427d8f5d054" + integrity sha512-n6wvpdE43VFtJq+lUDYDBFUwV8TZbuGXLV4D6wKafg13ldznKsyEvatubnmUe31zcvelSzOHF+XbaT+Bl9ObDg== + +es6-promisify@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" + integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM= + dependencies: + es6-promise "^4.0.3" + escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.4, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" @@ -2158,10 +2170,12 @@ multimatch@^2.1.0: arrify "^1.0.0" minimatch "^3.0.0" -mysah@^0.3.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/mysah/-/mysah-0.3.3.tgz#4fd0c0489e9f9f99898d1584b73357f5a60a1dda" - integrity sha512-hiottniB9LFmN6I4SL7VJtz7ahi9w0ndE4PJel7WZ6ovvA1LHRJVmbYTbRsy24rHD762ahwLYadxpDE9j+2K+A== +mysah@0.2: + version "0.2.0" + resolved "https://registry.yarnpkg.com/mysah/-/mysah-0.2.0.tgz#77bcaaa25829bf874ac40ee5c47069bcf52e7283" + integrity sha1-d7yqolgpv4dKxA7lxHBpvPUucoM= + dependencies: + es6-promisify "^5.0.0" nan@^2.9.2: version "2.11.1"