From d6fea97aad592406ab840de51bee7b8f46e8ca3f Mon Sep 17 00:00:00 2001 From: Maxime LUCE Date: Fri, 6 Nov 2020 02:54:20 +0100 Subject: [PATCH] refactor: create new module structure BREAKING CHANGE: applyEach now always return a function BREAKING CHANGE: Queue constructor now takes options instead of list BREAKING CHANGE: Queue 'length', 'running' and 'idle' now are properties BREAKING CHANGE: removing Queues factory functions --- index.ts | 72 +- jest.config.js | 6 +- lib/PriorityQueue.ts | 121 +++ lib/PriorityTaskQueue.ts | 17 + lib/ProgressPromise.ts | 180 ++++ lib/Queue.ts | 210 +++++ lib/QueueError.ts | 21 + lib/TaskQueue.ts | 17 + lib/_internal.ts | 83 ++ lib/_types.ts | 38 + lib/apply.ts | 14 + lib/applyEach.ts | 14 + lib/applyEachSeries.ts | 15 + lib/applyOn.ts | 28 + lib/cbpromisify.ts | 56 ++ lib/compose.ts | 27 + lib/concat.ts | 15 + lib/concatSeries.ts | 11 + lib/defer.ts | 19 + lib/denodify.ts | 34 + lib/dir.ts | 24 + lib/doUntil.ts | 24 + lib/doWhilst.ts | 23 + lib/each.ts | 22 + lib/eachLimit.ts | 18 + lib/eachSeries.ts | 25 + lib/every.ts | 15 + lib/exec.ts | 20 + lib/execOn.ts | 19 + lib/filter.ts | 21 + lib/filterSeries.ts | 21 + lib/find.ts | 30 + lib/findSeries.ts | 26 + lib/forEach.ts | 4 + lib/forEachLimit.ts | 4 + lib/forEachSeries.ts | 4 + lib/forever.ts | 15 + lib/immediate.ts | 8 + lib/log.ts | 24 + lib/map.ts | 20 + lib/mapLimit.ts | 20 + lib/mapSeries.ts | 21 + lib/memoize.ts | 40 + {src => lib}/nextTick.ts | 82 +- lib/parallel.ts | 33 + lib/parallelLimit.ts | 47 + lib/partial.ts | 16 + lib/partialOn.ts | 28 + lib/promisify.ts | 43 + lib/reduce.ts | 26 + lib/reduceRight.ts | 18 + lib/reject.ts | 19 + lib/rejectSeries.ts | 19 + lib/resolve.ts | 8 + lib/retry.ts | 17 + lib/seq.ts | 27 + lib/series.ts | 48 + lib/some.ts | 12 + lib/sortBy.ts | 26 + lib/tap.ts | 22 + lib/tapOn.ts | 27 + lib/timeout.ts | 10 + lib/times.ts | 17 + lib/timesSeries.ts | 22 + lib/uncallbackify.ts | 34 + lib/until.ts | 19 + lib/waterfall.ts | 19 + lib/whilst.ts | 19 + package.json | 4 +- src/ProgressPromise.ts | 128 --- src/collections.ts | 280 ------ src/flow.ts | 245 ----- src/queue.ts | 377 -------- src/tuples.ts | 15 - src/utils.ts | 260 ------ tests/PriorityQueue.spec.ts | 224 +++++ tests/PriorityTaskQueue.spec.ts | 32 + tests/ProgressPromise.spec.ts | 308 ++++++ tests/Queue.spec.ts | 439 +++++++++ tests/QueueError.spec.ts | 11 + tests/TaskQueue.spec.ts | 32 + tests/apply.spec.ts | 67 ++ tests/applyEach.spec.ts | 84 ++ tests/applyEachSeries.spec.ts | 81 ++ tests/applyOn.spec.ts | 79 ++ tests/cbpromisify.spec.ts | 115 +++ tests/compose.spec.ts | 78 ++ tests/concat.spec.ts | 60 ++ tests/concatSeries.spec.ts | 58 ++ tests/defer.spec.ts | 46 + tests/denodify.spec.ts | 107 +++ tests/dir.spec.ts | 63 ++ tests/doUntil.spec.ts | 115 +++ tests/doWhilst.spec.ts | 115 +++ tests/each.spec.ts | 71 ++ tests/eachLimit.spec.ts | 135 +++ tests/eachSeries.spec.ts | 69 ++ tests/every.spec.ts | 80 ++ tests/exec.spec.ts | 64 ++ tests/execOn.spec.ts | 76 ++ tests/filter.spec.ts | 71 ++ tests/filterSeries.spec.ts | 69 ++ tests/find.spec.ts | 95 ++ tests/findSeries.spec.ts | 90 ++ tests/forEach.spec.ts | 10 + tests/forEachLimit.spec.ts | 10 + tests/forEachSeries.spec.ts | 10 + tests/forever.spec.ts | 28 + tests/helpers/ProgressContext.ts | 90 ++ tests/helpers/createDeferreds.ts | 14 + tests/helpers/createExecutorObject.ts | 14 + tests/helpers/sort.ts | 3 + tests/helpers/timeout.ts | 3 + tests/immediate.spec.ts | 24 + tests/log.spec.ts | 63 ++ tests/map.spec.ts | 71 ++ tests/mapLimit.spec.ts | 138 +++ tests/mapSeries.spec.ts | 68 ++ tests/memoize.spec.ts | 110 +++ tests/nextTick.spec.ts | 24 + tests/parallel.spec.ts | 128 +++ tests/parallelLimit.spec.ts | 281 ++++++ tests/partial.spec.ts | 67 ++ tests/partialOn.spec.ts | 79 ++ tests/promisify.spec.ts | 115 +++ tests/promizr.spec.ts | 33 + tests/promizr/ProgressPromise.ts | 182 ---- tests/promizr/collections.ts | 1052 --------------------- tests/promizr/config.ts | 37 - tests/promizr/flow.ts | 1091 ---------------------- tests/promizr/helpers/ProgressContext.ts | 90 -- tests/promizr/helpers/common.ts | 77 -- tests/promizr/index.html | 22 - tests/promizr/queue.ts | 485 ---------- tests/promizr/tests.d.ts | 8 - tests/reduce.spec.ts | 72 ++ tests/reduceRight.spec.ts | 72 ++ tests/reject.spec.ts | 71 ++ tests/rejectSeries.spec.ts | 68 ++ tests/resolve.spec.ts | 10 + tests/retry.spec.ts | 38 + tests/seq.spec.ts | 78 ++ tests/series.spec.ts | 121 +++ tests/some.spec.ts | 80 ++ tests/sortBy.spec.ts | 69 ++ tests/tap.spec.ts | 44 + tests/tapOn.spec.ts | 122 +++ tests/timeout.spec.ts | 24 + tests/times.spec.ts | 87 ++ tests/timesSeries.spec.ts | 84 ++ tests/uncallbackify.spec.ts | 107 +++ tests/until.spec.ts | 95 ++ tests/waterfall.spec.ts | 57 ++ tests/whilst.spec.ts | 95 ++ tsconfig.json | 1 + 155 files changed, 7779 insertions(+), 4390 deletions(-) create mode 100644 lib/PriorityQueue.ts create mode 100644 lib/PriorityTaskQueue.ts create mode 100644 lib/ProgressPromise.ts create mode 100644 lib/Queue.ts create mode 100644 lib/QueueError.ts create mode 100644 lib/TaskQueue.ts create mode 100644 lib/_internal.ts create mode 100644 lib/_types.ts create mode 100644 lib/apply.ts create mode 100644 lib/applyEach.ts create mode 100644 lib/applyEachSeries.ts create mode 100644 lib/applyOn.ts create mode 100644 lib/cbpromisify.ts create mode 100644 lib/compose.ts create mode 100644 lib/concat.ts create mode 100644 lib/concatSeries.ts create mode 100644 lib/defer.ts create mode 100644 lib/denodify.ts create mode 100644 lib/dir.ts create mode 100644 lib/doUntil.ts create mode 100644 lib/doWhilst.ts create mode 100644 lib/each.ts create mode 100644 lib/eachLimit.ts create mode 100644 lib/eachSeries.ts create mode 100644 lib/every.ts create mode 100644 lib/exec.ts create mode 100644 lib/execOn.ts create mode 100644 lib/filter.ts create mode 100644 lib/filterSeries.ts create mode 100644 lib/find.ts create mode 100644 lib/findSeries.ts create mode 100644 lib/forEach.ts create mode 100644 lib/forEachLimit.ts create mode 100644 lib/forEachSeries.ts create mode 100644 lib/forever.ts create mode 100644 lib/immediate.ts create mode 100644 lib/log.ts create mode 100644 lib/map.ts create mode 100644 lib/mapLimit.ts create mode 100644 lib/mapSeries.ts create mode 100644 lib/memoize.ts rename {src => lib}/nextTick.ts (52%) create mode 100644 lib/parallel.ts create mode 100644 lib/parallelLimit.ts create mode 100644 lib/partial.ts create mode 100644 lib/partialOn.ts create mode 100644 lib/promisify.ts create mode 100644 lib/reduce.ts create mode 100644 lib/reduceRight.ts create mode 100644 lib/reject.ts create mode 100644 lib/rejectSeries.ts create mode 100644 lib/resolve.ts create mode 100644 lib/retry.ts create mode 100644 lib/seq.ts create mode 100644 lib/series.ts create mode 100644 lib/some.ts create mode 100644 lib/sortBy.ts create mode 100644 lib/tap.ts create mode 100644 lib/tapOn.ts create mode 100644 lib/timeout.ts create mode 100644 lib/times.ts create mode 100644 lib/timesSeries.ts create mode 100644 lib/uncallbackify.ts create mode 100644 lib/until.ts create mode 100644 lib/waterfall.ts create mode 100644 lib/whilst.ts delete mode 100644 src/ProgressPromise.ts delete mode 100644 src/collections.ts delete mode 100644 src/flow.ts delete mode 100644 src/queue.ts delete mode 100644 src/tuples.ts delete mode 100644 src/utils.ts create mode 100644 tests/PriorityQueue.spec.ts create mode 100644 tests/PriorityTaskQueue.spec.ts create mode 100644 tests/ProgressPromise.spec.ts create mode 100644 tests/Queue.spec.ts create mode 100644 tests/QueueError.spec.ts create mode 100644 tests/TaskQueue.spec.ts create mode 100644 tests/apply.spec.ts create mode 100644 tests/applyEach.spec.ts create mode 100644 tests/applyEachSeries.spec.ts create mode 100644 tests/applyOn.spec.ts create mode 100644 tests/cbpromisify.spec.ts create mode 100644 tests/compose.spec.ts create mode 100644 tests/concat.spec.ts create mode 100644 tests/concatSeries.spec.ts create mode 100644 tests/defer.spec.ts create mode 100644 tests/denodify.spec.ts create mode 100644 tests/dir.spec.ts create mode 100644 tests/doUntil.spec.ts create mode 100644 tests/doWhilst.spec.ts create mode 100644 tests/each.spec.ts create mode 100644 tests/eachLimit.spec.ts create mode 100644 tests/eachSeries.spec.ts create mode 100644 tests/every.spec.ts create mode 100644 tests/exec.spec.ts create mode 100644 tests/execOn.spec.ts create mode 100644 tests/filter.spec.ts create mode 100644 tests/filterSeries.spec.ts create mode 100644 tests/find.spec.ts create mode 100644 tests/findSeries.spec.ts create mode 100644 tests/forEach.spec.ts create mode 100644 tests/forEachLimit.spec.ts create mode 100644 tests/forEachSeries.spec.ts create mode 100644 tests/forever.spec.ts create mode 100644 tests/helpers/ProgressContext.ts create mode 100644 tests/helpers/createDeferreds.ts create mode 100644 tests/helpers/createExecutorObject.ts create mode 100644 tests/helpers/sort.ts create mode 100644 tests/helpers/timeout.ts create mode 100644 tests/immediate.spec.ts create mode 100644 tests/log.spec.ts create mode 100644 tests/map.spec.ts create mode 100644 tests/mapLimit.spec.ts create mode 100644 tests/mapSeries.spec.ts create mode 100644 tests/memoize.spec.ts create mode 100644 tests/nextTick.spec.ts create mode 100644 tests/parallel.spec.ts create mode 100644 tests/parallelLimit.spec.ts create mode 100644 tests/partial.spec.ts create mode 100644 tests/partialOn.spec.ts create mode 100644 tests/promisify.spec.ts create mode 100644 tests/promizr.spec.ts delete mode 100644 tests/promizr/ProgressPromise.ts delete mode 100644 tests/promizr/collections.ts delete mode 100644 tests/promizr/config.ts delete mode 100644 tests/promizr/flow.ts delete mode 100644 tests/promizr/helpers/ProgressContext.ts delete mode 100644 tests/promizr/helpers/common.ts delete mode 100644 tests/promizr/index.html delete mode 100644 tests/promizr/queue.ts delete mode 100644 tests/promizr/tests.d.ts create mode 100644 tests/reduce.spec.ts create mode 100644 tests/reduceRight.spec.ts create mode 100644 tests/reject.spec.ts create mode 100644 tests/rejectSeries.spec.ts create mode 100644 tests/resolve.spec.ts create mode 100644 tests/retry.spec.ts create mode 100644 tests/seq.spec.ts create mode 100644 tests/series.spec.ts create mode 100644 tests/some.spec.ts create mode 100644 tests/sortBy.spec.ts create mode 100644 tests/tap.spec.ts create mode 100644 tests/tapOn.spec.ts create mode 100644 tests/timeout.spec.ts create mode 100644 tests/times.spec.ts create mode 100644 tests/timesSeries.spec.ts create mode 100644 tests/uncallbackify.spec.ts create mode 100644 tests/until.spec.ts create mode 100644 tests/waterfall.spec.ts create mode 100644 tests/whilst.spec.ts diff --git a/index.ts b/index.ts index 662c028..dc6c8ec 100644 --- a/index.ts +++ b/index.ts @@ -1,6 +1,66 @@ -export * from "./src/collections"; -export * from "./src/flow"; -export * from "./src/nextTick"; -export * from "./src/ProgressPromise"; -export * from "./src/queue"; -export * from "./src/utils"; \ No newline at end of file +export * from "./lib/_types"; + +export { default as apply } from "./lib/apply"; +export { default as applyEach } from "./lib/applyEach"; +export { default as applyEachSeries } from "./lib/applyEachSeries"; +export { default as applyOn } from "./lib/applyOn"; +export { default as cbpromisify } from "./lib/cbpromisify"; +export { default as compose } from "./lib/compose"; +export { default as concat } from "./lib/concat"; +export { default as concatSeries } from "./lib/concatSeries"; +export { default as defer } from "./lib/defer"; +export { default as denodify } from "./lib/denodify"; +export { default as dir } from "./lib/dir"; +export { default as doUntil } from "./lib/doUntil"; +export { default as doWhilst } from "./lib/doWhilst"; +export { default as each } from "./lib/each"; +export { default as eachLimit } from "./lib/eachLimit"; +export { default as eachSeries } from "./lib/eachSeries"; +export { default as every } from "./lib/every"; +export { default as exec } from "./lib/exec"; +export { default as execOn } from "./lib/execOn"; +export { default as filter } from "./lib/filter"; +export { default as filterSeries } from "./lib/filterSeries"; +export { default as find } from "./lib/find"; +export { default as findSeries } from "./lib/findSeries"; +export { default as forEach } from "./lib/forEach"; +export { default as forEachLimit } from "./lib/forEachLimit"; +export { default as forEachSeries } from "./lib/forEachSeries"; +export { default as forever } from "./lib/forever"; +export { default as immediate } from "./lib/immediate"; +export { default as log } from "./lib/log"; +export { default as map } from "./lib/map"; +export { default as mapLimit } from "./lib/mapLimit"; +export { default as mapSeries } from "./lib/mapSeries"; +export { default as memoize } from "./lib/memoize"; +export { default as nextTick } from "./lib/nextTick"; +export { default as parallel } from "./lib/parallel"; +export { default as parallelLimit } from "./lib/parallelLimit"; +export { default as partial } from "./lib/partial"; +export { default as partialOn } from "./lib/partialOn"; +export { default as PriorityQueue } from "./lib/PriorityQueue"; +export { default as PriorityTaskQueue } from "./lib/PriorityTaskQueue"; +export { default as ProgressPromise } from "./lib/ProgressPromise"; +export { default as promisify } from "./lib/promisify"; +export { default as Queue } from "./lib/Queue"; +export { default as QueueError } from "./lib/QueueError"; +export { default as reduce } from "./lib/reduce"; +export { default as reduceRight } from "./lib/reduceRight"; +export { default as reject } from "./lib/reject"; +export { default as rejectSeries } from "./lib/rejectSeries"; +export { default as resolve } from "./lib/resolve"; +export { default as retry } from "./lib/retry"; +export { default as seq } from "./lib/seq"; +export { default as series } from "./lib/series"; +export { default as some } from "./lib/some"; +export { default as sortBy } from "./lib/sortBy"; +export { default as tap } from "./lib/tap"; +export { default as tapOn } from "./lib/tapOn"; +export { default as TaskQueue } from "./lib/TaskQueue"; +export { default as timeout } from "./lib/timeout"; +export { default as times } from "./lib/times"; +export { default as timesSeries } from "./lib/timesSeries"; +export { default as uncallbackify } from "./lib/uncallbackify"; +export { default as until } from "./lib/until"; +export { default as waterfall } from "./lib/waterfall"; +export { default as whilst } from "./lib/whilst"; diff --git a/jest.config.js b/jest.config.js index e0a65d8..2d85574 100644 --- a/jest.config.js +++ b/jest.config.js @@ -2,6 +2,10 @@ module.exports = { preset: "ts-jest", testEnvironment: "node", testMatch: [ - "tests/**/*.spec.ts" + "/tests/**/*.spec.ts" + ], + collectCoverageFrom: [ + "*.ts", + "lib/**/*.ts" ] }; \ No newline at end of file diff --git a/lib/PriorityQueue.ts b/lib/PriorityQueue.ts new file mode 100644 index 0000000..9eb2c25 --- /dev/null +++ b/lib/PriorityQueue.ts @@ -0,0 +1,121 @@ +import type { Deferred, QueueOptions } from "./_types"; +import type { QueueItem, QueueWorker } from "./_internal"; + +import Queue from "./Queue"; +import defer from "./defer"; + + +export default class PriorityQueue extends Queue { + public defaultPriority = 1; + + /** + * Creates a new PriorityQueue. + * + * @param worker The worker function to apply on each item in PriorityQueue + * @param limit The maximum number of concurrent workers to launch + * @param options The options for the PriorityQueue + */ + constructor(worker: QueueWorker, limit?: number, options?: QueueOptions) { + super(worker, limit, options); + } + + public push(priority: number, data?: T): Promise; + public push(priority: number, datas: T[]): Promise; + public push(priority: number, ...datas: T[]): Promise; + public push(data: T): Promise; + public push(datas: T[]): Promise; + public push(...datas: T[]): Promise; + public push(...datas: any[]): Promise { + let priority = this.defaultPriority; + if (typeof datas[0] === "number" && datas.length > 1) { + priority = datas.shift(); + } + + if (datas.length === 1 && Array.isArray(datas[0])) { + datas = datas[0]; + } + + return this.insertAt(datas, priority); + } + + public unshift(priority: number, data?: T): Promise; + public unshift(priority: number, datas: T[]): Promise; + public unshift(priority: number, ...datas: T[]): Promise; + public unshift(data: T): Promise; + public unshift(datas: T[]): Promise; + public unshift(...datas: T[]): Promise; + public unshift(...datas: any[]): Promise { + let priority = this.defaultPriority; + if (typeof datas[0] === "number") { + priority = datas.shift(); + } + + if (datas.length === 1 && Array.isArray(datas[0])) { + datas = datas[0]; + } + + return this.insertAt(datas, priority); + } + + private insertAt(datas: T[], priority: number): Promise { + const length = datas.length; + if (length === 0) { + return Promise.resolve([]); + } + + const index = this.binarySearch(this.items, { priority }, this.compareTasks) + 1; + + const dfd = defer(); + if (!this.started) { + this.started = true; + } + + const iterator = createIterator(length, dfd, priority); + this.items.splice(index, 0, ...datas.map(iterator, this)); + + if (this.onsaturated && this.items.length >= this.limit) { + this.onsaturated(); + } + + for (let i = this.limit; i > 0; i--) { + this.process(); + } + + return dfd.promise; + + + function createIterator(count: number, dfd: Deferred, priority: number): (data: T) => QueueItem { + const errors: any[] = []; + const results: U[] = []; + + return function (this: PriorityQueue, data: T): QueueItem { + const item = this.createItem(data, results, errors, count, dfd.resolve, dfd.reject); + item.priority = priority; + return item; + }; + } + } + + private binarySearch(seq: Array>, item: { priority?: number }, compare: (a: { priority?: number }, b: { priority?: number }) => number): number { + let beg = -1; + let end = seq.length - 1; + + let mid: number; + while (beg < end) { + mid = beg + ((end - beg + 1) >>> 1); + + if (compare(item, seq[mid]) >= 0) { + beg = mid; + } + else { + end = mid - 1; + } + } + + return beg; + } + + private compareTasks(a: { priority?: number }, b: { priority?: number }): number { + return (a.priority || 0) - (b.priority || 0); + } +} diff --git a/lib/PriorityTaskQueue.ts b/lib/PriorityTaskQueue.ts new file mode 100644 index 0000000..3813e4d --- /dev/null +++ b/lib/PriorityTaskQueue.ts @@ -0,0 +1,17 @@ +import type { AsyncTask, QueueOptions } from "./_types"; + +import PriorityQueue from "./PriorityQueue"; + +export default class PriorityTaskQueue extends PriorityQueue, T> { + + /** + * Creates a new PriorityTaskQueue. + * + * @param limit The maximum number of concurrent tasks to launch + * @param options The options for the PriorityTaskQueue + */ + constructor(limit?: number, options?: QueueOptions) { + super(item => item(), limit, options); + } + +} diff --git a/lib/ProgressPromise.ts b/lib/ProgressPromise.ts new file mode 100644 index 0000000..726b05b --- /dev/null +++ b/lib/ProgressPromise.ts @@ -0,0 +1,180 @@ +import resolve from "./resolve"; +import { Awaited } from "./_types"; + +type AwaitedTuple = { [K in keyof T]: Awaited }; + +type ProgressItems = { [K in keyof T]: T[K] extends ProgressPromise ? R : undefined }; + +type ProgressPromiseExecutor = (resolve: (value?: T | PromiseLike) => void, reject: (reason?: any) => void, progress: (progress: P) => void) => void; + +export interface ProgressPromiseDeferred { + resolve(val?: T | PromiseLike): void; + reject(err?: any): void; + progress(val: P): void; + + promise: ProgressPromise; +} + +export default class ProgressPromise implements PromiseLike { + protected _innerPromise: Promise; + protected _progress: P | undefined = undefined; + protected _progressesCallbacks: Array<(progress: P) => void> | undefined = []; + + constructor(executor: ProgressPromiseExecutor) { + if (!(this instanceof ProgressPromise)) { + throw new TypeError("Failed to construct 'ProgressPromise': Please use the 'new' operator, this object constructor cannot be called as a function."); + } + + this._innerPromise = new Promise((resolve, reject) => { + executor(resolve, reject, this.createProgressFunction()); + }); + + const clean = this.cleaner.bind(this); + this._innerPromise.then(clean, clean); + } + + /** + * Adds a progress callback who listen to progress evolution of the `ProgressPromise`. + * + * @param onprogress - The callback to execute when the ProgressPromise progress changed. + * @return - This Promise + */ + public progress(onprogress?: (progress: P) => void): this { + if (typeof onprogress !== "function") return this; + + this._progressesCallbacks?.push(onprogress); + + if (typeof this._progress !== "undefined") { + onprogress(this._progress); + } + + return this; + } + + /** + * Attaches callbacks for the resolution and/or rejection of the Promise. + * + * @param onfulfilled - The callback to execute when the Promise is resolved. + * @param onrejected - The callback to execute when the Promise is rejected. + * + * @returns - A Promise for the completion of which ever callback is executed. + */ + public then(onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): Promise { + return this._innerPromise.then(onfulfilled, onrejected); + } + + /** + * Attaches a callback for only the rejection of the Promise. + * + * @param onrejected - The callback to execute when the Promise is rejected. + * + * @returns - A Promise for the completion of the callback. + */ + public catch(onrejected?: ((reason: any) => TResult | PromiseLike) | undefined | null): Promise { + return this._innerPromise.catch(onrejected); + } + + /** + * Attaches a callback that is invoked when the `Promise` is settled (fulfilled or rejected). + * The resolved value cannot be modified from the callback. + * + * @param onfinally - The callback to execute when the `Promise` is settled (`fulfilled` or `rejected`). + * @returns - A Promise for the completion of the callback. + */ + public finally(onfinally?: (() => void) | undefined | null): Promise { + if (typeof onfinally !== "function") { + return this._innerPromise.then(); + } + + return this._innerPromise.then( + (value) => resolve(onfinally()).then(() => value), + (reason) => resolve(onfinally()).then(() => { throw reason; }) + ); + } + + /** + * Returns a new ProgressPromiseDeferred object. + */ + public static defer(): ProgressPromiseDeferred { + const def: any = {}; + def.promise = new ProgressPromise((res, rej, pro) => { + def.resolve = res; + def.reject = rej; + def.progress = pro; + }); + + return def; + } + + /** + * Creates a `ProgressPromise` that is resolved with an array of results when all of the provided Promises resolve, or rejected when any Promise is rejected. + * + * The `progress` function returns an array of all progresses from found ProgressPromises in `values`. + * + * @param values - An array of Promises. + * @returns - A new Promise. + */ + public static all(values: [...Args]): ProgressPromise, ProgressItems> { + return new ProgressPromise, ProgressItems>((resolve, reject, progress) => { + initAllProgresses(values, progress); + Promise.all(values).then(resolve as (arg: unknown) => void, reject); + }); + } + + /** + * Creates a Promise that is resolved or rejected when any of the provided Promises are resolved or rejected. + * The `progress` function returns an array of all progresses from found ProgressPromises in `values`. + * + * @param values - An array of Promises. + * @returns - A new Promise. + */ + public static race(promises: Args): ProgressPromise, ProgressItems> { + return new ProgressPromise, ProgressItems>((resolve, reject, progress) => { + initAllProgresses(promises, progress); + Promise.race(promises).then(resolve as (arg: unknown) => void, reject); + }); + } + + private cleaner(): void { + this._progressesCallbacks = undefined; + } + + private createProgressFunction(): (val: P) => void { + return (val: P) => { + const callbacks = this._progressesCallbacks; + if (!callbacks) return; + + const len = callbacks.length; + for (let i = 0; i < len; i++) { + callbacks[i](val); + } + + this._progress = val; + }; + } +} + +function initAllProgresses>(promises: T, progress: (progress: P) => void): void { + const len = promises.length; + const progresses = new Array

(len); + + for (let i = 0; i < len; i++) { + const p = promises[i]; + progresses[i] = undefined; + + if (isProgressPromise(p)) { + p.progress(createAllProgressCallback(progress as any, progresses, i)); + } + } +} + +function createAllProgressCallback

(progress: (progress: Array

) => void, progresses: Array

, index: number): (val: P) => void { + return (val: P) => { + progresses[index] = val; + progress(progresses); + }; +} + +function isProgressPromise(p: any): p is ProgressPromise { + return "progress" in p && "then" in p; +} diff --git a/lib/Queue.ts b/lib/Queue.ts new file mode 100644 index 0000000..7ba3467 --- /dev/null +++ b/lib/Queue.ts @@ -0,0 +1,210 @@ +import type { Deferred, QueueOptions } from "./_types"; +import type { QueueItem, QueueWorker } from "./_internal"; + +import QueueError from "./QueueError"; + +import nextTick from "./nextTick"; +import defer from "./defer"; +import exec from "./exec"; + +export default class Queue { + protected items: Array> = []; + protected worker: QueueWorker; + protected workers = 0; + + protected started = false; + protected paused = false; + protected hasException = false; + + public limit: number; + + public onempty: (() => any) | undefined; + public ondrain: (() => any) | undefined; + public onsaturated: (() => any) | undefined; + + public stopOnError = false; + public waitToReject = false; + + public get length(): number { + return this.items.length + this.workers; + } + + public get running(): boolean { + return this.workers > 0; + } + + public get idle(): boolean { + return this.items.length + this.workers === 0; + } + + /** + * Creates a new Queue. + * + * @param worker The worker function to apply on each item in Queue + * @param limit The maximum number of concurrent workers to launch + * @param options The options for the Queue + */ + constructor(worker: QueueWorker, limit = 1, options?: QueueOptions) { + this.worker = worker; + this.limit = limit; + + if (options) { + const keys = Object.keys(options) as Array; + for (const key of keys) { + if (options[key]) this[key] = options[key] as any; + } + } + } + + public push(data: T): Promise; + public push(datas: T[]): Promise; + public push(...datas: T[]): Promise; + public push(...datas: any[]): Promise { + if (datas.length === 1 && Array.isArray(datas[0])) { + datas = datas[0]; + } + + return this.insert(datas); + } + + public unshift(data: T): Promise; + public unshift(datas: T[]): Promise; + public unshift(...datas: T[]): Promise; + public unshift(...datas: any[]): Promise { + if (datas.length === 1 && Array.isArray(datas[0])) { + datas = datas[0]; + } + + return this.insert(datas, true); + } + + public pause(): void { + this.paused = true; + } + + public resume(): void { + if (!this.paused) { + return; + } + + this.paused = false; + this.hasException = false; + + for (let i = this.limit; i > 0; i--) { + this.process(); + } + } + + public clear(): void { + this.ondrain = undefined; + this.items = []; + } + + private insert(datas: T[], before?: boolean): Promise { + const length = datas.length; + if (length === 0) { + return Promise.resolve([]); + } + + const dfd = defer(); + if (!this.started) { + this.started = true; + } + + const iterator = createIterator(length, dfd); + if (before) this.items.unshift(...datas.map(iterator, this)); + else this.items.push(...datas.map(iterator, this)); + + if (this.onsaturated && this.items.length >= this.limit) { + this.onsaturated(); + } + + for (let i = this.limit; i > 0; i--) { + this.process(); + } + + return dfd.promise; + + function createIterator(count: number, dfd: Deferred): (data: T) => QueueItem { + const errors: any[] = []; + const results: U[] = []; + + return function (this: Queue, data: T) { + return this.createItem(data, results, errors, count, dfd.resolve, dfd.reject); + }; + } + } + + protected createItem(data: T, results: U[], errors: any[], count: number, resolve: (result: U | U[] | PromiseLike) => void, reject: (err?: any) => void): QueueItem { + return { + data: data, + resolver: res => { + results.push(res); + + const resultsLen = results.length; + if (resultsLen === count) { + resolve(resultsLen === 1 ? results[0] : results); + } + + if (errors.length + resultsLen === count) { + reject(new QueueError(errors, results)); + } + }, + rejecter: err => { + if (!this.waitToReject || this.stopOnError) { + return reject(err); + } + + errors.push(err); + + if (errors.length + results.length === count) { + reject(new QueueError(errors, results)); + } + } + }; + } + + protected process(): void { + if (this.paused || this.workers >= this.limit || !this.items.length || (this.stopOnError && this.hasException)) { + return; + } + + const item = this.items.shift(); + if (!item) { + return; + } + + if (this.onempty && this.items.length === 0) { + this.onempty(); + } + + this.workers += 1; + nextTick(this.createItemProcess(item)); + } + + private createItemProcess(item: QueueItem): () => void { + return () => { + exec(this.worker, item.data).then( + res => { + item.resolver.call(undefined, res as U); + this.onProcessEnd(); + }, + err => { + item.rejecter.call(undefined, err); + this.hasException = true; + this.onProcessEnd(); + } + ); + }; + } + + protected onProcessEnd(): void { + this.workers -= 1; + + if (this.ondrain && this.items.length + this.workers === 0) { + this.ondrain(); + } + + this.process(); + } +} diff --git a/lib/QueueError.ts b/lib/QueueError.ts new file mode 100644 index 0000000..554ed3e --- /dev/null +++ b/lib/QueueError.ts @@ -0,0 +1,21 @@ +export default class QueueError extends Error { + public innerErrors: Error[]; + public results: T[]; + + constructor(innerErrors: Error[], results: T[]) { + super("Errors occured while executing the Queue"); + + this.name = this.constructor.name; + + if (Object.setPrototypeOf) { + Object.setPrototypeOf(this, QueueError.prototype); + } + + if ((Error).captureStackTrace) { + (Error).captureStackTrace(this, this.constructor); + } + + this.innerErrors = innerErrors; + this.results = results; + } +} diff --git a/lib/TaskQueue.ts b/lib/TaskQueue.ts new file mode 100644 index 0000000..c30937d --- /dev/null +++ b/lib/TaskQueue.ts @@ -0,0 +1,17 @@ +import type { AsyncTask, QueueOptions } from "./_types"; + +import Queue from "./Queue"; + +export default class TaskQueue extends Queue, T> { + + /** + * Creates a new TaskQueue. + * + * @param limit The maximum number of concurrent tasks to launch + * @param options The options for the TaskQueue + */ + constructor(limit?: number, options?: QueueOptions) { + super(item => item(), limit, options); + } + +} diff --git a/lib/_internal.ts b/lib/_internal.ts new file mode 100644 index 0000000..813cfbf --- /dev/null +++ b/lib/_internal.ts @@ -0,0 +1,83 @@ +//#region Tuples + +export type GetLength = Tuple extends { length: infer L } ? L : -1; +export type GetLast = Tuple[PreviousIndex>]; +export type GetLast2 = Tuple[PreviousIndex>>]; +export type GetFirst = Tuple[0]; + +export type Prepend = [first: Item, ...rest: Tuple]; +export type Append = [...tuple: Tuple, last: Item]; +export type Concat = [...tuple: Tuple1, ...append: Tuple2]; + +export type RemoveFirst = Tuple extends [first: any, ...result: infer Result] ? Result : Tuple; +export type RemoveLast = Tuple extends [...result: infer Result, last: any] ? Result : Tuple; +export type RemoveFromStart = Tuple extends [...start: ToRemove, ...result: infer Result] ? Result : Tuple; +export type RemoveFromEnd = Tuple extends [...result: infer Result, ...end: ToRemove] ? Result : Tuple; + +export type PreviousIndex = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62][T]; + +//#endregion + +//#region Functions + +/** Utility type to extract keys from object where value is a function. */ +export type MethodNames = { + [K in keyof T]: T[K] extends (...args: unknown[]) => unknown ? K : never; +}[keyof T]; + +export type Func = (...args: any[]) => any; + +export type PartialParameters any> = T extends (...args: infer P) => any ? Partial

: never; +export type RestOfParameters any, UsedParameters extends any[]> = RemoveFromStart, UsedParameters>; +export type ParametersWithoutLast = RemoveFromEnd, [GetLast>]>; +export type ParametersWithoutLast2 = RemoveFromEnd, [GetLast2>, GetLast>]>; + +export type GetFirstReturnType = T extends [] ? void : ReturnType>; +export type GetLastReturnType = T extends [] ? void : ReturnType>; + +//#endregion + +//#region Node-style callback functions + +type NodeStyleCallback = (err: any, ...rest: T[]) => any; +type NodeStyleCallbackResultType = + T extends (err: any) => any ? void : + T extends (err: any, rest: infer Result) => any ? Result : + T extends (err: any, ...rest: infer Results) => any ? Results : + void; + +export type FunctionWithNodeStyleCallback = (...args: [...any, NodeStyleCallback]) => any; +export type FunctionWithNodeStyleCallbackReturnType = NodeStyleCallbackResultType>>; + +//#endregion + +//#region Multi-callbacks functions + +type ErrorCalback = (err: Error) => any; +type SimpleCallback = (...args: T[]) => any; +type SimpleCallbackResultType = + T extends () => any ? void : + T extends (arg: infer Result) => any ? Result : + T extends (...args: infer Results) => any ? Results : + void; + +export type FunctionWithMultiCallbacks = (...args: [...any, SimpleCallback, ErrorCalback]) => any; +export type FunctionWithMultiCallbacksReturnType = SimpleCallbackResultType>>; + +//#endregion + +//#region Queue + +export type QueueItemOptions = { + data: T; + priority?: number; +}; + +export type QueueItem = QueueItemOptions & { + resolver(result: U): void; + rejecter(err: Error): void; +} + +export type QueueWorker = (arg: T) => U | Promise; + +//#endregion diff --git a/lib/_types.ts b/lib/_types.ts new file mode 100644 index 0000000..ee60b18 --- /dev/null +++ b/lib/_types.ts @@ -0,0 +1,38 @@ +/** A function that take no arguments and may return a Promise. */ +export type AsyncTask = () => T | Promise; + +/** A function that may return a Promise. */ +export type AsyncFunction = (...args: any[]) => T | Promise; + +/** Asynchronous list iterator function. */ +export type AsyncListIterator = (item: T, index: number, list: T[]) => U | Promise; + +/** Asynchronous reduce iterator function. */ +export type AsyncReduceIterator = (memo: U, item: T, index: number, list: T[]) => U | Promise; + +/** Transform a source object in an object where every AsyncTask is awaited. */ +export type AwaitedObject = { [K in keyof T]: T[K] extends () => infer R ? Awaited : T[K]; } + +/** Utility type to extract Promise resolution Type. */ +export type Awaited = T extends PromiseLike ? R : T; + +/** Utility type to wrap value in a Promise. */ +export type Async = Promise>; + +/** A Deferred is an object to control a Promise outside of executor. */ +export type Deferred = { + resolve(val?: T | PromiseLike): void; + reject(err?: any): void; + + promise: Promise; +} + +/** Options to create Queues. */ +export interface QueueOptions { + onempty?: (() => any) | undefined; + ondrain?: (() => any) | undefined; + onsaturated?: (() => any) | undefined; + + stopOnError?: boolean; + waitToReject?: boolean; +} diff --git a/lib/apply.ts b/lib/apply.ts new file mode 100644 index 0000000..fb8dd79 --- /dev/null +++ b/lib/apply.ts @@ -0,0 +1,14 @@ +import type { Async } from "./_types"; +import type { Func } from "./_internal"; + +import exec from "./exec"; + +/** + * Create a new Task which exec `task` with given arguments. + * + * @param task - The function to apply + * @param args - The `task` argument + */ +export default function apply(task: T, ...args: Parameters): () => Async> { + return () => exec(task, ...args); +} diff --git a/lib/applyEach.ts b/lib/applyEach.ts new file mode 100644 index 0000000..9d3e155 --- /dev/null +++ b/lib/applyEach.ts @@ -0,0 +1,14 @@ +import type { Async, AsyncFunction } from "./_types"; + +import parallel from "./parallel"; + +/** + * Prepare a new function which call all `tasks` in parallel with given arguments. + * Returns an array with the result of all `tasks`. + */ +export default function applyEach(tasks: T): (...args: Parameters) => Async>> { + return function (this: unknown, ...args: unknown[]): Async>> { + const iterators = tasks.map(e => () => e.apply(this, args)); + return parallel(iterators); + }; +} diff --git a/lib/applyEachSeries.ts b/lib/applyEachSeries.ts new file mode 100644 index 0000000..8eaebd3 --- /dev/null +++ b/lib/applyEachSeries.ts @@ -0,0 +1,15 @@ +import type { Async, AsyncFunction } from "./_types"; + +import series from "./series"; + +/** + * The same as `applyEach`, only `tasks` are applied in series. + * The next `task` is only called once the current one has completed. + * This means the `task` functions will complete in order. + */ +export default function applyEachSeries(tasks: T): (...args: Parameters) => Async>> { + return function (this: unknown, ...args: unknown[]): Async>> { + const iterators = tasks.map(e => () => e.apply(this, args)); + return series(iterators); + }; +} diff --git a/lib/applyOn.ts b/lib/applyOn.ts new file mode 100644 index 0000000..c4dc17e --- /dev/null +++ b/lib/applyOn.ts @@ -0,0 +1,28 @@ +import type { Async } from "./_types"; +import type { MethodNames, Func } from "./_internal"; + +import execOn from "./execOn"; + +/** + * Same as {@link apply} but call the `task` with `owner` `this` context. + * If task is a string, it calls `owner[task]` function. + * + * @param owner - `this` context to use when calling `task` + * @param task - The property name of function in `owner` + * @param args - The `task` argument + */ +export default function applyOn>(owner: O, task: K, ...args: Parameters): () => Async>; + +/** + * Same as {@link apply} but call the `task` with `owner` `this` context + * + * @param owner - `this` context to use when calling `task` + * @param task - The function to apply + * @param args - The `task` argument + */ +export default function applyOn(owner: O, task: T, ...args: Parameters): () => Async>; + +export default function applyOn(owner: Record, taskOrFunction: string | Func, ...args: any[]): () => Async { + const task = typeof taskOrFunction === "string" ? owner[taskOrFunction] : taskOrFunction; + return () => execOn(owner, task, ...args); +} diff --git a/lib/cbpromisify.ts b/lib/cbpromisify.ts new file mode 100644 index 0000000..2b2baa8 --- /dev/null +++ b/lib/cbpromisify.ts @@ -0,0 +1,56 @@ + +import type { FunctionWithMultiCallbacks, FunctionWithMultiCallbacksReturnType, ParametersWithoutLast2 } from "./_internal"; +import type { Async } from "./_types"; + +/** + * Build a function that transform a multi-callback style function to a Promise version. + * + * @param fn - The function to promisify + */ +export default function cbpromisify(fn: T): (...args: ParametersWithoutLast2) => Async>; + +/** + * Build a function that transform a multi-callback style function to a Promise version. + * + * @param owner - The `this` context to use when calling `fn` + * @param fn - The function to promisify + */ +export default function cbpromisify(owner: O, fn: T): (...args: ParametersWithoutLast2) => Async>; + +export default function cbpromisify(owner: Record | undefined, fn?: T): (...args: ParametersWithoutLast2) => Async> { + if (typeof owner === "function" && typeof fn !== "function") { + fn = owner; + owner = undefined; + } + + if (!fn) { + throw new TypeError("fn should be provided!"); + } + + const executor = fn; + return (...args) => { + return new Promise((resolve, reject) => { + executor.apply(owner, [...args, success, error]); + + function success(...results: any[]): void { + if (results.length === 0) return resolve(); + if (results.length === 1) return resolve(results[0]); + resolve(results as any); + } + + function error(...errors: any[]): void { + if (errors.length === 1) { + errors = errors[0]; + } + + if (!(errors instanceof Error)) { + const err: any = new Error(errors.toString()); + err.innerError = errors; + return reject(errors); + } + + reject(errors); + } + }); + }; +} diff --git a/lib/compose.ts b/lib/compose.ts new file mode 100644 index 0000000..21efb1a --- /dev/null +++ b/lib/compose.ts @@ -0,0 +1,27 @@ +import type { Async, AsyncFunction } from "./_types"; +import type { GetLast, GetFirstReturnType } from "./_internal"; + +import resolve from "./resolve"; + +/** + * Prepare a new function that transfer its arguments to the last `task` then calls each `task` using the result of the previous `task`. + * Resolves with the result of the first `task`. + * Note: Execution order if from end to start. + */ +export default function compose(...tasks: T): (...args: Parameters>) => Async> { + return function (this: unknown, ...args: unknown[]): Async> { + let p: Promise = resolve(); + + const len = tasks.length; + if (!len) return p; + + const last = tasks[len - 1]; + p = p.then(() => last.apply(this, args)); + + for (let i = len - 2; i >= 0; i--) { + p = p.then(tasks[i]); + } + + return p; + }; +} diff --git a/lib/concat.ts b/lib/concat.ts new file mode 100644 index 0000000..4982453 --- /dev/null +++ b/lib/concat.ts @@ -0,0 +1,15 @@ +import type { AsyncListIterator } from "./_types"; + +import map from "./map"; + +/** + * Applies `iterator` to each item in `array`, concatenating the results. + * Returns the concatenated list. + * + * The `iterator`s are called in parallel, and the results are concatenated as they return. + * There is no guarantee that the results array will be returned in the original order of `array` passed to the `iterator` function. + */ +export default function concat(array: T[], iterator: AsyncListIterator): Promise { + return map(array, iterator) + .then(results => ([] as U[]).concat(...results.filter(a => !!a))); +} diff --git a/lib/concatSeries.ts b/lib/concatSeries.ts new file mode 100644 index 0000000..3709ca7 --- /dev/null +++ b/lib/concatSeries.ts @@ -0,0 +1,11 @@ +import type { AsyncListIterator } from "./_types"; + +import mapSeries from "./mapSeries"; + +/** + * Same as `concat`, but executes in series instead of parallel. + */ +export default function concatSeries(array: T[], iterator: AsyncListIterator): Promise { + return mapSeries(array, iterator) + .then(results => ([] as U[]).concat(...results.filter(a => !!a))); +} diff --git a/lib/defer.ts b/lib/defer.ts new file mode 100644 index 0000000..74279c8 --- /dev/null +++ b/lib/defer.ts @@ -0,0 +1,19 @@ +import type { Deferred } from "./_types"; + +/** + * Returns a new Deferred object. + * + * A Deferred object is an object containing 3 properties: `resolve`, `reject` and `promise`. + * The `resolve` function resolves the `promise`. + * The `reject` function rejects the `promise`. + */ +export default function defer(): Deferred { + const dfd: any = {}; + + dfd.promise = new Promise((resolve, reject) => { + dfd.resolve = resolve; + dfd.reject = reject; + }); + + return dfd; +} diff --git a/lib/denodify.ts b/lib/denodify.ts new file mode 100644 index 0000000..fb65152 --- /dev/null +++ b/lib/denodify.ts @@ -0,0 +1,34 @@ +import promisify from "./promisify"; + +import type { FunctionWithNodeStyleCallback, FunctionWithNodeStyleCallbackReturnType, ParametersWithoutLast } from "./_internal"; +import type { Async } from "./_types"; + +/** + * Same as {@link promisify} but call the function immediately. + * + * @param fn - The function to promisify + * @param args - The arguments to pass to fn + */ +export default function denodify(fn: T, ...args: ParametersWithoutLast): Async>; +/** + * Same as {@link promisify} but call the function immediately. + * + * @param owner - The `this` context to use when calling fn + * @param fn - The function to promisify + * @param args - The arguments to pass to fn + */ +export default function denodify, T extends FunctionWithNodeStyleCallback>(owner: O, fn: T, ...args: ParametersWithoutLast): Async>; + +export default function denodify(ownerOrFn: Record | undefined, ...args: unknown[]): Promise { + let owner = ownerOrFn, + fn = args[0] as FunctionWithNodeStyleCallback, + num = 1; + + if (typeof owner === "function" && typeof fn !== "function") { + fn = owner; + owner = undefined; + num = 0; + } + + return promisify(owner, fn)(...args.slice(num)); +} diff --git a/lib/dir.ts b/lib/dir.ts new file mode 100644 index 0000000..03e4cc2 --- /dev/null +++ b/lib/dir.ts @@ -0,0 +1,24 @@ +import type { Async, AsyncFunction } from "./_types"; + +import exec from "./exec"; + +/** + * Utility function to log using `console.dir` the result or the error of the given `task`. + * If the `task` succeeds, its result is returned. + * If the `task` failed, the error is thrown. + * + * @param task - The task to call + * @param args - The arguments to pass to the task + */ +export default function dir(task: T, ...args: Parameters): Async { + return exec(task, ...args).then( + result => { + console.dir(result); + return result; + }, + err => { + console.error(err); + throw err; + } + ); +} diff --git a/lib/doUntil.ts b/lib/doUntil.ts new file mode 100644 index 0000000..5e9f62a --- /dev/null +++ b/lib/doUntil.ts @@ -0,0 +1,24 @@ +import type { AsyncTask } from "./_types"; + +import exec from "./exec"; + +/** + * The opposite of `doWhilst`. + * Calls the `task` function until the `test` function returns `true`. + * + * Note: `test` is called after the first task. + */ +export default function doUntil(task: AsyncTask, test: (res: T) => boolean | Promise): Promise { + return next(); + + function next(): Promise { + return exec(task) + .then(res => test(res as T)) + .then(doContinue => { + if (!doContinue) { + return next(); + } + }); + } +} + diff --git a/lib/doWhilst.ts b/lib/doWhilst.ts new file mode 100644 index 0000000..ab29b1e --- /dev/null +++ b/lib/doWhilst.ts @@ -0,0 +1,23 @@ +import type { AsyncTask } from "./_types"; + +import exec from "./exec"; + +/** + * Equivalent of `do`, `while` loop. + * Calls the `task` function while the `test` function returns `true`. + * + * Note: `test` is called after the first task. + */ +export default function doWhilst(task: AsyncTask, test: (res: T) => boolean | Promise): Promise { + return next(); + + function next(): Promise { + return exec(task) + .then(res => test(res as T)) + .then(doContinue => { + if (doContinue) { + return next(); + } + }); + } +} diff --git a/lib/each.ts b/lib/each.ts new file mode 100644 index 0000000..7081179 --- /dev/null +++ b/lib/each.ts @@ -0,0 +1,22 @@ +import type { AsyncListIterator } from "./_types"; + +import exec from "./exec"; + +/** + * Applies the function `iterator` to each item in `arr`, in parallel. + * The `iterator` is called with an item from the list, the index of this item and the list itself. + * If the `iterator` emit a rejected Promise, the each function `Promise` result is instantly rejected. + * + * Note: since this function applies `iterator` to each item in parallel, there is no guarantee that the iterator functions will complete in order. + * + * @param array The array to iterate on + * @param iterator The iterator to apply on each item + */ +export default function each(array: T[], iterator: AsyncListIterator): Promise { + const promises = array.map(executor); + return Promise.all(promises).then(() => void 0); + + function executor(item: T, index: number, list: T[]): Promise { + return exec(iterator, item, index, list); + } +} diff --git a/lib/eachLimit.ts b/lib/eachLimit.ts new file mode 100644 index 0000000..2c2c4fc --- /dev/null +++ b/lib/eachLimit.ts @@ -0,0 +1,18 @@ +import type { AsyncListIterator, QueueOptions } from "./_types"; + +import TaskQueue from "./TaskQueue"; + +/** + * Sames as {@link each} but limit the number of concurrent iterator. + * + * @param array The array to iterate on + * @param limit The maximum number of iterator to run concurrently + * @param iterator The iterator to apply on each item + * @param options The options for the inner TaskQueue + */ +export default function eachLimit(array: T[], limit: number, iterator: AsyncListIterator, options?: QueueOptions): Promise { + const queue = new TaskQueue(limit, options || { stopOnError: true }); + + const iterators = array.map((value, index, list) => () => iterator(value, index, list)); + return queue.push(iterators).then(() => void 0); +} diff --git a/lib/eachSeries.ts b/lib/eachSeries.ts new file mode 100644 index 0000000..b605ffc --- /dev/null +++ b/lib/eachSeries.ts @@ -0,0 +1,25 @@ +import type { AsyncListIterator } from "./_types"; + +import resolve from "./resolve"; + +/** + * The same as each , only `iterator` is applied to each item in `array` in series. + * The next `iterator` is only called once the current one has completed. + * This means the `iterator` functions will complete in order. + */ +export default function eachSeries(array: T[], iterator: AsyncListIterator): Promise { + return new Promise((res, reject) => { + const len = array.length; + + let p: Promise = resolve(); + for (let i = 0; i < len; i++) { + p = p.then(createIterator(array[i], i, array)); + } + + return p.then(() => res(), reject); + + function createIterator(value: T, index: number, list: T[]): () => unknown | Promise { + return () => iterator(value, index, list); + } + }); +} diff --git a/lib/every.ts b/lib/every.ts new file mode 100644 index 0000000..e4cc9b8 --- /dev/null +++ b/lib/every.ts @@ -0,0 +1,15 @@ +import type { AsyncListIterator } from "./_types"; + +import exec from "./exec"; +import find from "./find"; + +/** + * Returns `true` if every element in `array` satisfies an async test. + */ +export default function every(array: T[], iterator: AsyncListIterator): Promise { + return find(array, invert).then(result => !result); + + function invert(item: T, index: number, list: T[]): Promise { + return exec(iterator, item, index, list).then(result => !result); + } +} diff --git a/lib/exec.ts b/lib/exec.ts new file mode 100644 index 0000000..9392bfe --- /dev/null +++ b/lib/exec.ts @@ -0,0 +1,20 @@ +import type { Async } from "./_types"; +import type { Func } from "./_internal"; + +import resolve from "../lib/resolve"; + +/** + * Execute `task` with given arguments by ensuring that the result is a Promise. + * If task throws synchronously, it's wrapped as a Promise. + * + * @param task - The function to call + * @param args - The arguments to pass to task + */ +export default function exec(task: T, ...args: Parameters): Async> { + try { + return resolve(task(...args)); + } + catch (err) { + return Promise.reject(err); + } +} diff --git a/lib/execOn.ts b/lib/execOn.ts new file mode 100644 index 0000000..cff9138 --- /dev/null +++ b/lib/execOn.ts @@ -0,0 +1,19 @@ +import type { Async, AsyncFunction } from "./_types"; + +import resolve from "./resolve"; + +/** + * Sames as {@link exec} but use `owner` as `this` context when calling `task`. + * + * @param owner - The this context + * @param task - The function to call + * @param args - The arguments to pass to task + */ +export default function execOn(owner: unknown, task: T, ...args: Parameters): Async> { + try { + return resolve(task.apply(owner, args)); + } + catch (err) { + return Promise.reject(err); + } +} diff --git a/lib/filter.ts b/lib/filter.ts new file mode 100644 index 0000000..a0dedfb --- /dev/null +++ b/lib/filter.ts @@ -0,0 +1,21 @@ +import type { AsyncListIterator } from "./_types"; + +import exec from "./exec"; + +/** + * Returns a new array of all the values in `array` which pass an async truth test. + * The Promise returned by each `iterator` call can only returns `boolean` value! + * This operation is performed in parallel, the results array could be in a different order as the original. + * If the order matters, you could use the `findSeries` function. + */ +export default function filter(array: T[], iterator: AsyncListIterator): Promise { + const results: T[] = []; + + return Promise.all(array.map(filterr)).then(() => results); + + function filterr(value: T, index: number, list: T[]): Promise { + return exec(iterator, value, index, list).then(include => { + if (include) results.push(value); + }); + } +} diff --git a/lib/filterSeries.ts b/lib/filterSeries.ts new file mode 100644 index 0000000..04061b5 --- /dev/null +++ b/lib/filterSeries.ts @@ -0,0 +1,21 @@ +import type { AsyncListIterator } from "./_types"; + +import resolve from "./resolve"; +import eachSeries from "./eachSeries"; + +/** + * The same as `filter` only the `iterator` is applied to each item in `array` in series. + * The next `iterator` is only called once the current one has completed. + * The results array will be in the same order as the original. + */ +export default function filterSeries(array: T[], iterator: AsyncListIterator): Promise { + const results: T[] = []; + + return eachSeries(array, filterr).then(() => results); + + function filterr(item: T, index: number): Promise { + return resolve(iterator(item, index, array)).then(include => { + if (include) results.push(item); + }); + } +} diff --git a/lib/find.ts b/lib/find.ts new file mode 100644 index 0000000..327fa42 --- /dev/null +++ b/lib/find.ts @@ -0,0 +1,30 @@ +import type { AsyncListIterator } from "./_types"; + +import exec from "./exec"; + +/** + * Returns the first value in `array` that passes an async truth test. + * The `iterator` is applied in parallel, meaning the first iterator to return `true` resolve the global `find` Promise. + * That means the result might not be the first item in the original `array` (in terms of order) that passes the test. + * If order within the original `array` is important, then look at `findSeries`. + */ +export default function find(array: T[], iterator: AsyncListIterator): Promise { + const len = array.length; + let count = 0; + + return new Promise((resolve, reject) => { + array.forEach(finder); + + function finder(value: T, index: number, list: T[]): Promise { + return exec(iterator, value, index, list).then(valid => { + if (valid) { + return resolve(value); + } + + if (++count === len) { + resolve(); + } + }, reject); + } + }); +} diff --git a/lib/findSeries.ts b/lib/findSeries.ts new file mode 100644 index 0000000..e6539df --- /dev/null +++ b/lib/findSeries.ts @@ -0,0 +1,26 @@ +import type { AsyncListIterator } from "./_types"; + +import exec from "./exec"; + +/** + * The same as `find`, only the `iterator` is applied to each item in `array` in series. + * This means the result is always the first in the original `array` (in terms of array order) that passes the truth test. + */ +export default function findSeries(array: T[], iterator: AsyncListIterator): Promise { + const last = array.length - 1; + + return recurse(); + + function recurse(index = 0): Promise { + const value = array[index]; + + return exec(iterator, value, index, array) + .then(valid => { + if (valid) { return value; } + + if (index < last) { + return recurse(index + 1); + } + }); + } +} diff --git a/lib/forEach.ts b/lib/forEach.ts new file mode 100644 index 0000000..7b3231a --- /dev/null +++ b/lib/forEach.ts @@ -0,0 +1,4 @@ +import forEach from "./each"; + +/** @alias each */ +export default forEach; \ No newline at end of file diff --git a/lib/forEachLimit.ts b/lib/forEachLimit.ts new file mode 100644 index 0000000..c65117d --- /dev/null +++ b/lib/forEachLimit.ts @@ -0,0 +1,4 @@ +import forEachLimit from "./eachLimit"; + +/** @alias eachLimit */ +export default forEachLimit; \ No newline at end of file diff --git a/lib/forEachSeries.ts b/lib/forEachSeries.ts new file mode 100644 index 0000000..39e531e --- /dev/null +++ b/lib/forEachSeries.ts @@ -0,0 +1,4 @@ +import forEachSeries from "./eachSeries"; + +/** @alias eachSeries */ +export default forEachSeries; \ No newline at end of file diff --git a/lib/forever.ts b/lib/forever.ts new file mode 100644 index 0000000..d8d5e92 --- /dev/null +++ b/lib/forever.ts @@ -0,0 +1,15 @@ +import type { AsyncTask } from "./_types"; + +import exec from "./exec"; + +/** + * Calls the `task` indefinitely. + * Note: if `task` throws, the process stops. + */ +export default function forever(task: AsyncTask): Promise { + return next(); + + function next(): Promise { + return exec(task).then(next); + } +} diff --git a/lib/immediate.ts b/lib/immediate.ts new file mode 100644 index 0000000..0c49d1b --- /dev/null +++ b/lib/immediate.ts @@ -0,0 +1,8 @@ +import nextTick from "./nextTick"; + +/** + * Returns a Promise that resolves on next tick. + */ +export default function immediate(): Promise { + return new Promise(resolve => { nextTick(resolve); }); +} diff --git a/lib/log.ts b/lib/log.ts new file mode 100644 index 0000000..b77a95f --- /dev/null +++ b/lib/log.ts @@ -0,0 +1,24 @@ +import type { Async, AsyncFunction } from "./_types"; + +import exec from "./exec"; + +/** + * Utility function to log the result or the error of the given `task`. + * If the `task` succeeds, its result is returned. + * If the `task` failed, the error is thrown. + * + * @param task - The task to call + * @param args - The arguments to pass to the task + */ +export default function log(task: T, ...args: Parameters): Async { + return exec(task, ...args).then( + result => { + console.log(result); + return result; + }, + err => { + console.error(err); + throw err; + } + ); +} diff --git a/lib/map.ts b/lib/map.ts new file mode 100644 index 0000000..8da1177 --- /dev/null +++ b/lib/map.ts @@ -0,0 +1,20 @@ +import type { AsyncListIterator } from "./_types"; + +import exec from "./exec"; + +/** + * Produces a new array of values by mapping each value in `array` through the `iterator` function. + * The `iterator` is called with an item from the list, the index of this item and the list itself. + * If the `iterator` emit a rejected Promise, the each function `Promise` result is instantly rejected. + * + * Note, that since this function applies the `iterator` to each item in parallel, there is no guarantee that the `iterator` functions will complete in order. + * However, the results array will be in the same order as the original `arr`. + */ +export default function map(array: T[], iterator: AsyncListIterator): Promise { + const promises = array.map(mapper); + return Promise.all(promises); + + function mapper(item: T, index: number, list: T[]): Promise { + return exec(iterator, item, index, list) as Promise; + } +} diff --git a/lib/mapLimit.ts b/lib/mapLimit.ts new file mode 100644 index 0000000..bc858f8 --- /dev/null +++ b/lib/mapLimit.ts @@ -0,0 +1,20 @@ +import type { AsyncListIterator, QueueOptions } from "./_types"; + +import TaskQueue from "./TaskQueue"; + +/** + * Sames as {@link map} but limit the number of iterators that run concurrently. + * + * Note: The resulting array may not be in the same order as the source array. + * + * @param array The array to iterate on + * @param limit The maximum number of iterator to run concurrently + * @param iterator The iterator that map each item + * @param options The options for the inner TaskQueue + */ +export default function mapLimit(array: T[], limit: number, iterator: AsyncListIterator, options?: QueueOptions): Promise { + const queue = new TaskQueue(limit, options || { stopOnError: true }); + + const iterators = array.map((value, index, list) => () => iterator(value, index, list)); + return queue.push(iterators); +} diff --git a/lib/mapSeries.ts b/lib/mapSeries.ts new file mode 100644 index 0000000..7975377 --- /dev/null +++ b/lib/mapSeries.ts @@ -0,0 +1,21 @@ +import type { AsyncListIterator } from "./_types"; + +import resolve from "./resolve"; +import eachSeries from "./eachSeries"; + +/** + * The same as `map`, only the `iterator` is applied to each item in `array` in series. + * The next `iterator` is only called once the current one has completed. + * The results array will be in the same order as the original. + */ +export default function mapSeries(array: T[], iterator: AsyncListIterator): Promise { + const results: U[] = []; + + return eachSeries(array, mapper).then(() => results); + + function mapper(item: T, index: number, list: T[]): Promise { + return resolve(iterator(item, index, list)).then(result => { + results[index] = result; + }); + } +} diff --git a/lib/memoize.ts b/lib/memoize.ts new file mode 100644 index 0000000..ebf61d6 --- /dev/null +++ b/lib/memoize.ts @@ -0,0 +1,40 @@ +import type { Awaited, Async, AsyncFunction } from "./_types"; + +import resolve from "./resolve"; +import exec from "./exec"; + +type HashFunction = (args: any[]) => string; + +/** + * Prepare a function that call the `task` and memoize the result to avoid calling it again. + * If `hash` is `true`, memoize the result based on a hash of input arguments (default hash function: `JSON.stringify(args)`). + * If `hash` is a function, memoize the result based on the hash returned by the function (signature: (args: any[]) => string). + * + * Note: The `hash` function is synchronous. + * + * @param task - The task to memoize + * @param hash - `true` to enable simple arguments hashing (JSON.stringify), or a function to hash arguments + */ +export default function memoize(task: T, hash?: boolean | HashFunction): (...args: Parameters) => Async> { + const cache: Record> | Async>> = {}; + const hasher: HashFunction | undefined = typeof hash === "function" ? hash : hash === true ? JSON.stringify : undefined; + + return (...args: Parameters) => { + const hashed = hasher?.(args); + const cached = cache[hashed || "default"]; + + if (cached) { + return resolve(cached); + } + + const promise = exec(task, ...args) + .then(res => save(cache, hashed, res)); + + return save(cache, hashed, promise); + } +} + +function save(cache: Record> | Async>>, hashed: string | undefined, value: Awaited> | Async>): typeof value { + cache[hashed || "default"] = value; + return value; +} diff --git a/src/nextTick.ts b/lib/nextTick.ts similarity index 52% rename from src/nextTick.ts rename to lib/nextTick.ts index 20c5c69..a8d18a2 100644 --- a/src/nextTick.ts +++ b/lib/nextTick.ts @@ -1,15 +1,19 @@ -export type NextTickCallback = () => void; +type NextTickCallback = () => void; -export const nextTick: (cb: NextTickCallback) => void = (function (self: any) { +/** + * Use the best next tick function depending on platform. + * @param cb - The callback to call on next tick + */ +const nextTick = (function (self: any): (cb: NextTickCallback) => void { // Node.JS - if (typeof self.process !== "undefined" && {}.toString.call(self.process) === "[object process]") { + if (typeof self.process !== "undefined" && Object.prototype.toString.call(self.process) === "[object process]") { if (global.setImmediate) { - return (cb: NextTickCallback) => { + return (cb) => { global.setImmediate(cb); }; } else { - return (cb: NextTickCallback) => { + return (cb) => { process.nextTick(cb); }; } @@ -18,7 +22,7 @@ export const nextTick: (cb: NextTickCallback) => void = (function (self: any) { else if (typeof self.Uint8ClampedArray !== "undefined" && typeof self.importScripts !== "undefined" && typeof self.MessageChannel !== "undefined") { const channel = new self.MessageChannel(); - return (cb: NextTickCallback) => { + return (cb) => { channel.port1.onmessage = cb; channel.port2.postMessage(0); }; @@ -43,7 +47,7 @@ export const nextTick: (cb: NextTickCallback) => void = (function (self: any) { observer.observe(node, { characterData: true }); let iterations = 0; - return (cb: NextTickCallback) => { + return (cb) => { tempCallbacks.push(cb); node.data = (iterations = ++iterations % 2); }; @@ -59,46 +63,60 @@ export const nextTick: (cb: NextTickCallback) => void = (function (self: any) { win.attachEvent("onmessage", onGlobalMessage); } - return function (cb: NextTickCallback) { + return (cb: NextTickCallback) => { tempCallbacks.push(cb); win.postMessage(messagePrefix + Math.random() * 1000, "*"); }; } // Set timeout else { - return (cb: NextTickCallback) => { + return (cb) => { setTimeout(cb, 1); }; } } +})(getGlobal()); - function createGlobalMessageHandler(win: any, tempCallbacks: NextTickCallback[], messagePrefix: string): (event: any) => void { - return (event: any) => { - if (event.source === win && typeof event.data === "string" && event.data.indexOf(messagePrefix) === 0) { - let cb: NextTickCallback | undefined; - while ((cb = tempCallbacks.shift()) || tempCallbacks.length) { - if (cb) cb(); - } +export default nextTick; + +function createGlobalMessageHandler(win: any, tempCallbacks: NextTickCallback[], messagePrefix: string): (event: any) => void { + return (event: any) => { + if (event.source === win && typeof event.data === "string" && event.data.indexOf(messagePrefix) === 0) { + let cb: NextTickCallback | undefined; + while ((cb = tempCallbacks.shift()) || tempCallbacks.length) { + if (cb) cb(); } - }; - } + } + }; +} - function canUsePostMessage(win: any): boolean { - // The test against `importScripts` prevents this implementation from being installed inside a web worker, - // where `global.postMessage` means something completely different and can"t be used for this purpose. - if (win.postMessage && !win.importScripts) { - const oldOnMessage = win.onmessage; +function canUsePostMessage(win: any): boolean { + // The test against `importScripts` prevents this implementation from being installed inside a web worker, + // where `global.postMessage` means something completely different and can"t be used for this purpose. + if (win.postMessage && !win.importScripts) { + const oldOnMessage = win.onmessage; - let postMessageIsAsynchronous = true; + let postMessageIsAsynchronous = true; - win.onmessage = () => { postMessageIsAsynchronous = false; }; - win.postMessage("", "*"); + win.onmessage = () => { postMessageIsAsynchronous = false; }; + win.postMessage("", "*"); - win.onmessage = oldOnMessage; + win.onmessage = oldOnMessage; - return postMessageIsAsynchronous; - } - - return false; + return postMessageIsAsynchronous; } -})(this); + + return false; +} + +/* eslint-disable @typescript-eslint/ban-ts-comment */ +function getGlobal(): any { + //@ts-ignore + if (typeof self !== "undefined") { return self; } + //@ts-ignore + if (typeof window !== "undefined") { return window; } + //@ts-ignore + if (typeof global !== "undefined") { return global; } + + throw new Error("unable to locate global object"); +} diff --git a/lib/parallel.ts b/lib/parallel.ts new file mode 100644 index 0000000..e2449fc --- /dev/null +++ b/lib/parallel.ts @@ -0,0 +1,33 @@ +import type { AsyncTask, AwaitedObject } from "./_types"; + +import exec from "./exec"; + +export default function parallel(tasks: Array>): Promise; +export default function parallel>(tasks: T): Promise>; +export default function parallel(tasks: AsyncTask[] | Record): Promise { + return Array.isArray(tasks) ? + listParallel(tasks) : + objectParallel(tasks); +} + +function listParallel(array: Array>): Promise { + const promises = array.map(task => exec(task)); + return Promise.all(promises as Array>); +} + +function objectParallel>(obj: T): Promise> { + const results: Record = {}; + const promises: Array> = [] + + for (const key in obj) { + if (typeof obj[key] === "function") { + promises.push(interator(key, obj[key] as AsyncTask)); + } + } + + return Promise.all(promises).then(() => results as AwaitedObject); + + function interator(key: string, executor: AsyncTask): Promise { + return exec(executor).then(result => { results[key] = result; }); + } +} diff --git a/lib/parallelLimit.ts b/lib/parallelLimit.ts new file mode 100644 index 0000000..04c3636 --- /dev/null +++ b/lib/parallelLimit.ts @@ -0,0 +1,47 @@ +import type { AsyncTask, AwaitedObject, QueueOptions } from "./_types"; + +import TaskQueue from "./TaskQueue"; +import Queue from "./Queue"; + +import execOn from "./execOn"; + +/** + * Sames as {@link parallel} but limit the number of tasks that run concurrently. + * + * Note: The resulting array may not be in the same order as the source array. + * + * @param tasks The array of tasks to execute + * @param limit The maximum number of tasks to run concurrently + * @param options The options for the inner TaskQueue + */ +export default function parallelLimit(tasks: Array>, limit: number, options?: QueueOptions): Promise; + +/** + * Sames as {@link parallel} but limit the number of tasks that run concurrently. + * + * @param tasks An object that contains AsyncTask + * @param limit The maximum number of tasks to run concurrently + * @param options The options for the inner Queue + */ +export default function parallelLimit>(tasks: T, limit: number, options?: QueueOptions): Promise>; + +export default function parallelLimit(tasks: AsyncTask[] | Record, limit: number, options?: QueueOptions): Promise> { + options = options || { stopOnError: true }; + + if (Array.isArray(tasks)) { + const queue = new TaskQueue(limit, options); + return queue.push(tasks); + } + + const obj = tasks; + const result: Record = {}; + + const queue = new Queue(worker, limit, options); + return queue.push(Object.keys(obj)).then(() => result); + + function worker(key: string): Promise { + return execOn(obj, obj[key]).then(res => { + result[key] = res; + }); + } +} diff --git a/lib/partial.ts b/lib/partial.ts new file mode 100644 index 0000000..ac55165 --- /dev/null +++ b/lib/partial.ts @@ -0,0 +1,16 @@ +import type { Async } from "./_types"; +import type { Func, PartialParameters, RestOfParameters } from "./_internal"; + +import exec from "./exec"; + +/** + * Create a new function which exec `task` by combining arguments. + * + * @param task - the function to partialize + * @param preArgs - arguments to bind to task + */ +export default function partial>(task: Method, ...preArgs: Arguments): (...args: RestOfParameters) => Async> { + return (...args) => { + return exec(task, ...preArgs.concat(args) as Parameters); + }; +} diff --git a/lib/partialOn.ts b/lib/partialOn.ts new file mode 100644 index 0000000..c453ac8 --- /dev/null +++ b/lib/partialOn.ts @@ -0,0 +1,28 @@ +import type { Async } from "./_types"; +import type { Func, MethodNames, PartialParameters, RestOfParameters } from "./_internal"; + +import execOn from "./execOn"; + +/** + * Same as {@link partial} but call the `task` with `owner` `this` context. + * If task is a string, it calls `owner[task]` function. + * + * @param owner - `this` context to use when calling `task` + * @param task - The property name of function in `owner` + * @param args - The `task` arguments + */ +export default function partialOn, Arguments extends PartialParameters>(owner: O, task: Key, ...args: Arguments): (...args: RestOfParameters) => Async>; + +/** + * Same as {@link partial} but call the `task` with `owner` `this` context + * + * @param owner - `this` context to use when calling `task` + * @param task - The function to partialize + * @param args - The `task` arguments + */ +export default function partialOn>(owner: O, task: Method, ...args: Arguments): (...args: RestOfParameters) => Async>; + +export default function partialOn(owner: Record, taskOrFunction: string | Func, ...topArgs: any[]): (...args: unknown[]) => Async { + const task = typeof taskOrFunction === "string" ? owner[taskOrFunction] : taskOrFunction; + return (...args) => execOn(owner, task, ...topArgs.concat(args)); +} diff --git a/lib/promisify.ts b/lib/promisify.ts new file mode 100644 index 0000000..b953fdb --- /dev/null +++ b/lib/promisify.ts @@ -0,0 +1,43 @@ +import type { FunctionWithNodeStyleCallback, FunctionWithNodeStyleCallbackReturnType, ParametersWithoutLast } from "./_internal"; +import type { Async } from "./_types"; + +/** + * Build a function that transform a Node-Style callback function to a Promise version. + * + * @param fn - The function to promisify + */ +export default function promisify(fn: T): (...args: ParametersWithoutLast) => Async>; + +/** + * Build a function that transform a Node-Style callback function to a Promise version. + * + * @param owner - The `this` context to use when calling `fn` + * @param fn - The function to promisify + */ +export default function promisify(owner: O, fn: T): (...args: ParametersWithoutLast) => Async>; + +export default function promisify(owner: Record | undefined, fn?: T): (...args: ParametersWithoutLast) => Async> { + if (typeof owner === "function" && typeof fn !== "function") { + fn = owner; + owner = undefined; + } + + if (!fn) { + throw new TypeError("fn should be provided!"); + } + + const executor = fn; + return (...args) => { + return new Promise((resolve, reject) => { + executor.apply(owner, [...args, callback]); + + function callback(err: Error, ...results: any[]): void { + if (err) return reject(err); + if (results.length === 0) return resolve(); + if (results.length === 1) return resolve(results[0]); + + resolve(results as any); + } + }); + }; +} diff --git a/lib/reduce.ts b/lib/reduce.ts new file mode 100644 index 0000000..dc01f02 --- /dev/null +++ b/lib/reduce.ts @@ -0,0 +1,26 @@ + +import type { AsyncReduceIterator } from "./_types"; + +import resolve from "./resolve"; +import eachSeries from "./eachSeries"; + +/** + * Reduces `array` into a single value using an async `iterator` to return each successive step. + * `memo` is the initial state of the reduction. + * This function only operates in series. + * + * For performance reasons, it may make sense to split a call to this function into a parallel map, + * and then use the normal `Array.prototype.reduce` on the results. + * + * This function is for situations where each step in the reduction needs to be async; + * if you can get the data before reducing it, then it's probably a good idea to do so. + */ +export default function reduce(array: T[], memo: U, iterator: AsyncReduceIterator): Promise { + return eachSeries(array, reducer).then(() => memo); + + function reducer(item: T, index: number, list: T[]): Promise { + return resolve(iterator(memo, item, index, list)).then(result => { + memo = result; + }); + } +} diff --git a/lib/reduceRight.ts b/lib/reduceRight.ts new file mode 100644 index 0000000..987c45f --- /dev/null +++ b/lib/reduceRight.ts @@ -0,0 +1,18 @@ + +import type { AsyncReduceIterator } from "./_types"; + +import resolve from "./resolve"; +import eachSeries from "./eachSeries"; + +/** + * Same as `reduce`, only operates on `array` in reverse order. + */ +export default function reduceRight(array: T[], memo: U, iterator: AsyncReduceIterator): Promise { + return eachSeries([...array].reverse(), reducer).then(() => memo); + + function reducer(item: T, index: number): Promise { + return resolve(iterator(memo, item, index, array)).then(result => { + memo = result; + }); + } +} diff --git a/lib/reject.ts b/lib/reject.ts new file mode 100644 index 0000000..80ab8a6 --- /dev/null +++ b/lib/reject.ts @@ -0,0 +1,19 @@ +import type { AsyncListIterator } from "./_types"; + +import exec from "./exec"; + +/** + * The opposite of `filter`. + * Removes values that pass an `async` truth test. + */ +export default function reject(array: T[], iterator: AsyncListIterator): Promise { + const results: T[] = []; + + return Promise.all(array.map(rejecter)).then(() => results); + + function rejecter(item: T, index: number, list: T[]): Promise { + return exec(iterator, item, index, list).then(include => { + if (!include) results.push(item); + }); + } +} diff --git a/lib/rejectSeries.ts b/lib/rejectSeries.ts new file mode 100644 index 0000000..071dad8 --- /dev/null +++ b/lib/rejectSeries.ts @@ -0,0 +1,19 @@ +import type { AsyncListIterator } from "./_types"; + +import resolve from "./resolve"; +import eachSeries from "./eachSeries"; + +/** + * The same as `reject`, only the `iterator` is applied to each item in `array` in series. + */ +export default function rejectSeries(array: T[], iterator: AsyncListIterator): Promise { + const results: T[] = []; + + return eachSeries(array, rejecter).then(() => results); + + function rejecter(item: T, index: number, list: T[]): Promise { + return resolve(iterator(item, index, list)).then(include => { + if (!include) results.push(item); + }); + } +} diff --git a/lib/resolve.ts b/lib/resolve.ts new file mode 100644 index 0000000..1dc24bf --- /dev/null +++ b/lib/resolve.ts @@ -0,0 +1,8 @@ +const resolve = Promise.resolve.bind(Promise); + +/** + * @alias Promise.resolve + * + * Alias for `Promise.resolve`. + */ +export default resolve; diff --git a/lib/retry.ts b/lib/retry.ts new file mode 100644 index 0000000..fdefbc7 --- /dev/null +++ b/lib/retry.ts @@ -0,0 +1,17 @@ +import type { AsyncTask } from "./_types"; + +import exec from "./exec"; + +/** + * Executes the `task` and retry if failed. + * If `task` fails the given number of `times`, the promise is rejected. + */ +export default function retry(times: number, task: AsyncTask): Promise { + return exec(task).catch(err => { + if (times > 1) { + return retry(times - 1, task); + } + + throw err; + }) as Promise; +} diff --git a/lib/seq.ts b/lib/seq.ts new file mode 100644 index 0000000..cdaa4f3 --- /dev/null +++ b/lib/seq.ts @@ -0,0 +1,27 @@ +import type { Async, AsyncFunction } from "./_types"; +import type { GetFirst, GetLastReturnType } from "./_internal"; + +import resolve from "./resolve"; + +/** + * Prepare a new function that transfer its arguments to the fist `task` then calls each `task` using the result of the previous `task`. + * Resolves with the result of the last `task`. + * Note: Execution order if from start to end. + */ +export default function seq(...tasks: T): (...args: Parameters>) => Async> { + return function (this: unknown, ...args: unknown[]): Async> { + let p: Promise = resolve(); + + const len = tasks.length; + if (!len) return p; + + const first = tasks[0]; + p = p.then(() => first.apply(this, args)); + + for (let i = 1; i < len; i++) { + p = p.then(tasks[i]); + } + + return p; + }; +} diff --git a/lib/series.ts b/lib/series.ts new file mode 100644 index 0000000..5db540f --- /dev/null +++ b/lib/series.ts @@ -0,0 +1,48 @@ +import type { AsyncTask, AwaitedObject } from "./_types"; + +import resolve from "./resolve"; + +export default function series(tasks: Array>): Promise; +export default function series>(tasks: T): Promise>; +export default function series(tasks: AsyncTask[] | Record): Promise { + return Array.isArray(tasks) ? + listSeries(tasks) : + objectSeries(tasks); +} + +function listSeries(array: Array>): Promise { + const results: T[] = []; + const len = array.length; + + let p = resolve(); + for (let i = 0; i < len; i++) { + p = p.then(createIterator(array[i])); + } + + return p.then(() => results); + + function createIterator(executor: AsyncTask): () => Promise { + return () => { + return resolve(executor()).then(result => { results.push(result); }); + }; + } +} + +function objectSeries>(obj: T): Promise> { + const results: Record = {}; + let p = resolve(); + + for (const key in obj) { + if (typeof obj[key] === "function") { + p = p.then(createIterator(key, obj[key] as AsyncTask)); + } + } + + return p.then(() => results as AwaitedObject); + + function createIterator(key: string, executor: AsyncTask): () => Promise { + return () => { + return resolve(executor()).then(result => { results[key] = result; }); + } + } +} diff --git a/lib/some.ts b/lib/some.ts new file mode 100644 index 0000000..15b640a --- /dev/null +++ b/lib/some.ts @@ -0,0 +1,12 @@ +import type { AsyncListIterator } from "./_types"; + +import find from "./find"; + +/** + * Returns `true` if at least one element in the `array` satisfies an async test. + * The `Promise` returned by each `iterator` call can only returns boolean value! + * Once any iterator call returns `true`, the main `Promise` is resolved. + */ +export default function some(array: T[], iterator: AsyncListIterator): Promise { + return find(array, iterator).then(result => !!result); +} diff --git a/lib/sortBy.ts b/lib/sortBy.ts new file mode 100644 index 0000000..2b3af85 --- /dev/null +++ b/lib/sortBy.ts @@ -0,0 +1,26 @@ +import type { AsyncListIterator } from "./_types"; + +import resolve from "./resolve"; +import map from "./map"; + +/** + * Sorts a list by the results of running each `array` value through an async `iterator`. + */ +export default function sortBy(array: T[], iterator: AsyncListIterator): Promise { + return map(array, sortMapper).then(result => result.sort(sortFunction).map(i => i.source)); + + function sortMapper(item: T, index: number): Promise> { + return resolve(iterator(item, index, array)) + .then(res => ({ source: item, result: res })); + } + + function sortFunction(left: SortItem, right: SortItem): number { + const a = left.result, b = right.result; + return a < b ? -1 : a > b ? 1 : 0; + } +} + +interface SortItem { + source: T; + result: U; +} diff --git a/lib/tap.ts b/lib/tap.ts new file mode 100644 index 0000000..f855241 --- /dev/null +++ b/lib/tap.ts @@ -0,0 +1,22 @@ +import type { AsyncFunction } from "./_types"; + +import exec from "./exec"; + +/** + * Build a function that takes an argument, calls the `task` and resolve with the input argument. + * This function is usefull to call a function during a Promise chain without breaking the chain. + * + * @example + * ```typescript + * return myAwesomeTask() + * .then(result => `prefix-${result}`) + * .then(promizr.tap(logActionToServer, token)) + * .then(result => result.startsWith("prefix-")); + * ``` + * + * @param task - The function to be called during tap. + * @param args - The arguments to be called to task. + */ +export default function tap(task: Task, ...args: Parameters): (arg: U) => Promise { + return (result) => exec(task, ...args).then(() => result); +} diff --git a/lib/tapOn.ts b/lib/tapOn.ts new file mode 100644 index 0000000..493cf24 --- /dev/null +++ b/lib/tapOn.ts @@ -0,0 +1,27 @@ +import type { AsyncFunction } from "./_types"; +import type { MethodNames } from "./_internal"; + +import execOn from "./execOn"; + +/** + * The sames as {@link tap} but apply the `task` with `owner` as this context. + * + * @param owner - The this context to apply when calling the task + * @param task - The key on owner that contains the function to be called during tap + * @param args - The arguments to apply to task + */ +export default function tapOn>(owner: O, task: K, ...args: Parameters): (arg: U) => Promise; + +/** + * The sames as {@link tap} but apply the `task` with `owner` as this context. + * + * @param owner - The this context to apply when calling the task + * @param task - The function to be called during tap + * @param args - The arguments to apply to task + */ +export default function tapOn(owner: O, task: T, ...args: Parameters): (arg: U) => Promise; + +export default function tapOn(owner: Record, taskOrFunction: string | AsyncFunction, ...args: any[]): (arg: U) => Promise { + const task = typeof taskOrFunction === "string" ? owner[taskOrFunction] : taskOrFunction; + return (res) => execOn(owner, task, ...args).then(() => res); +} diff --git a/lib/timeout.ts b/lib/timeout.ts new file mode 100644 index 0000000..aaf2f33 --- /dev/null +++ b/lib/timeout.ts @@ -0,0 +1,10 @@ +/** + * Returns a Promise that resolves when timer is done. + * + * @param ms - Milliseconds to wait before resolving the Promise + */ +export default function timeout(ms?: number): Promise { + return new Promise(resolve => { + setTimeout(() => { resolve(); }, ms || 1); + }); +} diff --git a/lib/times.ts b/lib/times.ts new file mode 100644 index 0000000..7925bcc --- /dev/null +++ b/lib/times.ts @@ -0,0 +1,17 @@ +import type { AsyncTask } from "./_types"; + +import exec from "./exec"; + +/** + * Executes `task` the given number of `times`. + * Returns an array with the result of each `task` execution. + */ +export default function times(times: number, task: AsyncTask): Promise { + const results: Array> = []; + + for (let i = times; i > 0; i--) { + results.push(exec(task)); + } + + return Promise.all(results); +} diff --git a/lib/timesSeries.ts b/lib/timesSeries.ts new file mode 100644 index 0000000..9a4501c --- /dev/null +++ b/lib/timesSeries.ts @@ -0,0 +1,22 @@ +import type { AsyncTask } from "./_types"; + +import resolve from "./resolve"; + +/** + * The same as `times`, only `tasks` are applied in series. + * The next `task` is only called once the current one has completed. + */ +export default function timesSeries(times: number, task: AsyncTask): Promise { + const results: T[] = []; + + let p = resolve(); + for (let i = times; i > 0; i--) { + p = p.then(capture); + } + + return p.then(() => results); + + function capture(): Promise { + return resolve(task()).then(result => { results.push(result); }); + } +} diff --git a/lib/uncallbackify.ts b/lib/uncallbackify.ts new file mode 100644 index 0000000..11cb2ee --- /dev/null +++ b/lib/uncallbackify.ts @@ -0,0 +1,34 @@ +import cbpromisify from "./cbpromisify"; + +import type { FunctionWithMultiCallbacks, FunctionWithMultiCallbacksReturnType, ParametersWithoutLast2 } from "./_internal"; +import type { Async } from "./_types"; + +/** + * Same as {@link cbpromisify} but call the function immediately. + * + * @param fn - The function to promisify + * @param args - The arguments to pass to fn + */ +export default function uncallbackify(fn: T, ...args: ParametersWithoutLast2): Async>; +/** + * Same as {@link promisify} but call the function immediately. + * + * @param owner - The `this` context to use when calling fn + * @param fn - The function to promisify + * @param args - The arguments to pass to fn + */ +export default function uncallbackify, T extends FunctionWithMultiCallbacks>(owner: O, fn: T, ...args: ParametersWithoutLast2): Async>; + +export default function uncallbackify(ownerOrFn: Record | undefined, ...args: unknown[]): Promise { + let owner = ownerOrFn, + fn = args[0] as FunctionWithMultiCallbacks, + num = 1; + + if (typeof owner === "function" && typeof fn !== "function") { + fn = owner; + owner = undefined; + num = 0; + } + + return cbpromisify(owner, fn)(...args.slice(num)); +} diff --git a/lib/until.ts b/lib/until.ts new file mode 100644 index 0000000..98f9398 --- /dev/null +++ b/lib/until.ts @@ -0,0 +1,19 @@ +import type { AsyncTask } from "./_types"; + +import exec from "./exec"; + +/** + * The opposite of `whilst`. + * Calls the `task` function until the `test` function returns `true`. + */ +export default function until(test: AsyncTask, task: AsyncTask): Promise { + return next(); + + function next(): Promise { + return exec(test).then(doContinue => { + if (!doContinue) { + return exec(task).then(next); + } + }); + } +} diff --git a/lib/waterfall.ts b/lib/waterfall.ts new file mode 100644 index 0000000..d2cb8ab --- /dev/null +++ b/lib/waterfall.ts @@ -0,0 +1,19 @@ +import type { Async, AsyncFunction } from "./_types"; +import type { GetLastReturnType } from "./_internal"; + +import resolve from "./resolve"; + +/** + * Calls each `task` using the result of the previous `task`. + * Resolves with the result of the last `task`. + * The first `task` should not take any argument. + */ +export default function waterfall(tasks: T): Async> { + let p: Promise = resolve(); + + for (const task of tasks) { + p = p.then(task); + } + + return p; +} diff --git a/lib/whilst.ts b/lib/whilst.ts new file mode 100644 index 0000000..3043b90 --- /dev/null +++ b/lib/whilst.ts @@ -0,0 +1,19 @@ +import type { AsyncTask } from "./_types"; + +import exec from "./exec"; + +/** + * Equivalent of `while` loop. + * Calls the `task` function while the `test` function returns `true`. + */ +export default function whilst(test: AsyncTask, task: AsyncTask): Promise { + return next(); + + function next(): Promise { + return exec(test).then(doContinue => { + if (doContinue) { + return exec(task).then(next); + } + }); + } +} diff --git a/package.json b/package.json index f3cfe0f..d257dcc 100644 --- a/package.json +++ b/package.json @@ -34,9 +34,9 @@ "test:coverage": "npm run test -- -- --coverage", "test:ci": "npm run test:coverage -- --ci", "lint": "npm run lint:ts", - "lint:ts": "eslint --ext .ts *.ts {src,lib}/**/*.ts", + "lint:ts": "eslint --ext .ts *.ts lib/**/*.ts", "clean": "npm run clean:ts", - "clean:ts": "rimraf *.{js,d.ts} {src,lib}/**/*.{js,d.ts}", + "clean:ts": "rimraf index.{js,d.ts} lib/**/*.{js,d.ts}", "preversion": "npm run lint", "postversion": "git push && git push --tags", "prepublishOnly": "npm test && npm run build" diff --git a/src/ProgressPromise.ts b/src/ProgressPromise.ts deleted file mode 100644 index 97a1595..0000000 --- a/src/ProgressPromise.ts +++ /dev/null @@ -1,128 +0,0 @@ -export type ProgressPromiseCallback

= (val: P) => void; -export type ProgressPromiseExecutor = (resolve: (value?: T | PromiseLike) => void, reject: (reason?: any) => void, progress: ProgressPromiseCallback

) => void; - -export interface ProgressPromiseDeferred { - resolve(val?: T | PromiseLike): void; - reject(err?: any): void; - progress(val: P): void; - - promise: ProgressPromise; -} - -export function isProgressPromise(p: T | PromiseLike): p is ProgressPromise { - return "progress" in p && "then" in p; -} - -export class ProgressPromise implements PromiseLike { - private _innerPromise: Promise; - protected _progress: P | undefined = undefined; - protected _progressesCallbacks: Array> = []; - - constructor(executor: ProgressPromiseExecutor) { - if (!(this instanceof ProgressPromise)) { - throw new TypeError("Failed to construct 'ProgressPromise': Please use the 'new' operator, this object constructor cannot be called as a function."); - } - - this._innerPromise = new Promise((resolve, reject) => { - executor(resolve, reject, this.createProgressFunction()); - }); - - const clean = this.cleaner.bind(this); - this._innerPromise.then(clean, clean); - } - - public progress(onProgress: ProgressPromiseCallback

): ProgressPromise { - if (this._progressesCallbacks) { - this._progressesCallbacks.push(onProgress); - } - - if (typeof this._progress !== "undefined") { - onProgress.call(undefined, this._progress); - } - - return this; - } - - /** - * Create a new Promise by chaining given callback to current Promise - * @param onfulfilled Callback to be called when Promise fulfills - * @param onrejected Callback to be called when Promise fails - * @returns Chained Promise - */ - public then(onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): Promise { - return this._innerPromise.then(onfulfilled, onrejected); - } - - /** - * The catch function allows to apply a callback on rejection handler. - * It is equivalent to promise.then(undefined, onRejected) - * @param onrejected callback to be called whenever promise fail - * @returns A chained Promise which handle error and fullfil - */ - public catch(onrejected?: ((reason: any) => TResult | PromiseLike) | undefined | null): Promise { - return this._innerPromise.catch(onrejected); - } - - public static defer(): ProgressPromiseDeferred { - const def: any = {}; - def.promise = new ProgressPromise((res, rej, pro) => { - def.resolve = res; - def.reject = rej; - def.progress = pro; - }); - - return def; - } - - public static all(promises: Array>): ProgressPromise> { - return new ProgressPromise>((resolve, reject, progress) => { - initAllProgresses(promises, progress); - Promise.all(promises).then(resolve, reject); - }); - } - public static race(promises: Array>): ProgressPromise> { - return new ProgressPromise>((resolve, reject, progress) => { - initAllProgresses(promises, progress); - Promise.race(promises).then(resolve, reject); - }); - } - - private cleaner(): void { - this._progressesCallbacks = []; - } - - private createProgressFunction(): ProgressPromiseCallback

{ - return (val: P) => { - const - cbs = this._progressesCallbacks, - len = cbs.length; - - for (let i = 0; i < len; i++) { - cbs[i].call(undefined, val); - } - - this._progress = val; - }; - } -} - -function initAllProgresses(promises: Array>, progress: ProgressPromiseCallback>): void { - const len = promises.length; - const progresses = new Array

(len); - - for (let i = 0; i < len; i++) { - const p = promises[i]; - progresses[i] = undefined; - - if (isProgressPromise(p)) { - p.progress(createAllProgressCallback(progress, progresses, i)); - } - } -} - -function createAllProgressCallback

(progress: ProgressPromiseCallback>, progresses: Array

, index: number): (val: P) => void { - return (val: P) => { - progresses[index] = val; - progress(progresses); - }; -} diff --git a/src/collections.ts b/src/collections.ts deleted file mode 100644 index a911615..0000000 --- a/src/collections.ts +++ /dev/null @@ -1,280 +0,0 @@ -import { ensure } from "./utils"; - -/** Base List Iterator for promizr */ -export type PromiseListIterator = (item: T, index: number, list: T[]) => U | Promise; - -/** Iterator for promizr.reduce */ -export type PromiseReduceIterator = (memo: U, item: T) => U | Promise; - -/** - * Applies the function `iterator` to each item in `arr`, in parallel. - * The `iterator` is called with an item from the list, the index of this item and the list itself. - * If the `iterator` emit a rejected Promise, the each function `Promise` result is instantly rejected. - * - * Note, that since this function applies `iterator` to each item in parallel, there is no guarantee that the iterator functions will complete in order. - */ -export function each(array: T[], iterator: PromiseListIterator): Promise { - const promises = array.map(iterator); - return Promise.all(promises).then(() => void 0); -} - -/** @alias each */ -export const forEach = each; - -/** - * The same as each , only `iterator` is applied to each item in `array` in series. - * The next `iterator` is only called once the current one has completed. - * This means the `iterator` functions will complete in order. - */ -export function eachSeries(array: T[], iterator: PromiseListIterator): Promise { - return new Promise((resolve, reject) => { - const len = array.length; - let p = ensure(); - for (let i = 0; i < len; i++) { - p = p.then(partial(array[i], i)); - } - - return p.then(() => resolve(), reject); - - function partial(value: T, index: number): () => Promise { - return () => ensure(iterator(value, index, array)); - } - }); -} - -/** @alias eachSeries */ -export const forEachSeries = each; - -/** - * Produces a new array of values by mapping each value in array through the iterator function. - * The iterator is called with an item from the list, the index of this item and the list itself. - * If the iterator emit a rejected Promise, the each function Promise result is instantly rejected. - * - * Note, that since this function applies the iterator to each item in parallel, there is no guarantee that the iterator functions will complete in order. - * However, the results array will be in the same order as the original arr . - */ -export function map(array: T[], iterator: PromiseListIterator): Promise { - const promises = array.map(iterator); - return Promise.all(promises); -} - -/** - * The same as map , only the iterator is applied to each item in array in series. - * The next iterator is only called once the current one has completed. - * The results array will be in the same order as the original. - */ -export function mapSeries(array: T[], iterator: PromiseListIterator): Promise { - const results: U[] = []; - - return eachSeries(array, mapper).then(() => results); - - function mapper(item: T, index: number): Promise { - return ensure(iterator(item, index, array)).then(result => { - results[index] = result; - }); - } -} - - -/** - * Returns a new array of all the values in `array` which pass an async truth test. - * The Promise returned by each `iterator` call can only returns `boolean` value! - * This operation is performed in parallel, but the results array will be in the same order as the original. - */ -export function filter(array: T[], iterator: PromiseListIterator): Promise { - const results: T[] = []; - - return Promise.all(array.map(filterr)).then(() => results); - - function filterr(value: T, index: number, list: T[]): Promise { - return ensure(iterator(value, index, list)).then(include => { - if (include) results.push(value); - }); - } -} - -/** - * The same as `filter` only the `iterator` is applied to each item in `array` in series. - * The next `iterator` is only called once the current one has completed. - * The results array will be in the same order as the original. - */ -export function filterSeries(array: T[], iterator: PromiseListIterator): Promise { - const results: T[] = []; - - return eachSeries(array, filterr).then(() => results); - - function filterr(item: T, index: number): Promise { - return ensure(iterator(item, index, array)).then(include => { - if (include) results.push(item); - }); - } -} - - -/** - * The opposite of `filter`. Removes values that pass an `async` truth test. - */ -export function reject(array: T[], iterator: PromiseListIterator): Promise { - const results: T[] = []; - - return Promise.all(array.map(rejecter)).then(() => results); - - function rejecter(item: T, index: number, list: T[]): Promise { - return ensure(iterator(item, index, list)).then(include => { - if (!include) results.push(item); - }); - } -} - -/** - * The same as `reject`, only the `iterator` is applied to each item in `array` in series. - */ -export function rejectSeries(array: T[], iterator: PromiseListIterator): Promise { - const results: T[] = []; - - return eachSeries(array, rejecter).then(() => results); - - function rejecter(item: T, index: number, list: T[]): Promise { - return ensure(iterator(item, index, list)).then(include => { - if (!include) results.push(item); - }); - } -} - -/** - * Reduces `array` into a single value using an async `iterator` to return each successive step. - * `memo` is the initial state of the reduction. - * This function only operates in series. - * - * For performance reasons, it may make sense to split a call to this function into a parallel map, - * and then use the normal `Array.prototype.reduce` on the results. - * - * This function is for situations where each step in the reduction needs to be async; - * if you can get the data before reducing it, then it's probably a good idea to do so. - */ -export function reduce(array: T[], memo: U, iterator: PromiseReduceIterator): Promise { - return eachSeries(array, reducer).then(() => memo); - - function reducer(item: T): Promise { - return ensure(iterator(memo, item)).then(result => { - memo = result; - }); - } -} - -/** - * Same as `reduce`, only operates on `array` in reverse order. - */ -export function reduceRight(array: T[], memo: U, iterator: PromiseReduceIterator): Promise { - const clone = [...array].reverse(); - return reduce(clone, memo, iterator); -} - - -/** - * Returns the first value in `array` that passes an async truth test. - * The iterator is applied in parallel, meaning the first iterator to return `true` resolve the global `find` Promise. - * That means the result might not be the first item in the original array (in terms of order) that passes the test. - * If order within the original `array` is important, then look at `findSeries`. - */ -export function find(array: T[], iterator: PromiseListIterator): Promise { - const len = array.length; - let count = 0; - - return new Promise((resolve, reject) => { - array.forEach(finder); - - function finder(value: T, index: number, list: T[]): Promise { - return ensure(iterator(value, index, list)).then(valid => { - if (valid || ++count === len) { - resolve(value); - } - }, reject); - } - }); - -} - -/** - * The same as `find`, only the `iterator` is applied to each item in `array` in series. - * This means the result is always the first in the original `array` (in terms of array order) that passes the truth test. - */ -export function findSeries(array: T[], iterator: PromiseListIterator): Promise { - const last = array.length - 1; - - return recurse(); - - function recurse(index = 0): Promise { - const value = array[index]; - - return ensure(iterator(value, index, array)) - .then(valid => { - if (valid) { return value; } - - if (index < last) { - return recurse(index + 1); - } - }); - } -} - - -/** - * Sorts a list by the results of running each `array` value through an async `iterator`. - */ -export function sortBy(array: T[], iterator: PromiseListIterator): Promise { - return map(array, sortMapper).then(result => result.sort(sortFunction).map(i => i.source)); - - function sortMapper(item: T, index: number): Promise> { - return ensure(iterator(item, index, array)) - .then(res => ({ source: item, result: res })); - } - - function sortFunction(left: SortItem, right: SortItem): number { - const a = left.result, b = right.result; - return a < b ? -1 : a > b ? 1 : 0; - } -} - -interface SortItem { - source: T; - result: U; -} - - -/** - * Returns `true` if at least one element in the `array` satisfies an async test. - * The `Promise` returned by each `iterator` call can only returns boolean value! - * Once any iterator call returns `true`, the main `Promise` is resolved. - */ -export function some(array: T[], iterator: PromiseListIterator): Promise { - return find(array, iterator).then(result => !!result); -} - -/** - * Returns `true` if every element in array satisfies an async test. - */ -export function every(array: T[], iterator: PromiseListIterator): Promise { - return find(array, iterator).then(result => !result); -} - - -/** - * Applies `iterator` to each item in `array`, concatenating the results. - * Returns the concatenated list. - * - * The `iterator`s are called in parallel, and the results are concatenated as they return. - * There is no guarantee that the results array will be returned in the original order of `array` passed to the `iterator` function. - */ -export function concat(array: T[], iterator: PromiseListIterator): Promise { - return map(array, iterator) - .then(results => Array.prototype.concat.apply([], results.filter(a => !!a))); -} - -/** - * Same as `concat`, but executes in series instead of parallel. - */ -export function concatSeries(array: T[], iterator: PromiseListIterator): Promise { - return mapSeries(array, iterator) - .then(results => Array.prototype.concat.apply([], results.filter(a => !!a))); -} diff --git a/src/flow.ts b/src/flow.ts deleted file mode 100644 index e64f869..0000000 --- a/src/flow.ts +++ /dev/null @@ -1,245 +0,0 @@ -import type * as Tuples from "./tuples"; - -import { ensure, EnsurePromise, PromiseExecutor } from "./utils"; - -export type PromiseTaskExecutor = () => T | Promise; -export type PromiseTaskExecutorObject = Record Promise>; -export type PromizrObjectResult = { [K in keyof T]: T[K] extends () => Promise ? R : T[K]; } - -function listSeries(array: Array>): Promise { - const results: T[] = []; - const len = array.length; - let p = ensure(); - for (let i = 0; i < len; i++) { - p = p.then(createIterator(array[i])); - } - - return p.then(() => results); - - function createIterator(executor: PromiseTaskExecutor): () => Promise { - return function iterator(): Promise { - return ensure(executor()).then(result => { results.push(result); }); - } - } -} - -function objectSeries>(obj: T): Promise> { - const results: Record = {}; - let p = ensure(); - - for (const key in obj) { - if (typeof obj[key] === "function") { - p = p.then(createIterator(key, obj[key] as PromiseTaskExecutor)); - } - } - - return p.then(() => results as PromizrObjectResult); - - function createIterator(key: string, executor: PromiseTaskExecutor): () => Promise { - return function iterator(): Promise { - return ensure(executor()).then(result => { results[key] = result; }); - } - } -} - -export function series(tasks: Array>): Promise; -export function series>(tasks: T): Promise>; -export function series(tasks: unknown): Promise { - return Array.isArray(tasks) ? - listSeries(tasks) : - objectSeries(tasks as Record); -} - - -function listParallel(array: Array>): Promise { - const promises = array.map(exec => exec()); - return Promise.all(promises); -} -function objectParallel>(obj: T): Promise> { - const results: Record = {}; - const promises: Array> = [] - - for (const key in obj) { - if (typeof obj[key] === "function") { - promises.push(interator(key, obj[key] as PromiseTaskExecutor)); - } - } - - return Promise.all(promises).then(() => results as PromizrObjectResult); - - function interator(key: string, executor: PromiseTaskExecutor): Promise { - return ensure(executor()).then(result => { results[key] = result; }); - } -} - -export function parallel(tasks: Array>): Promise; -export function parallel>(tasks: T): Promise>; -export function parallel(tasks: unknown): Promise { - return Array.isArray(tasks) ? - listParallel(tasks) : - objectParallel(tasks as Record); -} - - -export function whilst(test: () => boolean, task: PromiseTaskExecutor): Promise { - return Promise.resolve().then(next); - - function next(): void | Promise { - if (test()) { - return ensure(task()).then(next); - } - } -} - -export function doWhilst(executor: PromiseTaskExecutor, test: (res?: T) => boolean): Promise { - return Promise.resolve().then(next); - - function next(): Promise { - return ensure(executor()).then(res => { - if (test(res)) { - return next(); - } - }); - } -} - -export function until(test: () => boolean, task: PromiseTaskExecutor): Promise { - return Promise.resolve().then(next); - - function next(): void | Promise { - if (!test()) { - return ensure(task()).then(next); - } - } -} - -export function doUntil(task: PromiseTaskExecutor, test: (res?: T) => boolean): Promise { - return Promise.resolve().then(next); - - function next(): Promise { - return ensure(task()).then(res => { - if (!test(res)) { - return next(); - } - }); - } -} - -export function forever(task: PromiseTaskExecutor): Promise { - return Promise.resolve().then(next); - - function next(): Promise { - return ensure(task()).then(next); - } -} - -export function waterfall(tasks: PromiseTaskExecutor[]): Promise { - let p = ensure(); - - for (const task of tasks) { - p = p.then(task); - } - - return p as Promise; -} - -type GetFirstReturnType = T extends [] ? void : ReturnType>; -type GetLastReturnType = T extends [] ? void : ReturnType>; - -export function compose(...tasks: T): (...args: Parameters>) => EnsurePromise> { - return function (this: unknown, ...args: unknown[]): EnsurePromise> { - const last = tasks.pop(); - if (!last) return ensure() as Promise; - - let p = ensure(); - p = p.then(() => last.apply(this, args)); - - for (let i = tasks.length - 1; i >= 0; i--) { - p = p.then(tasks[i]); - } - - return p as EnsurePromise>; - }; -} - -export function seq(...tasks: T): (...args: Parameters>) => EnsurePromise> { - return function (this: unknown, ...args: unknown[]): EnsurePromise> { - const first = tasks.shift(); - if (!first) return ensure() as Promise; - - let p = ensure(); - p = p.then(() => first.apply(this, args)); - - const len = tasks.length; - for (let i = 0; i < len; i++) { - p = p.then(tasks[i]); - } - - return p as EnsurePromise>; - }; -} - -export function applyEach(tasks: T): (...args: Parameters) => EnsurePromise> { - return function (this: unknown, ...args: unknown[]): EnsurePromise> { - const iterators = tasks.map(e => () => e.apply(this, args)); - return parallel(iterators) as EnsurePromise>; - }; -} - -export function applyEachSeries(tasks: T): (...args: Parameters) => EnsurePromise> { - return function (this: unknown, ...args: unknown[]): EnsurePromise> { - const iterators = tasks.map(e => () => e.apply(this, args)); - return series(iterators) as EnsurePromise>; - }; -} - - -export function retry(times: number, task: PromiseTaskExecutor): Promise { - let promise: Promise; - - try { - promise = ensure(task()); - } - catch (e) { - promise = Promise.reject(e); - } - - return promise.catch(err => { - if (times > 1) { - return retry(times - 1, task); - } - - throw err; - }); -} - -export function times(times: number, task: PromiseTaskExecutor): Promise { - const results: Array> = []; - - for (let i = times; i > 0; i--) { - try { - results.push(task()); - } - catch (e) { - results.push(Promise.reject(e)); - } - } - - return Promise.all(results); -} - -export function timesSeries(times: number, task: PromiseTaskExecutor): Promise { - const results: T[] = []; - - let p = ensure(); - for (let i = times; i > 0; i--) { - p = p.then(capture); - } - - return p.then(() => results); - - function capture(): Promise { - return ensure(task()).then(result => { results.push(result); }); - } -} - diff --git a/src/queue.ts b/src/queue.ts deleted file mode 100644 index cb12e13..0000000 --- a/src/queue.ts +++ /dev/null @@ -1,377 +0,0 @@ -import { PromiseListIterator } from "./collections"; -import { PromiseTaskExecutor, PromiseTaskExecutorObject, PromizrObjectResult } from "./flow"; -import { nextTick } from "./nextTick"; - -export interface QueueItemOptions { - data: T; - priority?: number; -} - -export interface QueueItem extends QueueItemOptions { - resolver(result: U): void; - rejecter(err: Error): void; -} -export interface QueueWorker { - (arg: T): U | Promise; -} - -export interface QueueError extends Error { - innerException?: Error | any; - innerExceptions?: Error[] | any[]; -} - -export class Queue { - protected items: Array> = []; - protected workers = 0; - - protected started = false; - protected paused = false; - protected hasException = false; - - public onempty: (() => any) | undefined; - public ondrain: (() => any) | undefined; - public onsaturated: (() => any) | undefined; - - public stopOnError = false; - public waitToReject = false; - - constructor( - protected worker: QueueWorker, - public limit: number = 1, - list?: T[]) { - - this.process = this.process.bind(this); - - if (list && list.length > 0) { - this.push(list); - } - } - - public push(data: T): Promise; - public push(datas: T[]): Promise; - public push(...datas: T[]): Promise; - public push(...datas: any[]): Promise { - if (datas.length === 1 && Array.isArray(datas[0])) { - datas = datas[0]; - } - - return this.insert(datas); - } - - public unshift(data: T): Promise; - public unshift(datas: T[]): Promise; - public unshift(...datas: T[]): Promise; - public unshift(...datas: any[]): Promise { - if (datas.length === 1 && Array.isArray(datas[0])) { - datas = datas[0]; - } - - return this.insert(datas, true); - } - - public length(): number { - return this.items.length; - } - public running(): boolean { - return this.workers > 0; - } - public idle(): boolean { - return this.items.length + this.workers === 0; - } - - public pause(): void { - if (this.paused) { - return; - } - - this.paused = true; - } - public resume(): void { - if (!this.paused || !this.hasException) { - return; - } - - this.paused = false; - this.hasException = false; - - const len = this.limit; - const process = this.process.bind(this); - - for (let i = 0; i < len; i++) { - nextTick(process); - } - } - - public clear(): void { - this.ondrain = undefined; - this.items = []; - } - - private insert(datas: T[], before?: boolean): Promise { - let resolver: (result?: U | U[] | PromiseLike) => void, - rejecter: (err?: any) => void; - - const - promise = new Promise((res, rej) => { resolver = res; rejecter = rej; }), - length = datas.length, - errors: any[] = [], - results: U[] = []; - - if (length === 0) { - return Promise.resolve([]); - } - - if (!this.started) { - this.started = true; - } - - if (before) this.items.unshift(...datas.map(capture, this)); - else this.items.push(...datas.map(capture, this)); - - if (this.onsaturated && this.items.length >= this.limit) { - this.onsaturated(); - } - - return promise; - - function capture(this: Queue, data: T): QueueItem { - nextTick(() => this.process); - return this.createItem(data, results, errors, length, resolver, rejecter); - } - } - - protected createItem(data: T, results: U[], errors: any[], count: number, resolve: (result: U | U[] | PromiseLike) => void, reject: (err?: any) => void): QueueItem { - return { - data: data, - resolver: res => { - results.push(res); - - if (results.length === count) { - resolve(count === 1 ? results[0] : results); - } - }, - rejecter: err => { - if (!this.waitToReject) { - return reject(err); - } - - errors.push(err); - - if (errors.length + results.length === count) { - const error: QueueError = new Error("Queue worker exception"); - - count === 1 ? - error.innerException = errors[0] : - error.innerExceptions = errors; - - reject(error); - } - } - }; - } - - protected process(): void { - if (!this.paused && this.workers < this.limit && this.items.length > 0 && !(this.stopOnError && this.hasException)) { - const item = this.items.shift(); - if (!item) { - return; - } - - if (this.onempty && this.items.length === 0) { - this.onempty(); - } - - this.workers += 1; - Promise.resolve(this.worker(item.data)).then( - res => { - item.resolver.call(undefined, res); - this.onProcessEnd(); - }, - err => { - item.rejecter.call(undefined, err); - this.hasException = true; - this.onProcessEnd(); - } - ); - } - } - - protected onProcessEnd(): void { - this.workers -= 1; - - if (this.ondrain && this.items.length + this.workers === 0) { - this.ondrain(); - } - - this.process(); - } -} - -export class PriorityQueue extends Queue { - public defaultPriority = 1; - - constructor(worker: QueueWorker, limit?: number, list?: T[]) { - super(worker, limit, list); - } - - public push(priority: number, data?: T): Promise; - public push(priority: number, datas: T[]): Promise; - public push(priority: number, ...datas: T[]): Promise; - public push(data: T): Promise; - public push(datas: T[]): Promise; - public push(...datas: T[]): Promise; - public push(...datas: any[]): Promise { - let priority = this.defaultPriority; - if (typeof datas[0] === "number") { - priority = datas.shift(); - } - - if (datas.length === 1 && Array.isArray(datas[0])) { - datas = datas[0]; - } - - return this.insertAt(datas, priority); - } - - public unshift(priority: number, data?: T): Promise; - public unshift(priority: number, datas: T[]): Promise; - public unshift(priority: number, ...datas: T[]): Promise; - public unshift(data: T): Promise; - public unshift(datas: T[]): Promise; - public unshift(...datas: T[]): Promise; - public unshift(...datas: any[]): Promise { - let priority = this.defaultPriority; - if (typeof datas[0] === "number") { - priority = datas.shift(); - } - - if (datas.length === 1 && Array.isArray(datas[0])) { - datas = datas[0]; - } - - return this.insertAt(datas, priority); - } - - private insertAt(datas: T[], priority: number): Promise { - const length = datas.length; - if (length === 0) { - return Promise.resolve([]); - } - - let resolver: (result?: U | U[] | PromiseLike) => void, - rejecter: (err?: any) => void; - - const - promise = new Promise((res, rej) => { resolver = res; rejecter = rej; }), - errors: any[] = [], - results: U[] = [], - index = this.binarySearch(this.items, { priority: priority }, this.compareTasks) + 1; - - if (!this.started) { - this.started = true; - } - - this.items.splice(index, 0, ...datas.map(capture, this)); - - if (this.onsaturated && this.items.length >= this.limit) { - this.onsaturated(); - } - - return promise; - - function capture(this: PriorityQueue, data: T): QueueItem { - const item = this.createItem(data, results, errors, length, resolver, rejecter); - item.priority = priority; - - nextTick(this.process); - - return item; - } - } - - private binarySearch(seq: Array>, item: { priority?: number }, compare: (a: { priority?: number }, b: { priority?: number }) => number): number { - let beg = -1; - let end = seq.length - 1; - - let mid: number; - while (beg < end) { - mid = beg + ((end - beg + 1) >>> 1); - - if (compare(item, seq[mid]) >= 0) { - beg = mid; - } - else { - end = mid - 1; - } - } - - return beg; - } - private compareTasks(a: { priority?: number }, b: { priority?: number }): number { - return (a.priority || 0) - (b.priority || 0); - } -} - -export class TaskQueue extends Queue, T> { - - constructor(limit?: number, list?: Array>) { - super(item => item(), limit, list); - } - -} - -export class PriorityTaskQueue extends PriorityQueue, T> { - - constructor(limit?: number, list?: Array>) { - super(item => item(), limit, list); - } - -} - - -export function queue(worker: QueueWorker, limit?: number, list?: T[]): Queue { - return new Queue(worker, limit, list); -} - -export function priorityQueue(worker: QueueWorker, limit?: number, list?: T[]): PriorityQueue { - return new PriorityQueue(worker, limit, list); -} - -export function taskQueue(limit?: number, list?: Array>): TaskQueue { - return new TaskQueue(limit, list); -} - -export function priorityTaskQueue(limit?: number, list?: Array>): PriorityTaskQueue { - return new PriorityTaskQueue(limit, list); -} - - -export function eachLimit(array: T[], limit: number, iterator: PromiseListIterator): Promise { - const iterators = array.map>((value, index, list) => () => iterator(value, index, list)); - return taskQueue(limit).push(iterators).then(() => void 0); -} - -export function mapLimit(array: T[], limit: number, iterator: PromiseListIterator): Promise { - const iterators = array.map>((value, index, list) => () => iterator(value, index, list)); - return taskQueue(limit).push(iterators); -} - -export function parallelLimit(tasks: Array>, limit: number): Promise; -export function parallelLimit(tasks: PromiseTaskExecutorObject, limit: number): Promise>; -export function parallelLimit(tasks: Array> | PromiseTaskExecutorObject, limit: number): Promise> { - if (Array.isArray(tasks)) { - return taskQueue(limit).push(>>tasks); - } - - const obj = >tasks; - const data = Object.keys(obj); - const result: any = {}; - - return queue(worker, limit).push(data).then(() => result); - - function worker(key: string): Promise { - return obj[key]().then(res => { - result[key] = res; - }); - } -} diff --git a/src/tuples.ts b/src/tuples.ts deleted file mode 100644 index e418505..0000000 --- a/src/tuples.ts +++ /dev/null @@ -1,15 +0,0 @@ -export type GetLength = Tuple extends { length: infer L } ? L : -1; -export type GetLast = Tuple[PreviousIndex>]; -export type GetLast2 = Tuple[PreviousIndex>>]; -export type GetFirst = Tuple[0]; - -export type Prepend = [first: Item, ...rest: Tuple]; -export type Append = [...tuple: Tuple, last: Item]; -export type Concat = [...tuple: Tuple1, ...append: Tuple2]; - -export type RemoveFirst = Tuple extends [first: any, ...result: infer Result] ? Result : Tuple; -export type RemoveLast = Tuple extends [...result: infer Result, last: any] ? Result : Tuple; -export type RemoveFromStart = Tuple extends [...start: ToRemove, ...result: infer Result] ? Result : Tuple; -export type RemoveFromEnd = Tuple extends [...result: infer Result, ...end: ToRemove] ? Result : Tuple; - -export type PreviousIndex = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62][T]; diff --git a/src/utils.ts b/src/utils.ts deleted file mode 100644 index c66aceb..0000000 --- a/src/utils.ts +++ /dev/null @@ -1,260 +0,0 @@ -import type * as Tuples from "./tuples"; -import { nextTick } from "./nextTick"; - -export type PromiseExecutor = (...args: any[]) => T | Promise; - -export type UnPromise = T extends Promise ? R : T; -export type EnsurePromise = Promise>; - -type TypedFunction = (...args: any[]) => T; -type HashFunction = (args: any[]) => string; - -export type MethodNames = { - [K in keyof T]: T[K] extends (...args: unknown[]) => unknown ? K : never; -}[keyof T]; - -type PartialParameters any> = T extends (...args: infer P) => any ? Partial

: never; -type RestOfParameters any, UsedParameters extends any[]> = Tuples.RemoveFromStart, UsedParameters>; -type ParametersWithoutLast any> = Tuples.RemoveFromEnd, [Tuples.GetLast>]>; -type ParametersWithoutLast2 any> = Tuples.RemoveFromEnd, [Tuples.GetLast2>, Tuples.GetLast>]>; - -type NodeStyleCallback = (err: any, ...rest: T[]) => any; -type NodeStyleCallbackResultType = - T extends (err: any) => any ? void : - T extends (err: any, rest: infer Result) => any ? Result : - T extends (err: any, ...rest: infer Results) => any ? Results : - void; - -type FunctionWithNodeStyleCallback = (...args: [...any, NodeStyleCallback]) => any; -type FunctionWithNodeStyleCallbackReturnType = NodeStyleCallbackResultType>>; - -type ErrorCalback = (err: Error) => any; -type SimpleCallback = (...args: T[]) => any; -type SimpleCallbackResultType = - T extends () => any ? void : - T extends (arg: infer Result) => any ? Result : - T extends (...args: infer Results) => any ? Results : - void; - -type FunctionWithMultiCallbacks = (...args: [...any, SimpleCallback, ErrorCalback]) => any; -type FunctionWithMultiCallbacksReturnType = SimpleCallbackResultType>>; - -export interface Deferred { - resolve(val?: T | PromiseLike): void; - reject(err?: any): void; - - promise: Promise; -} - -export function apply(task: T, ...args: Parameters): () => ReturnType { - return () => { - return task(...args); - }; -} -export function applyOn>(owner: O, task: K, ...args: Parameters): () => ReturnType; -export function applyOn(owner: O, task: T, ...args: Parameters): () => ReturnType; -export function applyOn(owner: Record, task: string | TypedFunction, ...args: any[]): TypedFunction { - if (typeof task === "string") { - return () => owner[task](...args); - } - - return () => task.apply(owner, args); -} - -export function partial>(task: Method, ...topArgs: Arguments): (...args: RestOfParameters) => ReturnType { - return (...args) => { - return task(...topArgs.concat(args)); - }; -} -export function partialOn, Arguments extends PartialParameters>(owner: O, task: Key, ...args: Arguments): (...args: RestOfParameters) => ReturnType; -export function partialOn>(owner: O, task: Method, ...args: Arguments): (...args: RestOfParameters) => ReturnType; -export function partialOn(owner: Record, task: string | TypedFunction, ...topArgs: any[]): TypedFunction { - if (typeof task === "string") { - return (...args) => owner[task](...topArgs.concat(args)); - } - - return (...args) => task.apply(owner, topArgs.concat(args)); -} - -export function tap(task: Task, ...args: Parameters): (arg: U) => Promise { - return (result) => ensure(task(...args)).then(() => result); -} -export function tapOn>(owner: O, task: K, ...args: Parameters): (arg: U) => Promise; -export function tapOn(owner: O, task: T, ...args: Parameters): (arg: U) => Promise; -export function tapOn(owner: Record, task: string | TypedFunction, ...args: any[]): (arg: U) => Promise { - if (typeof task === "string") { - return (res) => ensure(owner[task](...args)).then(() => res); - } - - return (res) => task.apply(owner, args).then(() => res); -} - -export function memoize>(task: T, hash?: boolean | HashFunction): (...args: Parameters) => EnsurePromise> { - const cache: Record> | EnsurePromise>> = {}; - const hasher: HashFunction | undefined = typeof hash === "function" ? hash : hash === true ? JSON.stringify : undefined; - - function save(hashed: string | undefined, value: UnPromise> | EnsurePromise>): typeof value { - cache[hashed || "default"] = value; - return value; - } - - return (...args: Parameters) => { - const hashed = hasher?.(args); - const cached = cache[hashed || "default"]; - - if (cached) { - return ensure(cached); - } - - const promise = ensure(task(...args)) - .then(res => save(hashed, res)); - - return save(hashed, promise); - } -} - -export function log(task: T, ...args: Parameters): EnsurePromise { - return ensure(task(...args)).then( - result => { console.log(result); return result; }, - err => { console.error(err); throw err; } - ); -} - -export function dir(task: T, ...args: Parameters): EnsurePromise { - return ensure(task(...args)).then( - result => { console.dir(result); return result; }, - err => { console.error(err); throw err; } - ); -} - -export function timeout(ms?: number): Promise { - return new Promise(resolve => { - setTimeout(() => { resolve(); }, ms || 1); - }); -} - -export function immediate(): Promise { - return new Promise(resolve => { nextTick(resolve); }); -} - -export function denodify(fn: T, ...args: ParametersWithoutLast): EnsurePromise>; -export function denodify, T extends FunctionWithNodeStyleCallback>(owner: O, fn: T, ...args: ParametersWithoutLast): EnsurePromise>; -export function denodify(ownerOrFn: Record | undefined, ...args: unknown[]): Promise { - let owner = ownerOrFn, - fn = args[0] as FunctionWithNodeStyleCallback, - num = 1; - - if (typeof owner === "function" && typeof fn !== "function") { - fn = owner; - owner = undefined; - num = 0; - } - - return promisify(owner, fn)(...args.slice(num)); -} - -export function promisify(fn: T): (...args: ParametersWithoutLast) => EnsurePromise>; -export function promisify(owner: O, fn: T): (...args: ParametersWithoutLast) => EnsurePromise>; -export function promisify(owner: Record | undefined, fn?: T): (...args: ParametersWithoutLast) => EnsurePromise> { - if (typeof owner === "function" && typeof fn !== "function") { - fn = owner; - owner = undefined; - } - - if (!fn) { - throw new TypeError("fn should be provided!"); - } - - const executor = fn; - return (...args) => { - return new Promise((resolve, reject) => { - executor.apply(owner, [...args, callback]); - - function callback(err: Error, ...results: any[]): void { - if (err) return reject(err); - if (results.length === 0) return resolve(); - if (results.length === 1) return resolve(results[0]); - - resolve(results as any); - } - }); - }; -} - -export function uncallbackify(fn: T, ...args: ParametersWithoutLast2): EnsurePromise>; -export function uncallbackify, T extends FunctionWithMultiCallbacks>(owner: O, fn: T, ...args: ParametersWithoutLast2): EnsurePromise>; -export function uncallbackify(ownerOrFn: Record | undefined, ...args: unknown[]): Promise { - let owner = ownerOrFn, - fn = args[0] as FunctionWithMultiCallbacks, - num = 1; - - if (typeof owner === "function" && typeof fn !== "function") { - fn = owner; - owner = undefined; - num = 0; - } - - return cbpromisify(owner, fn)(...args.slice(num)); -} - -export function cbpromisify(fn: T): (...args: ParametersWithoutLast2) => EnsurePromise>; -export function cbpromisify(owner: O, fn: T): (...args: ParametersWithoutLast2) => EnsurePromise>; -export function cbpromisify(owner: Record | undefined, fn?: T): (...args: ParametersWithoutLast2) => EnsurePromise> { - if (typeof owner === "function" && typeof fn !== "function") { - fn = owner; - owner = undefined; - } - - if (!fn) { - throw new TypeError("fn should be provided!"); - } - - const executor = fn; - return (...args) => { - return new Promise((resolve, reject) => { - executor.apply(owner, [...args, success, error]); - - function success(...results: any[]): void { - if (results.length === 0) return resolve(); - if (results.length === 1) return resolve(results[0]); - resolve(results as any); - } - - function error(...errors: any[]): void { - if (errors.length === 1) { - errors = errors[0]; - } - - if (!(errors instanceof Error)) { - const err: any = new Error(errors.toString()); - err.innerError = errors; - return reject(errors); - } - - reject(errors); - } - }); - }; -} - -export function defer(): Deferred { - const dfd: any = {}; - - dfd.promise = new Promise((resolve, reject) => { - dfd.resolve = resolve; - dfd.reject = reject; - }); - - return dfd; -} - -export function ensure(): Promise; -export function ensure(value: null | undefined): Promise; -export function ensure(value: T | Promise): Promise; -export function ensure(value?: unknown): Promise { - if (typeof value === "undefined") { - return Promise.resolve(); - } - - return Promise.resolve(value); -} diff --git a/tests/PriorityQueue.spec.ts b/tests/PriorityQueue.spec.ts new file mode 100644 index 0000000..4084339 --- /dev/null +++ b/tests/PriorityQueue.spec.ts @@ -0,0 +1,224 @@ +import PriorityQueue from "../lib/PriorityQueue"; +import QueueError from "../lib/QueueError"; + +import immediate from "../lib/immediate"; + +import timeout from "./helpers/timeout"; +import createDeferreds from "./helpers/createDeferreds"; + +const LIST = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; +const TOTAL = LIST.length; +const LIMIT = 3; + +describe("promizr.PriorityQueue", () => { + + describe("constructor", () => { + + test("should not start queue if no list provided", async () => { + const spy = jest.fn(); + const instance = new PriorityQueue(spy, LIMIT); + + await timeout(10); + expect(spy).not.toHaveBeenCalled(); + }); + + // test("should start queue if list provided", async () => { + // const spy = jest.fn(); + // const instance = new PriorityQueue(spy, limit, list); + + // await timeout(20); + // sinon.assert.callCount(spy, list.length); + // }); + + // test("should start queue asynchronously", () => { + // const spy = jest.fn(); + // const instance = new PriorityQueue(spy, limit, list); + + // expect(spy).not.toHaveBeenCalled(); + // }); + + }); + + describe(".push()", () => { + + test("returns a Promise which resolves when all tasks are done", async () => { + const spy = jest.fn(() => timeout(10)); + const instance = new PriorityQueue(spy, LIMIT); + + const promise = instance.push(LIST); + expect(promise).toEqual(expect.any(Promise)); + + await expect(promise) + .resolves.toEqual(expect.any(Array)); + + expect(spy).toHaveBeenCalledTimes(TOTAL); + }); + + test("should resolve with an array containing results of each workers", async () => { + const instance = new PriorityQueue(i => i, LIMIT); + + const res = await instance.push(LIST); + + expect(res).toEqual(LIST); + expect(res).not.toBe(LIST); + }); + + test("should resolve with only one item if only one item is passed to pushed function", async () => { + const instance = new PriorityQueue(i => i, LIMIT); + + const res = await instance.push(LIST[0]); + expect(res).toBe(LIST[0]); + }); + + test("should reject immediately if any item reject", async () => { + const spy = jest.fn(n => Promise.reject(n)); + const instance = new PriorityQueue(n => immediate().then(() => spy(n)), LIMIT); + + await expect(instance.push(LIST)) + .rejects.toBe(LIST[0]); + + expect(spy).toHaveBeenCalledTimes(1); + }); + + test("should wait to reject QueueError if waitToReject is set to true", async () => { + const spy = jest.fn(n => n % 2 === 0 ? Promise.reject(n) : Promise.resolve(n)); + const instance = new PriorityQueue(n => immediate().then(() => spy(n)), LIMIT, { waitToReject: true }); + + await expect(instance.push(LIST)) + .rejects.toBeInstanceOf(QueueError); + + await expect(instance.push(LIST)) + .rejects.toHaveProperty("innerErrors", LIST.filter(n => n % 2 === 0)); + + await expect(instance.push(LIST)) + .rejects.toHaveProperty("results", LIST.filter(n => n % 2 === 1)); + }); + + }); + + describe("#process()", () => { + + test("should only launch specified limit number of task simultaneously", async () => { + const spy = jest.fn(() => timeout(20)); + + const instance = new PriorityQueue(spy, LIMIT); + instance.push(LIST); + + await timeout(10); + + expect(spy).toHaveBeenCalledTimes(LIMIT); + }); + + test("should continue to limit number of task simultaneously when promises are resolved", async () => { + const dfds = createDeferreds(LIST) + const spy = jest.fn(n => dfds[n].promise); + + const instance = new PriorityQueue(spy, LIMIT); + instance.push(LIST); + + expect(spy).not.toHaveBeenCalled(); + + await timeout(10); + + expect(spy).toHaveBeenCalledTimes(LIMIT); + + for (let i = 0, len = LIMIT; i < len; i++) { + dfds[i].resolve(); + } + + await dfds[LIMIT - 1].promise; + await timeout(10); + + expect(spy).toHaveBeenCalledTimes(LIMIT * 2); + + for (let i = LIMIT, len = LIMIT * 3; i < len; i++) { + dfds[i].resolve(); + } + + await dfds[LIMIT * 2 - 1].promise; + await timeout(10); + + expect(spy).toHaveBeenCalledTimes(LIMIT * 4); + }); + + test("should continue to execute works even if exception occurred", async () => { + const err = new Error("test"); + const dfds = createDeferreds(LIST) + + const spy = jest.fn(n => { + return timeout(n).then(() => { + throw err; + }); + }); + + const instance = new PriorityQueue(spy, LIMIT); + await expect(instance.push(LIST)) + .rejects.toBe(err); + + expect(spy).toHaveBeenCalledTimes(LIMIT); + + await timeout(100); + + expect(spy).toHaveBeenCalledTimes(TOTAL); + }); + + test("should not continue to execute works if exception occurred and stopOnError is set to true", async () => { + const err = new Error("This is an error !"); + const dfds = createDeferreds(LIST) + const spy = jest.fn(n => dfds[n].promise); + + const instance = new PriorityQueue(spy, LIMIT, { stopOnError: true }); + const promise = instance.push(LIST); + + expect(spy).not.toHaveBeenCalled(); + + await timeout(10); + + expect(spy).toHaveBeenCalledTimes(LIMIT); + + for (let i = 0, len = LIMIT; i < len; i++) { + dfds[i].reject(err); + } + + await expect(promise) + .rejects.toBe(err); + + expect(spy).toHaveBeenCalledTimes(LIMIT); + }); + + describe("with priority", () => { + + test("call tasks in priority order", async () => { + const firstSpy = jest.fn(() => { + expect(secondSpy).not.toHaveBeenCalled(); + expect(thirdSpy).not.toHaveBeenCalled(); + }); + + const secondSpy = jest.fn(() => { + expect(firstSpy).toHaveBeenCalled(); + expect(thirdSpy).not.toHaveBeenCalled(); + }); + + const thirdSpy = jest.fn(() => { + expect(firstSpy).toHaveBeenCalled(); + expect(secondSpy).toHaveBeenCalled(); + }); + + const instance = new PriorityQueue(immediate, LIMIT); + + await Promise.all([ + instance.push(5, LIST).then(thirdSpy), + instance.push(1, LIST).then(firstSpy), + instance.push(3, LIST).then(secondSpy) + ]); + + expect(firstSpy).toHaveBeenCalledTimes(1); + expect(secondSpy).toHaveBeenCalledTimes(1); + expect(thirdSpy).toHaveBeenCalledTimes(1); + }); + + }); + + }); + +}); \ No newline at end of file diff --git a/tests/PriorityTaskQueue.spec.ts b/tests/PriorityTaskQueue.spec.ts new file mode 100644 index 0000000..0fd6c62 --- /dev/null +++ b/tests/PriorityTaskQueue.spec.ts @@ -0,0 +1,32 @@ +import PriorityTaskQueue from "../lib/PriorityTaskQueue"; +import PriorityQueue from "../lib/Queue"; + +import timeout from "./helpers/timeout"; + +describe("promizr.PriorityTaskQueue", () => { + + test("should extends PriorityQueue", () => { + const queue = new PriorityTaskQueue(3); + expect(queue).toBeInstanceOf(PriorityTaskQueue); + expect(queue).toBeInstanceOf(PriorityQueue); + }); + + test("should call the task passed as items", async () => { + const queue = new PriorityTaskQueue(3); + + const spies = [ + jest.fn(() => timeout(5)), + jest.fn(() => timeout(1)), + jest.fn(() => timeout(10)), + jest.fn(() => timeout(8)), + jest.fn(() => timeout(15)) + ]; + + await queue.push(spies); + + for (const spy of spies) { + expect(spy).toHaveBeenCalledTimes(1); + } + }); + +}); diff --git a/tests/ProgressPromise.spec.ts b/tests/ProgressPromise.spec.ts new file mode 100644 index 0000000..db65273 --- /dev/null +++ b/tests/ProgressPromise.spec.ts @@ -0,0 +1,308 @@ +import ProgressPromise from "../lib/ProgressPromise"; + +import ProgressContext from "./helpers/ProgressContext"; + +describe("promizr.ProgressPromise", () => { + + describe("contructor", () => { + + test("should call executor arguments with three functions", () => { + const spy = jest.fn(); + new ProgressPromise(spy); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(expect.any(Function), expect.any(Function), expect.any(Function)); + }); + + test("should resolve when first function is called", async () => { + const spy = jest.fn(); + + let resolve: (value?: string) => void = () => void 0; + const promise = new ProgressPromise(r => resolve = r).then(spy); + + expect(spy).not.toHaveBeenCalled(); + + resolve("result"); + + await promise; + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith("result"); + }); + + test("should reject when second function is called", async () => { + const spy = jest.fn(); + + let reject: (reason: unknown) => void = () => void 0; + const promise = new ProgressPromise((r1, r2) => reject = r2).catch(spy); + + expect(spy).not.toHaveBeenCalled(); + + const err = new Error("test"); + reject(err); + + await promise; + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(err); + }); + + test("should reject if executor function throw an error", async () => { + const err = new Error("This is an error"); + const spy = jest.fn(); + + const promise = new ProgressPromise(() => { throw err; }).catch(spy); + + expect(spy).not.toHaveBeenCalled(); + + await promise; + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(err); + }); + + test("should dispatch progress when calling the thrid callback", async () => { + let progress: (value: number) => void = () => void 0; + const promise = new ProgressPromise((r, re, pr) => { progress = pr; }); + + const spy1 = jest.fn(); + const spy2 = jest.fn(); + + promise.progress(spy1).progress(spy2); + + expect(spy1).not.toHaveBeenCalled(); + expect(spy2).not.toHaveBeenCalled(); + + progress(0.5); + + expect(spy1).toHaveBeenCalledTimes(1); + expect(spy2).toHaveBeenCalledTimes(1); + + expect(spy1).toHaveBeenCalledWith(0.5); + expect(spy2).toHaveBeenCalledWith(0.5); + }); + + }); + + describe(".progress()", () => { + + test("should returns this", async () => { + const p1 = new ProgressPromise(jest.fn()); + const p2 = p1.progress(); + + expect(p1).toBe(p2); + }); + + test("should register callbacks passed to progress", async () => { + let progress: (value: number) => void = () => void 0; + const promise = new ProgressPromise((r, re, pr) => { progress = pr; }); + + const spy1 = jest.fn(); + const spy2 = jest.fn(); + + promise.progress(spy1).progress(spy2); + + expect(spy1).not.toHaveBeenCalled(); + expect(spy2).not.toHaveBeenCalled(); + + progress(0.5); + + expect(spy1).toHaveBeenCalledTimes(1); + expect(spy2).toHaveBeenCalledTimes(1); + + expect(spy1).toHaveBeenCalledWith(0.5); + expect(spy2).toHaveBeenCalledWith(0.5); + }); + + test("should not dispatch progress after resolution", async () => { + let resolve: (value: string) => void = () => void 0; + let progress: (value: number) => void = () => void 0; + const promise = new ProgressPromise((r, re, pr) => { resolve = r; progress = pr; }); + + const spy1 = jest.fn(); + const spy2 = jest.fn(); + + promise.progress(spy1).progress(spy2); + + expect(spy1).not.toHaveBeenCalled(); + expect(spy2).not.toHaveBeenCalled(); + + progress(0.5); + + expect(spy1).toHaveBeenCalledTimes(1); + expect(spy2).toHaveBeenCalledTimes(1); + + resolve("result"); + await promise; + + progress(1); + + expect(spy1).toHaveBeenCalledTimes(1); + expect(spy2).toHaveBeenCalledTimes(1); + }); + + test("should not register callback after resolution", async () => { + let resolve: (value: string) => void = () => void 0; + let progress: (value: number) => void = () => void 0; + const promise = new ProgressPromise((r, re, pr) => { resolve = r; progress = pr; }); + + const spy1 = jest.fn(); + const spy2 = jest.fn(); + const spy3 = jest.fn(); + + promise.progress(spy1).progress(spy2); + + progress(0.5); + + expect(spy1).toHaveBeenCalledTimes(1); + expect(spy2).toHaveBeenCalledTimes(1); + + resolve("result"); + await promise; + + promise.progress(spy3); + + expect(spy3).toHaveBeenCalledTimes(1); + + progress(1); + + expect(spy1).toHaveBeenCalledTimes(1); + expect(spy2).toHaveBeenCalledTimes(1); + expect(spy3).toHaveBeenCalledTimes(1); + }); + + test("should call onprogress immediately if progress already have a value", async () => { + let progress: (value: number) => void = () => void 0; + const promise = new ProgressPromise((r, re, pr) => { progress = pr; }); + + const spy1 = jest.fn(); + + progress(0.5); + + expect(spy1).not.toHaveBeenCalled(); + + promise.progress(spy1); + + expect(spy1).toHaveBeenCalledTimes(1); + }); + + }); + + describe("static .all()", () => { + + test("should return an instance of ProgressPromise", () => { + const result = ProgressPromise.all([]); + expect(result).toBeInstanceOf(ProgressPromise); + }); + + test("should report progresses of each inner ProgressPromises", () => { + const spy = jest.fn(); + const ctx = new ProgressContext(3); + + const promise = ProgressPromise.all(ctx.promises()).progress(spy); + + ctx.progress(0, 1); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith([1, undefined, undefined]); + + ctx.progress(1, 1); + + expect(spy).toHaveBeenCalledTimes(2); + expect(spy).toHaveBeenCalledWith([1, 1, undefined]); + + ctx.progress(2, 1); + + expect(spy).toHaveBeenCalledTimes(3); + expect(spy).toHaveBeenCalledWith([1, 1, 1]); + }); + + test("should report progresses of each inner ProgressPromises and undefined for others", () => { + const spy = jest.fn(); + const ctx = new ProgressContext(3).addFake(); + + const promise = ProgressPromise.all(ctx.promises()).progress(spy); + + ctx.progress(0, 1); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith([1, undefined, undefined, undefined]); + + ctx.progress(1, 1); + + expect(spy).toHaveBeenCalledTimes(2); + expect(spy).toHaveBeenCalledWith([1, 1, undefined, undefined]); + + ctx.progress(2, 1); + + expect(spy).toHaveBeenCalledTimes(3); + expect(spy).toHaveBeenCalledWith([1, 1, 1, undefined]); + + ctx.progress(3, 1); + + expect(spy).toHaveBeenCalledTimes(3); + expect(spy).toHaveBeenCalledWith([1, 1, 1, undefined]); + }); + + }); + + describe("static .race()", () => { + + test("should return an instance of ProgressPromise", () => { + const result = ProgressPromise.race([]); + expect(result).toBeInstanceOf(ProgressPromise); + }); + + test("should report progresses of each inner ProgressPromises", () => { + const spy = jest.fn(); + const ctx = new ProgressContext(3); + + const promise = ProgressPromise.race(ctx.promises()).progress(spy); + + ctx.progress(0, 1); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith([1, undefined, undefined]); + + ctx.progress(1, 1); + + expect(spy).toHaveBeenCalledTimes(2); + expect(spy).toHaveBeenCalledWith([1, 1, undefined]); + + ctx.progress(2, 1); + + expect(spy).toHaveBeenCalledTimes(3); + expect(spy).toHaveBeenCalledWith([1, 1, 1]); + }); + + test("should report progresses of each inner ProgressPromises and undefined for others", () => { + const spy = jest.fn(); + const ctx = new ProgressContext(3).addFake(); + + const promise = ProgressPromise.race(ctx.promises()).progress(spy); + + ctx.progress(0, 1); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith([1, undefined, undefined, undefined]); + + ctx.progress(1, 1); + + expect(spy).toHaveBeenCalledTimes(2); + expect(spy).toHaveBeenCalledWith([1, 1, undefined, undefined]); + + ctx.progress(2, 1); + + expect(spy).toHaveBeenCalledTimes(3); + expect(spy).toHaveBeenCalledWith([1, 1, 1, undefined]); + + ctx.progress(3, 1); + + expect(spy).toHaveBeenCalledTimes(3); + expect(spy).toHaveBeenCalledWith([1, 1, 1, undefined]); + }); + + }); + +}); + diff --git a/tests/Queue.spec.ts b/tests/Queue.spec.ts new file mode 100644 index 0000000..e5dc896 --- /dev/null +++ b/tests/Queue.spec.ts @@ -0,0 +1,439 @@ +import Queue from "../lib/Queue"; +import QueueError from "../lib/QueueError"; + +import immediate from "../lib/immediate"; + +import timeout from "./helpers/timeout"; +import createDeferreds from "./helpers/createDeferreds"; + +const LIST = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; +const TOTAL = LIST.length; +const LIMIT = 3; + +describe("promizr.Queue", () => { + + describe(".constructor()", () => { + + test("should not start queue if no list provided", async () => { + const spy = jest.fn(); + + new Queue(spy, LIMIT); + + await timeout(10); + + expect(spy).not.toHaveBeenCalled(); + }); + + test("should assign options to instance", async () => { + const instance = new Queue(i => i, LIMIT, { + stopOnError: true, + waitToReject: true + }); + + expect(instance).toHaveProperty("stopOnError", true); + expect(instance).toHaveProperty("waitToReject", true); + }); + + }); + + describe(".push()", () => { + + test("should returns a Promise which resolves when all tasks are done", async () => { + const spy = jest.fn(() => timeout(10)); + const instance = new Queue(spy, LIMIT); + + const promise = instance.push(LIST); + expect(promise).toEqual(expect.any(Promise)); + + await expect(promise) + .resolves.toEqual(expect.any(Array)); + + expect(spy).toHaveBeenCalledTimes(TOTAL); + }); + + test("should resolve with an array containing results of each workers", async () => { + const instance = new Queue(i => i, LIMIT); + + const res = await instance.push(LIST); + + expect(res).toEqual(LIST); + expect(res).not.toBe(LIST); + }); + + test("should resolve with only one item if only one item is passed to pushed function", async () => { + const instance = new Queue(i => i, LIMIT); + + const res = await instance.push(LIST[0]); + expect(res).toBe(LIST[0]); + }); + + test("should reject immediately if any item reject", async () => { + const spy = jest.fn(n => Promise.reject(n)); + const instance = new Queue(n => immediate().then(() => spy(n)), LIMIT); + + await expect(instance.push(LIST)) + .rejects.toBe(LIST[0]); + + expect(spy).toHaveBeenCalledTimes(1); + }); + + test("should wait to reject QueueError if waitToReject is set to true", async () => { + const spy = jest.fn(n => n % 2 === 0 ? Promise.reject(n) : Promise.resolve(n)); + const instance = new Queue(n => immediate().then(() => spy(n)), LIMIT, { waitToReject: true }); + + await expect(instance.push(LIST)) + .rejects.toBeInstanceOf(QueueError); + + await expect(instance.push(LIST)) + .rejects.toHaveProperty("innerErrors", LIST.filter(n => n % 2 === 0)); + + await expect(instance.push(LIST)) + .rejects.toHaveProperty("results", LIST.filter(n => n % 2 === 1)); + }); + + }); + + describe(".unshift()", () => { + + test("returns a Promise which resolves when all tasks are done", async () => { + const spy = jest.fn(() => timeout(10)); + const instance = new Queue(spy, LIMIT); + + const promise = instance.unshift(LIST); + expect(promise).toEqual(expect.any(Promise)); + + await expect(promise) + .resolves.toEqual(expect.any(Array)); + + expect(spy).toHaveBeenCalledTimes(TOTAL); + }); + + test("should resolve with an array containing results of each workers", async () => { + const instance = new Queue(i => i, LIMIT); + + const res = await instance.unshift(LIST); + + expect(res).toEqual(LIST); + expect(res).not.toBe(LIST); + }); + + test("should resolve with only one item if only one item is passed to unshift function", async () => { + const instance = new Queue(i => i, LIMIT); + + const res = await instance.unshift(LIST[0]); + expect(res).toBe(LIST[0]); + }); + + test("should resolve before push", async () => { + const pushDfds = createDeferreds(LIST); + const unshiftDfds = createDeferreds(LIST); + + const spy = jest.fn(n => { + const dfds = n > TOTAL ? pushDfds : unshiftDfds; + return dfds[n % TOTAL].promise.then(() => n); + }); + + const instance = new Queue(spy, LIMIT); + const pushPromise = instance.push(LIST.map(n => n + TOTAL)); + const unshiftPromise = instance.unshift(LIST); + + for (let i = 0, len = TOTAL; i < len; i++) { + unshiftDfds[i].resolve(); + } + + for (let i = 0, len = TOTAL; i < len; i++) { + pushDfds[i].resolve(); + } + + await unshiftPromise; + + expect(spy).not.toHaveBeenCalledTimes(TOTAL * 2); + + await pushPromise; + + expect(spy).toHaveBeenCalledTimes(TOTAL * 2); + }); + + }); + + describe(".pause()", () => { + + test("should stop launching task while in pause", async () => { + const dfds = createDeferreds(LIST) + const spy = jest.fn(n => dfds[n].promise); + + const instance = new Queue(spy, LIMIT); + instance.push(LIST); + + await timeout(10); + + expect(spy).toHaveBeenCalledTimes(LIMIT); + + for (let i = 0, len = LIMIT; i < len; i++) { + dfds[i].resolve(); + } + + await dfds[LIMIT - 1].promise; + await timeout(10); + + expect(spy).toHaveBeenCalledTimes(LIMIT * 2); + + instance.pause(); + + for (let i = LIMIT, len = LIMIT * 3; i < len; i++) { + dfds[i].resolve(); + } + + await dfds[LIMIT * 2 - 1].promise; + await timeout(10); + + expect(spy).toHaveBeenCalledTimes(LIMIT * 2); + + }); + + }); + + describe(".resume()", () => { + + test("should resume launching task", async () => { + const dfds = createDeferreds(LIST) + const spy = jest.fn(n => dfds[n].promise); + + const instance = new Queue(spy, LIMIT); + instance.push(LIST); + + await timeout(10); + + expect(spy).toHaveBeenCalledTimes(LIMIT); + + for (let i = 0, len = LIMIT; i < len; i++) { + dfds[i].resolve(); + } + + await dfds[LIMIT - 1].promise; + await timeout(10); + + expect(spy).toHaveBeenCalledTimes(LIMIT * 2); + + instance.pause(); + + for (let i = LIMIT, len = LIMIT * 3; i < len; i++) { + dfds[i].resolve(); + } + + await dfds[LIMIT * 2 - 1].promise; + await timeout(10); + + expect(spy).toHaveBeenCalledTimes(LIMIT * 2); + + instance.resume(); + + await timeout(10); + + expect(spy).toHaveBeenCalledTimes(LIMIT * 4); + }); + + }); + + describe(".clear()", () => { + + test("should remove all tasks from queue", async () => { + const dfds = createDeferreds(LIST) + const spy = jest.fn(n => dfds[n].promise); + + const instance = new Queue(spy, LIMIT); + instance.push(LIST); + + await timeout(10); + + expect(spy).toHaveBeenCalledTimes(LIMIT); + + for (let i = 0, len = LIMIT; i < len; i++) { + dfds[i].resolve(); + } + + await dfds[LIMIT - 1].promise; + await timeout(10); + + expect(spy).toHaveBeenCalledTimes(LIMIT * 2); + + instance.clear(); + + for (let i = LIMIT, len = LIMIT * 3; i < len; i++) { + dfds[i].resolve(); + } + + await dfds[LIMIT * 2 - 1].promise; + await timeout(10); + + expect(spy).toHaveBeenCalledTimes(LIMIT * 2); + + expect(instance).toHaveProperty("length", 0); + + }); + + }); + + describe(".length", () => { + + test("should returns items length", () => { + const dfds = createDeferreds(LIST); + const spy = jest.fn(n => dfds[n].promise); + + const instance = new Queue(spy, LIMIT); + + expect(instance).toHaveProperty("length", 0); + + instance.push(LIST); + + expect(instance).toHaveProperty("length", TOTAL); + + instance.push(LIST); + + expect(instance).toHaveProperty("length", TOTAL * 2); + }); + + }); + + describe(".running", () => { + + test("should returns true if some workers are running", async () => { + const dfds = createDeferreds(LIST); + const spy = jest.fn(n => dfds[n].promise); + + const instance = new Queue(spy, LIMIT); + + expect(instance).toHaveProperty("running", false); + + const promise = instance.push(LIST); + + expect(instance).toHaveProperty("running", true); + + for (let i = 0; i < TOTAL; i++) { + dfds[i].resolve(); + } + + await promise; + + expect(instance).toHaveProperty("running", false); + }); + + }); + + describe(".idle", () => { + + test("should returns true when empty", async () => { + const dfds = createDeferreds(LIST); + const spy = jest.fn(n => dfds[n].promise); + + const instance = new Queue(spy, LIMIT); + + expect(instance).toHaveProperty("idle", true); + + const promise = instance.push(LIST); + + expect(instance).toHaveProperty("idle", false); + + for (let i = 0; i < TOTAL; i++) { + dfds[i].resolve(); + } + + await promise; + + expect(instance).toHaveProperty("idle", true); + }); + + }); + + describe("#process()", () => { + + test("should only launch specified limit number of task simultaneously", async () => { + const spy = jest.fn(() => timeout(20)); + + const instance = new Queue(spy, LIMIT); + instance.push(LIST); + + await timeout(10); + + expect(spy).toHaveBeenCalledTimes(LIMIT); + }); + + test("should continue to limit number of task simultaneously when promises are resolved", async () => { + const dfds = createDeferreds(LIST) + const spy = jest.fn(n => dfds[n].promise); + + const instance = new Queue(spy, LIMIT); + instance.push(LIST); + + expect(spy).not.toHaveBeenCalled(); + + await timeout(10); + + expect(spy).toHaveBeenCalledTimes(LIMIT); + + for (let i = 0, len = LIMIT; i < len; i++) { + dfds[i].resolve(); + } + + await dfds[LIMIT - 1].promise; + await timeout(10); + + expect(spy).toHaveBeenCalledTimes(LIMIT * 2); + + for (let i = LIMIT, len = LIMIT * 3; i < len; i++) { + dfds[i].resolve(); + } + + await dfds[LIMIT * 2 - 1].promise; + await timeout(10); + + expect(spy).toHaveBeenCalledTimes(LIMIT * 4); + }); + + test("should continue to execute works even if exception occurred", async () => { + const err = new Error("test"); + + const spy = jest.fn(n => { + return timeout(n).then(() => { + throw err; + }); + }); + + const instance = new Queue(spy, LIMIT); + await expect(instance.push(LIST)) + .rejects.toBe(err); + + expect(spy).toHaveBeenCalledTimes(LIMIT); + + await timeout(100); + + expect(spy).toHaveBeenCalledTimes(TOTAL); + }); + + test("should not continue to execute works if exception occurred and stopOnError is set to true", async () => { + const err = new Error("This is an error !"); + const dfds = createDeferreds(LIST) + const spy = jest.fn(n => dfds[n].promise); + + const instance = new Queue(spy, LIMIT, { stopOnError: true }); + const promise = instance.push(LIST); + + expect(spy).not.toHaveBeenCalled(); + + await timeout(10); + + expect(spy).toHaveBeenCalledTimes(LIMIT); + + for (let i = 0, len = LIMIT; i < len; i++) { + dfds[i].reject(err); + } + + await expect(promise) + .rejects.toBe(err); + + expect(spy).toHaveBeenCalledTimes(LIMIT); + }); + + }); + +}); diff --git a/tests/QueueError.spec.ts b/tests/QueueError.spec.ts new file mode 100644 index 0000000..51bd148 --- /dev/null +++ b/tests/QueueError.spec.ts @@ -0,0 +1,11 @@ +import QueueError from "../lib/QueueError"; + +describe("promizr.QueueError", () => { + + test("should correctly extends Error", () => { + const err = new QueueError([], []); + expect(err).toBeInstanceOf(QueueError); + expect(err).toBeInstanceOf(Error); + }); + +}); diff --git a/tests/TaskQueue.spec.ts b/tests/TaskQueue.spec.ts new file mode 100644 index 0000000..60a4f14 --- /dev/null +++ b/tests/TaskQueue.spec.ts @@ -0,0 +1,32 @@ +import TaskQueue from "../lib/TaskQueue"; +import Queue from "../lib/Queue"; + +import timeout from "./helpers/timeout"; + +describe("promizr.TaskQueue", () => { + + test("should extends Queue", () => { + const queue = new TaskQueue(3); + expect(queue).toBeInstanceOf(TaskQueue); + expect(queue).toBeInstanceOf(Queue); + }); + + test("should call the task passed as items", async () => { + const queue = new TaskQueue(3); + + const spies = [ + jest.fn(() => timeout(5)), + jest.fn(() => timeout(1)), + jest.fn(() => timeout(10)), + jest.fn(() => timeout(8)), + jest.fn(() => timeout(15)) + ]; + + await queue.push(spies); + + for (const spy of spies) { + expect(spy).toHaveBeenCalledTimes(1); + } + }); + +}); diff --git a/tests/apply.spec.ts b/tests/apply.spec.ts new file mode 100644 index 0000000..b02d572 --- /dev/null +++ b/tests/apply.spec.ts @@ -0,0 +1,67 @@ +import apply from "../lib/apply"; + +describe("promizr.apply()", () => { + + test("should create a function which call task with given arguments", async () => { + const spy = jest.fn((arg: string, test: boolean) => `${arg}-${test}`); + const fn = apply(spy, "arg", true); + + expect(spy).not.toHaveBeenCalled(); + + await fn(); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith("arg", true); + }); + + test("should resolve with the task result if sync", async () => { + const spy = jest.fn(() => "result"); + + const promise = apply(spy)(); + + expect(spy).toHaveBeenCalledTimes(1); + expect(promise).toEqual(expect.any(Promise)); + + await expect(promise) + .resolves.toBe("result"); + }); + + test("should resolve with the task resolution if async", async () => { + const spy = jest.fn(async () => "result"); + + const promise = apply(spy)(); + + expect(spy).toHaveBeenCalledTimes(1); + expect(promise).toEqual(expect.any(Promise)); + + await expect(promise) + .resolves.toBe("result"); + }); + + test("should reject Promise if task throws", async () => { + const err = new Error("test"); + const spy = jest.fn(() => { throw err; }); + + const promise = apply(spy)(); + + expect(spy).toHaveBeenCalledTimes(1); + expect(promise).toEqual(expect.any(Promise)); + + await expect(promise) + .rejects.toBe(err); + }); + + test("should reject Promise if task rejects", async () => { + const err = new Error("test"); + const spy = jest.fn(async () => { throw err; }); + + const promise = apply(spy)(); + + expect(spy).toHaveBeenCalledTimes(1); + expect(promise).toEqual(expect.any(Promise)); + + await expect(promise) + .rejects.toBe(err); + }); + +}); diff --git a/tests/applyEach.spec.ts b/tests/applyEach.spec.ts new file mode 100644 index 0000000..76d748e --- /dev/null +++ b/tests/applyEach.spec.ts @@ -0,0 +1,84 @@ +import applyEach from "../lib/applyEach"; + +import timeout from "./helpers/timeout"; +import sort from "./helpers/sort"; + +const LIST = [15, 1, 8]; +const STOP_LIST = [251, 1, 50]; + +describe("promizr.applyEach()", () => { + + test("should return a promised function which should transfer its arguments to all tasks", async () => { + const spy = jest.fn((...args) => timeout(1)); + const executors = [spy, spy, spy]; + const owner = {}; + + const fn = applyEach(executors); + + expect(spy).not.toHaveBeenCalled(); + + await fn.apply(owner, LIST); + + expect(spy).toHaveBeenCalledTimes(3); + + expect(spy).toHaveBeenNthCalledWith(1, LIST[0], LIST[1], LIST[2]); + expect(spy).toHaveBeenNthCalledWith(2, LIST[0], LIST[1], LIST[2]); + expect(spy).toHaveBeenNthCalledWith(3, LIST[0], LIST[1], LIST[2]); + // sinon.assert.alwaysCalledOn(spy, owner); + + }); + + test("should call each provided methods with given arguments in parallel", async () => { + const spy = jest.fn(); + const executors = LIST.map(num => (arg: number[]) => timeout(num).then(() => spy(num, arg))); + + await applyEach(executors)(LIST); + expect(spy).toHaveBeenCalledTimes(3); + + const ordered = sort(LIST); + for (let i = 0; i < LIST.length; i++) { + expect(spy).toHaveBeenNthCalledWith(i + 1, ordered[i], LIST); + } + }); + + test("should return an array with each values returned by executors", async () => { + const spy = jest.fn(num => num * num), + executors = LIST.map(num => () => timeout(num).then(() => spy(num))); + + const results = await applyEach(executors)(); + expect(spy).toHaveBeenCalledTimes(3); + + expect(results).toHaveLength(3); + expect(results[0]).toBe(LIST[0] * LIST[0]); + expect(results[1]).toBe(LIST[1] * LIST[1]); + expect(results[2]).toBe(LIST[2] * LIST[2]); + + }); + + test("should stop if an executor throws", async () => { + const spy = jest.fn(); + const err = new Error("test"); + + const executors = STOP_LIST.map(num => jest.fn(() => { + return timeout(num).then(() => { + if (num === STOP_LIST[2]) { + throw err; + } + + spy(num); + }); + })); + + await expect(applyEach(executors)()) + .rejects.toBe(err); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(STOP_LIST[1]); + + expect(executors[0]).toHaveBeenCalledTimes(1); + expect(executors[1]).toHaveBeenCalledTimes(1); + expect(executors[2]).toHaveBeenCalledTimes(1); + + }); + +}); diff --git a/tests/applyEachSeries.spec.ts b/tests/applyEachSeries.spec.ts new file mode 100644 index 0000000..6107d42 --- /dev/null +++ b/tests/applyEachSeries.spec.ts @@ -0,0 +1,81 @@ +import applyEachSeries from "../lib/applyEachSeries"; + +import timeout from "./helpers/timeout"; + +const LIST = [15, 1, 8]; +const STOP_LIST = [251, 1, 50]; + +describe("promizr.applyEachSeries()", () => { + + test("should return a promised function which should transfer its arguments to all tasks", async () => { + const spy = jest.fn((...args) => timeout(1)); + const executors = [spy, spy, spy]; + const owner = {}; + + const fn = applyEachSeries(executors); + + expect(spy).not.toHaveBeenCalled(); + + await fn.apply(owner, LIST); + + expect(spy).toHaveBeenCalledTimes(3); + + expect(spy).toHaveBeenNthCalledWith(1, LIST[0], LIST[1], LIST[2]); + expect(spy).toHaveBeenNthCalledWith(2, LIST[0], LIST[1], LIST[2]); + expect(spy).toHaveBeenNthCalledWith(3, LIST[0], LIST[1], LIST[2]); + // sinon.assert.alwaysCalledOn(spy, owner); + }); + + test("should call each provided methods with given arguments in series", async () => { + const spy = jest.fn(); + const executors = LIST.map(num => (arg: number[]) => timeout(num).then(() => spy(num, arg))); + + await applyEachSeries(executors)(LIST); + expect(spy).toHaveBeenCalledTimes(3); + + for (let i = 0; i < LIST.length; i++) { + expect(spy).toHaveBeenNthCalledWith(i + 1, LIST[i], LIST); + } + }); + + test("should return an array with each values returned by executors", async () => { + const spy = jest.fn(num => num * num), + executors = LIST.map(num => () => timeout(num).then(() => spy(num))); + + const results = await applyEachSeries(executors)(); + expect(spy).toHaveBeenCalledTimes(3); + + expect(results).toHaveLength(3); + expect(results[0]).toBe(LIST[0] * LIST[0]); + expect(results[1]).toBe(LIST[1] * LIST[1]); + expect(results[2]).toBe(LIST[2] * LIST[2]); + + }); + + test("should stop if an executor throws", async () => { + const spy = jest.fn(); + const err = new Error("test"); + + const executors = STOP_LIST.map(num => jest.fn(() => { + return timeout(num).then(() => { + if (num === STOP_LIST[1]) { + throw err; + } + + spy(num); + }); + })); + + await expect(applyEachSeries(executors)()) + .rejects.toBe(err); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(STOP_LIST[0]); + + expect(executors[0]).toHaveBeenCalledTimes(1); + expect(executors[1]).toHaveBeenCalledTimes(1); + expect(executors[2]).not.toHaveBeenCalled(); + + }); + +}); diff --git a/tests/applyOn.spec.ts b/tests/applyOn.spec.ts new file mode 100644 index 0000000..06566d8 --- /dev/null +++ b/tests/applyOn.spec.ts @@ -0,0 +1,79 @@ +import applyOn from "../lib/applyOn"; + +describe("promizr.applyOn()", () => { + const owner = {}; + + test("should create a function which call task with given arguments", async () => { + const spy = jest.fn((arg: string, test: boolean) => `${arg}-${test}`); + const fn = applyOn(owner, spy, "arg", true); + + expect(spy).not.toHaveBeenCalled(); + + await fn(); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith("arg", true); + }); + + test("should use owner as this context when calling task", async () => { + const spy = jest.fn(function (this: unknown, arg: string, test: boolean): string { + expect(this).toBe(owner); + return `${arg}-${test}`; + }); + + await applyOn(owner, spy, "arg", true)(); + + expect(spy).toHaveBeenCalledTimes(1); + }); + + test("should resolve with the task result if sync", async () => { + const spy = jest.fn(() => "result"); + + const promise = applyOn(owner, spy)(); + + expect(spy).toHaveBeenCalledTimes(1); + expect(promise).toEqual(expect.any(Promise)); + + await expect(promise) + .resolves.toBe("result"); + }); + + test("should resolve with the task resolution if async", async () => { + const spy = jest.fn(async () => "result"); + + const promise = applyOn(owner, spy)(); + + expect(spy).toHaveBeenCalledTimes(1); + expect(promise).toEqual(expect.any(Promise)); + + await expect(promise) + .resolves.toBe("result"); + }); + + test("should reject Promise if task throws", async () => { + const err = new Error("test"); + const spy = jest.fn(() => { throw err; }); + + const promise = applyOn(owner, spy)(); + + expect(spy).toHaveBeenCalledTimes(1); + expect(promise).toEqual(expect.any(Promise)); + + await expect(promise) + .rejects.toBe(err); + }); + + test("should reject Promise if task rejects", async () => { + const err = new Error("test"); + const spy = jest.fn(async () => { throw err; }); + + const promise = applyOn(owner, spy)(); + + expect(spy).toHaveBeenCalledTimes(1); + expect(promise).toEqual(expect.any(Promise)); + + await expect(promise) + .rejects.toBe(err); + }); + +}); diff --git a/tests/cbpromisify.spec.ts b/tests/cbpromisify.spec.ts new file mode 100644 index 0000000..d3a9a59 --- /dev/null +++ b/tests/cbpromisify.spec.ts @@ -0,0 +1,115 @@ +import cbpromisify from "../lib/cbpromisify"; + +describe("promizr.cbpromisify()", () => { + + describe("with no owner", () => { + + test("should return a function that calls the inner function with the generated callbacks", async () => { + const spy = jest.fn(multiCallbackFunction); + + const fn = cbpromisify(spy); + + expect(spy).not.toHaveBeenCalled(); + + await fn("result", false, null); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith("result", false, null, expect.any(Function), expect.any(Function)); + }); + + test("should return the result of the callback function", async () => { + const res = await cbpromisify(multiCallbackFunction)("result", false, null); + expect(res).toBe("result"); + }); + + test("should return the result of the callback function", async () => { + const res = await cbpromisify(multiCallbackFunction)("result", false, null); + expect(res).toBe("result"); + }); + + test("should return an array of the callback function results if it has multiple results", async () => { + const res = await cbpromisify(multiCallbackFunction)("result", true, null); + expect(res).toEqual(["result", true]); + }); + + test("should return void if the callback function has no result", async () => { + const res = await cbpromisify(multiCallbackFunction)(undefined, true, null); + expect(res).toBeUndefined(); + }); + + test("should throw if the callback function returns an error", async () => { + const err = new Error("test"); + + await expect(cbpromisify(multiCallbackFunction)("result", false, err)) + .rejects.toBe(err); + }); + }); + + describe("with owner", () => { + const owner = {}; + + const thisMultiCallbackFunction = function (this: unknown, ...args: Parameters): void { + expect(this).toBe(owner); + multiCallbackFunction(...args); + }; + + test("should return a function that calls the inner function with the generated callbacks", async () => { + const spy = jest.fn(thisMultiCallbackFunction); + + const fn = cbpromisify(owner, spy); + + expect(spy).not.toHaveBeenCalled(); + + await fn("result", false, null); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith("result", false, null, expect.any(Function), expect.any(Function)); + }); + + test("should return the result of the callback function", async () => { + const res = await cbpromisify(owner, thisMultiCallbackFunction)("result", false, null); + expect(res).toBe("result"); + }); + + test("should return the result of the callback function", async () => { + const res = await cbpromisify(owner, thisMultiCallbackFunction)("result", false, null); + expect(res).toBe("result"); + }); + + test("should return an array of the callback function results if it has multiple results", async () => { + const res = await cbpromisify(owner, thisMultiCallbackFunction)("result", true, null); + expect(res).toEqual(["result", true]); + }); + + test("should return void if the callback function has no result", async () => { + const res = await cbpromisify(owner, thisMultiCallbackFunction)(undefined, true, null); + expect(res).toBeUndefined(); + }); + + test("should throw if the callback function returns an error", async () => { + const err = new Error("test"); + + await expect(cbpromisify(owner, thisMultiCallbackFunction)("result", false, err)) + .rejects.toBe(err); + }); + }); + +}); + +function multiCallbackFunction(res: string | undefined, multi: boolean | null | undefined, throws: Error | null | undefined, successCallback: (...args: any[]) => void, errorCallback: (err: Error) => void): void { + if (throws) { + setImmediate(() => errorCallback(throws)); + return; + } + + if (typeof res === "undefined") { + setImmediate(() => successCallback()); + } + + if (multi) { + setImmediate(() => successCallback(res, true)); + return; + } + + setImmediate(() => successCallback(res)); +} diff --git a/tests/compose.spec.ts b/tests/compose.spec.ts new file mode 100644 index 0000000..f14824d --- /dev/null +++ b/tests/compose.spec.ts @@ -0,0 +1,78 @@ +import compose from "../lib/compose"; + +import timeout from "./helpers/timeout"; + +const LIST = [15, 1, 8]; + +describe("promizr.compose()", () => { + + test("should return a promised function which should transfer its arguments and owner to the last given task", async () => { + const task = jest.fn((...args: number[]) => args.reduce((a, b) => a + b, 0)); + const owner = {}; + + const fn = compose(task); + + expect(task).not.toHaveBeenCalled(); + + await fn.apply(owner, LIST); + + expect(task).toHaveBeenCalledTimes(1); + expect(task).toHaveBeenCalledWith(LIST[0], LIST[1], LIST[2]); + // sinon.assert.calledOn(task, owner); + }); + + describe("when result called", () => { + + test("should call each task with previous task result as argument", async () => { + const spy = jest.fn((a, b) => a + b); + const executors = LIST.map(a => (b: number) => timeout(a).then(() => spy(a, b))); + + await compose(...executors)(0); + + expect(spy).toHaveBeenCalledTimes(3); + + expect(spy).toHaveBeenNthCalledWith(1, LIST[2], 0); + expect(spy).toHaveBeenNthCalledWith(2, LIST[1], LIST[2]); + expect(spy).toHaveBeenNthCalledWith(3, LIST[0], LIST[2] + LIST[1]); + }); + + test("should return the result of the first given task", async () => { + const executors = LIST.map(a => (b: number) => timeout(a).then(() => a + b)); + + const result = await compose(...executors)(0); + expect(result).toBe(LIST[0] + LIST[1] + LIST[2]); + }); + + test("should resolved with void if empty array is passed", async () => { + const res = await compose()(); + expect(res).toBeUndefined(); + }); + + test("should stop if an executor throws", async () => { + const spy = jest.fn(); + const err = new Error("test"); + + let i = 0; + + const executors = LIST.map(num => jest.fn(() => { + if (i++ === 1) { + throw err; + } + + return timeout(num).then(() => spy(num)); + })); + + await expect(compose(...executors)()) + .rejects.toBe(err); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(LIST[2]); + + expect(executors[2]).toHaveBeenCalledTimes(1); + expect(executors[1]).toHaveBeenCalledTimes(1); + expect(executors[0]).not.toHaveBeenCalled() + }); + + }); + +}); diff --git a/tests/concat.spec.ts b/tests/concat.spec.ts new file mode 100644 index 0000000..a4e9be6 --- /dev/null +++ b/tests/concat.spec.ts @@ -0,0 +1,60 @@ +import concat from "../lib/concat"; + +import timeout from "./helpers/timeout"; +import sort from "./helpers/sort"; + +const LIST = [15, 1, 8]; +const STOP_LIST = [251, 1, 50]; + +describe("promizr.concat()", () => { + + test("should call each provided methods with given values in parallel", async () => { + const spy = jest.fn((num, i, l) => [num]); + + await concat(LIST, (num, i, list) => timeout(num).then(() => spy(num, i, list))); + + expect(spy).toHaveBeenCalledTimes(3); + + const ordered = sort(LIST); + for (let i = 0; i < LIST.length; i++) { + expect(spy).toHaveBeenNthCalledWith(i + 1, ordered[i], LIST.indexOf(ordered[i]), LIST); + } + }); + + test("should return flattened array with arrays returned by iterator", async () => { + const spy = jest.fn(num => [num]); + + const results = await concat(LIST, num => timeout(num).then(() => spy(num))); + expect(spy).toHaveBeenCalledTimes(3); + + expect(results).toHaveLength(3); + expect(results).toEqual(LIST); + }); + + test("should stop if an iterator throws", async () => { + const spy = jest.fn(num => [num]); + const err = new Error("test"); + + const iterator = jest.fn(num => { + return timeout(num).then(() => { + if (num === STOP_LIST[2]) { + throw err; + } + + return spy(num); + }); + }); + + await expect(concat(STOP_LIST, iterator)) + .rejects.toBe(err); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(STOP_LIST[1]); + + expect(iterator).toHaveBeenCalledTimes(3); + expect(iterator).toHaveBeenNthCalledWith(1, STOP_LIST[0], 0, STOP_LIST); + expect(iterator).toHaveBeenNthCalledWith(2, STOP_LIST[1], 1, STOP_LIST); + expect(iterator).toHaveBeenNthCalledWith(3, STOP_LIST[2], 2, STOP_LIST); + }); + +}); diff --git a/tests/concatSeries.spec.ts b/tests/concatSeries.spec.ts new file mode 100644 index 0000000..d0a5d45 --- /dev/null +++ b/tests/concatSeries.spec.ts @@ -0,0 +1,58 @@ +import concatSeries from "../lib/concatSeries"; + +import timeout from "./helpers/timeout"; +import sort from "./helpers/sort"; + +const LIST = [15, 1, 8]; +const STOP_LIST = [251, 1, 50]; + +describe("promizr.concatSeries()", () => { + + test("should call each provided methods with given values in parallel", async () => { + const spy = jest.fn((num, i, l) => [num]); + + await concatSeries(LIST, (num, i, list) => timeout(num).then(() => spy(num, i, list))); + + expect(spy).toHaveBeenCalledTimes(3); + + for (let i = 0; i < LIST.length; i++) { + expect(spy).toHaveBeenNthCalledWith(i + 1, LIST[i], i, LIST); + } + }); + + test("should return flattened array with arrays returned by iterator", async () => { + const spy = jest.fn(num => [num]); + + const results = await concatSeries(LIST, num => timeout(num).then(() => spy(num))); + expect(spy).toHaveBeenCalledTimes(3); + + expect(results).toHaveLength(3); + expect(results).toEqual(LIST); + }); + + test("should stop if an iterator throws", async () => { + const spy = jest.fn(num => [num]); + const err = new Error("test"); + + const iterator = jest.fn(num => { + return timeout(num).then(() => { + if (num === STOP_LIST[1]) { + throw err; + } + + return spy(num); + }); + }); + + await expect(concatSeries(STOP_LIST, iterator)) + .rejects.toBe(err); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(STOP_LIST[0]); + + expect(iterator).toHaveBeenCalledTimes(2); + expect(iterator).toHaveBeenNthCalledWith(1, STOP_LIST[0], 0, STOP_LIST); + expect(iterator).toHaveBeenNthCalledWith(2, STOP_LIST[1], 1, STOP_LIST); + }); + +}); diff --git a/tests/defer.spec.ts b/tests/defer.spec.ts new file mode 100644 index 0000000..dbe6a34 --- /dev/null +++ b/tests/defer.spec.ts @@ -0,0 +1,46 @@ +import defer from "../lib/defer"; + +describe("promizr.defer()", () => { + + test("should return a Deferred object", () => { + const dfd = defer(); + + expect(dfd).toHaveProperty("resolve", expect.any(Function)); + expect(dfd).toHaveProperty("reject", expect.any(Function)); + expect(dfd).toHaveProperty("promise", expect.any(Promise)); + }); + + test("should resolve the Promise when resolve function is called", async () => { + const spy = jest.fn(); + const dfd = defer(); + + const promise = dfd.promise.then(spy); + + expect(spy).not.toHaveBeenCalled(); + + dfd.resolve("value"); + + await promise; + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith("value"); + }); + + test("should reject the Promise when reject function is called", async () => { + const spy = jest.fn(); + const dfd = defer(); + + const promise = dfd.promise.catch(spy); + + expect(spy).not.toHaveBeenCalled(); + + const err = new Error("test"); + dfd.reject(err); + + await promise; + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(err); + }); + +}); diff --git a/tests/denodify.spec.ts b/tests/denodify.spec.ts new file mode 100644 index 0000000..2cf8db5 --- /dev/null +++ b/tests/denodify.spec.ts @@ -0,0 +1,107 @@ +import denodify from "../lib/denodify"; + +describe("promizr.denodify()", () => { + + describe("with no owner", () => { + + test("should call the inner function with a generated callback", async () => { + const spy = jest.fn(nodeStyleFunction); + + await denodify(spy, "result", false, null); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith("result", false, null, expect.any(Function)); + }); + + test("should return the result of the callback function", async () => { + const res = await denodify(nodeStyleFunction, "result", false, null); + expect(res).toBe("result"); + }); + + test("should return the result of the callback function", async () => { + const res = await denodify(nodeStyleFunction, "result", false, null); + expect(res).toBe("result"); + }); + + test("should return an array of the callback function results if it has multiple results", async () => { + const res = await denodify(nodeStyleFunction, "result", true, null); + expect(res).toEqual(["result", true]); + }); + + test("should return void if the callback function has no result", async () => { + const res = await denodify(nodeStyleFunction, undefined, true, null); + expect(res).toBeUndefined(); + }); + + test("should throw if the callback function returns an error", async () => { + const err = new Error("test"); + + await expect(denodify(nodeStyleFunction, "result", false, err)) + .rejects.toBe(err); + }); + }); + + describe("with owner", () => { + const owner = {}; + + const thisNodeStyleFunction = function (this: unknown, ...args: Parameters): void { + expect(this).toBe(owner); + nodeStyleFunction(...args); + }; + + test("should return a function that calls the inner function with a generated callback", async () => { + const spy = jest.fn(thisNodeStyleFunction); + + await denodify(owner, spy, "result", false, null); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith("result", false, null, expect.any(Function)); + }); + + test("should return the result of the callback function", async () => { + const res = await denodify(owner, thisNodeStyleFunction, "result", false, null); + expect(res).toBe("result"); + }); + + test("should return the result of the callback function", async () => { + const res = await denodify(owner, thisNodeStyleFunction, "result", false, null); + expect(res).toBe("result"); + }); + + test("should return an array of the callback function results if it has multiple results", async () => { + const res = await denodify(owner, thisNodeStyleFunction, "result", true, null); + expect(res).toEqual(["result", true]); + }); + + test("should return void if the callback function has no result", async () => { + const res = await denodify(owner, thisNodeStyleFunction, undefined, true, null); + expect(res).toBeUndefined(); + }); + + test("should throw if the callback function returns an error", async () => { + const err = new Error("test"); + + await expect(denodify(owner, thisNodeStyleFunction, "result", false, err)) + .rejects.toBe(err); + }); + }); + +}); + +function nodeStyleFunction(res: string | undefined, multi: boolean | null | undefined, throws: Error | null | undefined, cb: (err?: Error | null, ...args: any[]) => void): void { + if (throws) { + setImmediate(() => cb(throws)); + return; + } + + if (typeof res === "undefined") { + setImmediate(() => cb()); + } + + if (multi) { + setImmediate(() => cb(null, res, true)); + return; + } + + setImmediate(() => cb(null, res)); +} diff --git a/tests/dir.spec.ts b/tests/dir.spec.ts new file mode 100644 index 0000000..18fbc8f --- /dev/null +++ b/tests/dir.spec.ts @@ -0,0 +1,63 @@ +import dir from "../lib/dir"; + +import timeout from "./helpers/timeout"; + +type Spy any> = jest.SpyInstance, Parameters>; + +describe("promizr.dir()", () => { + let consoleDirSpy: Spy; + let consoleErrorSpy: Spy; + beforeEach(() => { + consoleDirSpy = jest.spyOn(console, "dir"); + consoleDirSpy.mockReturnValue(); + + consoleErrorSpy = jest.spyOn(console, "error"); + consoleErrorSpy.mockReturnValue(); + }); + afterEach(() => { + consoleDirSpy.mockRestore(); + consoleErrorSpy.mockRestore(); + }); + + test("should call the given task with passed arguments", async () => { + const task = jest.fn((...args) => timeout(10).then(() => "result")); + + await dir(task, "arg1", true); + + expect(task).toHaveBeenCalledTimes(1); + expect(task).toHaveBeenCalledWith("arg1", true); + }); + + test("should return the result of the task", async () => { + const task = jest.fn((...args) => timeout(10).then(() => "result")); + + const res = await dir(task, "arg1", true); + + expect(res).toBe("result"); + }); + + test("should call console.dir with the result of the task if it succeeds", async () => { + await dir((...args) => timeout(10).then(() => "result"), "arg1", true); + + expect(consoleDirSpy).toHaveBeenCalledTimes(1); + expect(consoleDirSpy).toHaveBeenCalledWith("result"); + }); + + test("should throw with the Error of the task if it throws", async () => { + const err = new Error("test"); + + await expect(dir((...args) => timeout(10).then(() => { throw err; }), "arg1", true)) + .rejects.toBe(err); + }); + + test("should call console.error with the result of the task if it throws", async () => { + const err = new Error("test"); + + await expect(dir((...args) => timeout(10).then(() => { throw err; }), "arg1", true)) + .rejects.toBe(err); + + expect(consoleErrorSpy).toHaveBeenCalledTimes(1); + expect(consoleErrorSpy).toHaveBeenCalledWith(err); + }); + +}); diff --git a/tests/doUntil.spec.ts b/tests/doUntil.spec.ts new file mode 100644 index 0000000..742f0b2 --- /dev/null +++ b/tests/doUntil.spec.ts @@ -0,0 +1,115 @@ +import doUntil from "../lib/doUntil"; + +import timeout from "./helpers/timeout"; + +const LIST = [15, 1, 8]; + +describe("promizr.doUntil()", () => { + + test("should call provided method while given test pass", async () => { + const spy = jest.fn(num => num); + let i = 0; + + const test = jest.fn(res => res === LIST[1]); + const executor = () => { + const num = LIST[i++]; + return timeout(num).then(() => spy(num)); + }; + + await doUntil(executor, test); + + expect(spy).toHaveBeenCalledTimes(2); + expect(test).toHaveBeenCalledTimes(2); + + expect(spy).toHaveBeenNthCalledWith(1, LIST[0]); + expect(spy).toHaveBeenNthCalledWith(2, LIST[1]); + + }); + + test("should accept a Promise as test result", async () => { + let i = 0; + + const test = jest.fn(() => Promise.resolve(LIST[i] % 2 === 0)); + const executor = jest.fn(() => timeout(LIST[i++])); + + await doUntil(executor, test); + + expect(executor).toHaveBeenCalledTimes(2); + expect(test).toHaveBeenCalledTimes(2); + }); + + test("should pass the result of the task to the test function", async () => { + const spy = jest.fn(num => num); + let i = 0; + + const test = jest.fn(res => res === LIST[1]); + const executor = () => { + const num = LIST[i++]; + return timeout(num).then(() => spy(num)); + }; + + await doUntil(executor, test); + + expect(spy).toHaveBeenCalledTimes(2); + expect(test).toHaveBeenCalledTimes(2); + + expect(test).toHaveBeenNthCalledWith(1, LIST[0]); + expect(test).toHaveBeenNthCalledWith(2, LIST[1]); + }); + + test("should stop if an executor throws", async () => { + const spy = jest.fn(num => num); + const err = new Error("test"); + + let i = 0; + + const test = jest.fn(() => false); + const executor = jest.fn(() => { + const num = LIST[i++]; + if (num === LIST[1]) { + throw err; + } + + return timeout(num).then(() => spy(num)); + }); + + await expect(doUntil(executor, test)) + .rejects.toBe(err); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(LIST[0]); + + expect(test).toHaveBeenCalledTimes(1); + expect(executor).toHaveBeenCalledTimes(2); + }); + + test("should stop if a test throws", async () => { + const spy = jest.fn(num => num); + const err = new Error("test"); + + let i = 0; + + const test = jest.fn(res => { + if (res === LIST[1]) { + throw err; + } + return false; + }); + + const executor = jest.fn(() => { + const num = LIST[i++]; + return timeout(num).then(() => spy(num)); + }); + + await expect(doUntil(executor, test)) + .rejects.toBe(err); + + expect(spy).toHaveBeenCalledTimes(2); + expect(spy).toHaveBeenNthCalledWith(1, LIST[0]); + expect(spy).toHaveBeenNthCalledWith(2, LIST[1]); + + expect(test).toHaveBeenCalledTimes(2); + expect(executor).toHaveBeenCalledTimes(2); + }); + +}); diff --git a/tests/doWhilst.spec.ts b/tests/doWhilst.spec.ts new file mode 100644 index 0000000..ae4ce8e --- /dev/null +++ b/tests/doWhilst.spec.ts @@ -0,0 +1,115 @@ +import doWhilst from "../lib/doWhilst"; + +import timeout from "./helpers/timeout"; + +const LIST = [15, 1, 8]; + +describe("promizr.doWhilst()", () => { + + test("should call provided method while given test pass", async () => { + const spy = jest.fn(num => num); + let i = 0; + + const test = jest.fn(res => res !== LIST[1]); + const executor = () => { + const num = LIST[i++]; + return timeout(num).then(() => spy(num)); + }; + + await doWhilst(executor, test); + + expect(spy).toHaveBeenCalledTimes(2); + expect(test).toHaveBeenCalledTimes(2); + + expect(spy).toHaveBeenNthCalledWith(1, LIST[0]); + expect(spy).toHaveBeenNthCalledWith(2, LIST[1]); + + }); + + test("should accept a Promise as test result", async () => { + let i = 0; + + const test = jest.fn(() => Promise.resolve(LIST[i] % 2 !== 0)); + const executor = jest.fn(() => timeout(LIST[i++])); + + await doWhilst(executor, test); + + expect(executor).toHaveBeenCalledTimes(2); + expect(test).toHaveBeenCalledTimes(2); + }); + + test("should pass the result of the task to the test function", async () => { + const spy = jest.fn(num => num); + let i = 0; + + const test = jest.fn(res => res !== LIST[1]); + const executor = () => { + const num = LIST[i++]; + return timeout(num).then(() => spy(num)); + }; + + await doWhilst(executor, test); + + expect(spy).toHaveBeenCalledTimes(2); + expect(test).toHaveBeenCalledTimes(2); + + expect(test).toHaveBeenNthCalledWith(1, LIST[0]); + expect(test).toHaveBeenNthCalledWith(2, LIST[1]); + }); + + test("should stop if an executor throws", async () => { + const spy = jest.fn(num => num); + const err = new Error("test"); + + let i = 0; + + const test = jest.fn(() => true); + const executor = jest.fn(() => { + const num = LIST[i++]; + if (num === LIST[1]) { + throw err; + } + + return timeout(num).then(() => spy(num)); + }); + + await expect(doWhilst(executor, test)) + .rejects.toBe(err); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(LIST[0]); + + expect(test).toHaveBeenCalledTimes(1); + expect(executor).toHaveBeenCalledTimes(2); + }); + + test("should stop if a test throws", async () => { + const spy = jest.fn(num => num); + const err = new Error("test"); + + let i = 0; + + const test = jest.fn(res => { + if (res === LIST[1]) { + throw err; + } + return true; + }); + + const executor = jest.fn(() => { + const num = LIST[i++]; + return timeout(num).then(() => spy(num)); + }); + + await expect(doWhilst(executor, test)) + .rejects.toBe(err); + + expect(spy).toHaveBeenCalledTimes(2); + expect(spy).toHaveBeenNthCalledWith(1, LIST[0]); + expect(spy).toHaveBeenNthCalledWith(2, LIST[1]); + + expect(test).toHaveBeenCalledTimes(2); + expect(executor).toHaveBeenCalledTimes(2); + }); + +}); diff --git a/tests/each.spec.ts b/tests/each.spec.ts new file mode 100644 index 0000000..c4f1eeb --- /dev/null +++ b/tests/each.spec.ts @@ -0,0 +1,71 @@ +import each from "../lib/each"; + +import timeout from "./helpers/timeout"; +import sort from "./helpers/sort"; + +const LIST = [15, 1, 8]; +const STOP_LIST = [251, 1, 50]; + +describe("promizr.each()", () => { + + test("should returns void", async () => { + const spy = jest.fn(); + + const res = await each(LIST, spy); + + expect(res).toBeUndefined(); + }); + + test("should call each provided methods with given values in parallel", async () => { + const spy = jest.fn(); + + await each(LIST, (num, index, list) => timeout(num).then(() => spy(num, index, list))); + + expect(spy).toHaveBeenCalledTimes(LIST.length); + + const ordered = sort(LIST); + for (let i = 0; i < LIST.length; i++) { + expect(spy).toHaveBeenNthCalledWith(i + 1, ordered[i], LIST.indexOf(ordered[i]), LIST); + } + }); + + test("should reject if an iterator throws", async () => { + const spy = jest.fn(); + const err = new Error("test"); + + spy.mockResolvedValueOnce(undefined); + spy.mockRejectedValueOnce(err); + spy.mockResolvedValueOnce(undefined); + + await expect(each(LIST, spy)) + .rejects.toBe(err); + }); + + test("should stop if an iterator throws", async () => { + const spy = jest.fn(); + const err = new Error("test"); + + const iterator = jest.fn((num: number, i: number) => { + return timeout(num).then(() => { + if (i === 2) { + return Promise.reject(err); + } + + spy(num); + }); + }); + + await expect(each(STOP_LIST, iterator)) + .rejects.toBe(err); + + + expect(spy).toBeCalledTimes(1); + expect(spy).toBeCalledWith(STOP_LIST[1]); + + expect(iterator).toBeCalledTimes(3); + expect(iterator).toHaveBeenNthCalledWith(1, STOP_LIST[0], 0, STOP_LIST); + expect(iterator).toHaveBeenNthCalledWith(2, STOP_LIST[1], 1, STOP_LIST); + expect(iterator).toHaveBeenNthCalledWith(3, STOP_LIST[2], 2, STOP_LIST); + }); + +}); diff --git a/tests/eachLimit.spec.ts b/tests/eachLimit.spec.ts new file mode 100644 index 0000000..e5b4b9a --- /dev/null +++ b/tests/eachLimit.spec.ts @@ -0,0 +1,135 @@ +import eachLimit from "../lib/eachLimit"; +import QueueError from "../lib/QueueError"; +import createDeferreds from "./helpers/createDeferreds"; + +import timeout from "./helpers/timeout"; + +const LIST = [15, 1, 8, 25, 5]; +const LIMIT = 3; +const TOTAL = LIST.length; + +const BIG_LIST = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; + +describe("promizr.eachLimit()", () => { + + test("should returns a Promise which resolves when all tasks are done", async () => { + const spy = jest.fn((n, i, l) => timeout(n)); + + const promise = eachLimit(LIST, LIMIT, spy); + + expect(promise).toEqual(expect.any(Promise)); + + await expect(promise) + .resolves.toBeUndefined(); + + expect(spy).toHaveBeenCalledTimes(TOTAL); + + for (let i = 0; i < TOTAL; i++) { + expect(spy).toHaveBeenCalledWith(LIST[i], i, LIST); + } + }); + + test("should reject immediately if any item reject", async () => { + const err = new Error("test"); + const spy = jest.fn(() => { throw err; }); + + await expect(eachLimit(LIST, LIMIT, () => timeout(1).then(spy))) + .rejects.toBe(err); + + expect(spy).toHaveBeenCalledTimes(1); + }); + + test("should wait to reject QueueError if waitToReject is set to true", async () => { + const spy = jest.fn(n => n % 2 === 0 ? Promise.reject(new Error("test")) : Promise.resolve(n)); + + const iterator = (n: number) => timeout(1).then(() => spy(n)); + + await expect(eachLimit(LIST, LIMIT, iterator, { waitToReject: true })) + .rejects.toBeInstanceOf(QueueError); + + expect(spy).toHaveBeenCalledTimes(TOTAL); + }); + + test("should only launch specified limit number of task simultaneously", async () => { + const spy = jest.fn(() => timeout(20)); + + eachLimit(LIST, LIMIT, spy); + + await timeout(10); + + expect(spy).toHaveBeenCalledTimes(LIMIT); + }); + + test("should continue to limit number of task simultaneously when promises are resolved", async () => { + const dfds = createDeferreds(BIG_LIST) + const spy = jest.fn((n) => dfds[n].promise); + + eachLimit(BIG_LIST, LIMIT, spy); + + expect(spy).not.toHaveBeenCalled(); + + await timeout(10); + + expect(spy).toHaveBeenCalledTimes(LIMIT); + + for (let i = 0, len = LIMIT; i < len; i++) { + dfds[i].resolve(); + } + + await dfds[LIMIT - 1].promise; + await timeout(10); + + expect(spy).toHaveBeenCalledTimes(LIMIT * 2); + + for (let i = LIMIT, len = LIMIT * 3; i < len; i++) { + dfds[i].resolve(); + } + + await dfds[LIMIT * 2 - 1].promise; + await timeout(10); + + expect(spy).toHaveBeenCalledTimes(LIMIT * 4); + }); + + test("should not continue to execute works if exception occurred", async () => { + const err = new Error("This is an error !"); + const dfds = createDeferreds(BIG_LIST) + const spy = jest.fn(n => dfds[n].promise); + + const promise = eachLimit(BIG_LIST, LIMIT, spy); + + expect(spy).not.toHaveBeenCalled(); + + await timeout(10); + + expect(spy).toHaveBeenCalledTimes(LIMIT); + + for (let i = 0, len = LIMIT; i < len; i++) { + dfds[i].reject(err); + } + + await expect(promise) + .rejects.toBe(err); + + expect(spy).toHaveBeenCalledTimes(LIMIT); + }); + + test("should continue to execute works even if exception occurred and stopOnError is false", async () => { + const err = new Error("test"); + const spy = jest.fn(n => { + return timeout(n).then(() => { + throw err; + }); + }); + + await expect(eachLimit(BIG_LIST, LIMIT, spy, { stopOnError: false })) + .rejects.toBe(err); + + expect(spy).toHaveBeenCalledTimes(LIMIT); + + await timeout(100); + + expect(spy).toHaveBeenCalledTimes(BIG_LIST.length); + }); + +}); diff --git a/tests/eachSeries.spec.ts b/tests/eachSeries.spec.ts new file mode 100644 index 0000000..a67b218 --- /dev/null +++ b/tests/eachSeries.spec.ts @@ -0,0 +1,69 @@ +import eachSeries from "../lib/eachSeries"; + +import timeout from "./helpers/timeout"; +import sort from "./helpers/sort"; + +const LIST = [15, 1, 8]; +const STOP_LIST = [251, 1, 50]; + +describe("promizr.eachSeries()", () => { + + test("should returns void", async () => { + const spy = jest.fn(); + + const res = await eachSeries(LIST, spy); + + expect(res).toBeUndefined(); + }); + + test("should call each provided methods with given values in series", async () => { + const spy = jest.fn(); + + await eachSeries(LIST, (num, index, list) => timeout(num).then(() => spy(num, index, list))); + + expect(spy).toHaveBeenCalledTimes(LIST.length); + + for (let i = 0; i < LIST.length; i++) { + expect(spy).toHaveBeenNthCalledWith(i + 1, LIST[i], i, LIST); + } + }); + + test("should reject if an iterator throws", async () => { + const spy = jest.fn(); + const err = new Error("test"); + + spy.mockResolvedValueOnce(undefined); + spy.mockRejectedValueOnce(err); + spy.mockResolvedValueOnce(undefined); + + await expect(eachSeries(LIST, spy)) + .rejects.toBe(err); + }); + + test("should stop if an iterator throws", async () => { + const spy = jest.fn(); + const err = new Error("test"); + + const iterator = jest.fn((num: number, i: number) => { + return timeout(num).then(() => { + if (i === 1) { + return Promise.reject(err); + } + + spy(num); + }); + }); + + await expect(eachSeries(STOP_LIST, iterator)) + .rejects.toBe(err); + + + expect(spy).toBeCalledTimes(1); + expect(spy).toHaveBeenCalledWith(STOP_LIST[0]); + + expect(iterator).toBeCalledTimes(2); + expect(iterator).toHaveBeenNthCalledWith(1, STOP_LIST[0], 0, STOP_LIST); + expect(iterator).toHaveBeenNthCalledWith(2, STOP_LIST[1], 1, STOP_LIST); + }); + +}); diff --git a/tests/every.spec.ts b/tests/every.spec.ts new file mode 100644 index 0000000..6d74359 --- /dev/null +++ b/tests/every.spec.ts @@ -0,0 +1,80 @@ +import every from "../lib/every"; + +import timeout from "./helpers/timeout"; +import sort from "./helpers/sort"; + +const LIST = [15, 1, 8]; +const STOP_LIST = [251, 1, 50]; + +describe("promizr.every()", () => { + + test("should call each provided methods with given values in parallel", async () => { + const spy = jest.fn((num, i, l) => true); + + await every(LIST, (num, i, list) => timeout(num).then(() => spy(num, i, list))); + expect(spy).toHaveBeenCalledTimes(3); + + const ordered = sort(LIST); + for (let i = 0; i < LIST.length; i++) { + expect(spy).toHaveBeenNthCalledWith(i + 1, ordered[i], LIST.indexOf(ordered[i]), LIST); + } + }); + + test("should return true if all items match given iterator", async () => { + const spy = jest.fn(num => true); + + const result = await every(LIST, num => timeout(num).then(() => spy(num))); + expect(spy).toHaveBeenCalledTimes(3); + expect(result).toBe(true); + }); + + test("should return false if any item does not match given iterator", async () => { + const spy = jest.fn(num => num % 2 === 0); + + const result = await every(LIST, num => timeout(num).then(() => spy(num))); + expect(spy).toHaveBeenCalledTimes(1); + expect(result).toBe(false); + }); + + test("should stop if an item does not match given iterator", async () => { + const spy = jest.fn(num => num % 2 === 0); + const iterator = jest.fn(num => timeout(num).then(() => spy(num))); + + await every(STOP_LIST, iterator); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(STOP_LIST[1]); + + expect(iterator).toHaveBeenCalledTimes(3); + expect(iterator).toHaveBeenNthCalledWith(1, STOP_LIST[0], 0, STOP_LIST); + expect(iterator).toHaveBeenNthCalledWith(2, STOP_LIST[1], 1, STOP_LIST); + expect(iterator).toHaveBeenNthCalledWith(3, STOP_LIST[2], 2, STOP_LIST); + }); + + test("should stop if an iterator throws", async () => { + const spy = jest.fn(num => true); + const err = new Error("test"); + + const iterator = jest.fn(num => { + return timeout(num).then(() => { + if (num === STOP_LIST[2]) { + return Promise.reject(err); + } + + return spy(num); + }); + }); + + await expect(every(STOP_LIST, iterator)) + .rejects.toBe(err); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(STOP_LIST[1]); + + expect(iterator).toHaveBeenCalledTimes(3); + expect(iterator).toHaveBeenNthCalledWith(1, STOP_LIST[0], 0, STOP_LIST); + expect(iterator).toHaveBeenNthCalledWith(2, STOP_LIST[1], 1, STOP_LIST); + expect(iterator).toHaveBeenNthCalledWith(3, STOP_LIST[2], 2, STOP_LIST); + }); + +}); diff --git a/tests/exec.spec.ts b/tests/exec.spec.ts new file mode 100644 index 0000000..3f0c0d4 --- /dev/null +++ b/tests/exec.spec.ts @@ -0,0 +1,64 @@ +import exec from "../lib/exec"; + +describe("promizr.exec()", () => { + + test("should exec task with given arguments", async () => { + const spy = jest.fn((arg: string, test: boolean) => `${arg}-${test}`); + + await exec(spy, "arg", true); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith("arg", true); + }); + + test("should resolve with the task result if sync", async () => { + const spy = jest.fn(() => "result"); + + const promise = exec(spy); + + expect(spy).toHaveBeenCalledTimes(1); + expect(promise).toEqual(expect.any(Promise)); + + await expect(promise) + .resolves.toBe("result"); + }); + + test("should resolve with the task resolution if async", async () => { + const spy = jest.fn(async () => "result"); + + const promise = exec(spy); + + expect(spy).toHaveBeenCalledTimes(1); + expect(promise).toEqual(expect.any(Promise)); + + await expect(promise) + .resolves.toBe("result"); + }); + + test("should reject Promise if task throws", async () => { + const err = new Error("test"); + const spy = jest.fn(() => { throw err; }); + + const promise = exec(spy); + + expect(spy).toHaveBeenCalledTimes(1); + expect(promise).toEqual(expect.any(Promise)); + + await expect(promise) + .rejects.toBe(err); + }); + + test("should reject Promise if task rejects", async () => { + const err = new Error("test"); + const spy = jest.fn(async () => { throw err; }); + + const promise = exec(spy); + + expect(spy).toHaveBeenCalledTimes(1); + expect(promise).toEqual(expect.any(Promise)); + + await expect(promise) + .rejects.toBe(err); + }); + +}); diff --git a/tests/execOn.spec.ts b/tests/execOn.spec.ts new file mode 100644 index 0000000..f6863f1 --- /dev/null +++ b/tests/execOn.spec.ts @@ -0,0 +1,76 @@ +import execOn from "../lib/execOn"; + +describe("promizr.execOn()", () => { + const owner = {}; + + test("should create a function which call task with given arguments", async () => { + const spy = jest.fn((arg: string, test: boolean) => `${arg}-${test}`); + + await execOn(owner, spy, "arg", true); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith("arg", true); + }); + + test("should use owner as this context when calling task", async () => { + const spy = jest.fn(function (this: unknown, arg: string, test: boolean): string { + expect(this).toBe(owner); + return `${arg}-${test}`; + }); + + await execOn(owner, spy, "arg", true); + + expect(spy).toHaveBeenCalledTimes(1); + }); + + test("should resolve with the task result if sync", async () => { + const spy = jest.fn(() => "result"); + + const promise = execOn(owner, spy); + + expect(spy).toHaveBeenCalledTimes(1); + expect(promise).toEqual(expect.any(Promise)); + + await expect(promise) + .resolves.toBe("result"); + }); + + test("should resolve with the task resolution if async", async () => { + const spy = jest.fn(async () => "result"); + + const promise = execOn(owner, spy); + + expect(spy).toHaveBeenCalledTimes(1); + expect(promise).toEqual(expect.any(Promise)); + + await expect(promise) + .resolves.toBe("result"); + }); + + test("should reject Promise if task throws", async () => { + const err = new Error("test"); + const spy = jest.fn(() => { throw err; }); + + const promise = execOn(owner, spy); + + expect(spy).toHaveBeenCalledTimes(1); + expect(promise).toEqual(expect.any(Promise)); + + await expect(promise) + .rejects.toBe(err); + }); + + test("should reject Promise if task rejects", async () => { + const err = new Error("test"); + const spy = jest.fn(async () => { throw err; }); + + const promise = execOn(owner, spy); + + expect(spy).toHaveBeenCalledTimes(1); + expect(promise).toEqual(expect.any(Promise)); + + await expect(promise) + .rejects.toBe(err); + }); + +}); diff --git a/tests/filter.spec.ts b/tests/filter.spec.ts new file mode 100644 index 0000000..7e7b20b --- /dev/null +++ b/tests/filter.spec.ts @@ -0,0 +1,71 @@ +import filter from "../lib/filter"; + +import timeout from "./helpers/timeout"; +import sort from "./helpers/sort"; + +const LIST = [15, 1, 8]; +const STOP_LIST = [251, 1, 50]; + +describe("promizr.filter()", () => { + + test("should returns the filtered array", async () => { + const spy = jest.fn(num => num % 2 === 1); + + const res = await filter(LIST, num => timeout(num).then(() => spy(num))); + + expect(res).toEqual(sort(LIST.filter(n => n % 2 === 1))); + }); + + test("should call each provided methods with given values in parallel", async () => { + const spy = jest.fn(); + + await filter(LIST, (num, index, list) => timeout(num).then(() => spy(num, index, list))); + + expect(spy).toHaveBeenCalledTimes(LIST.length); + + const ordered = sort(LIST); + for (let i = 0; i < LIST.length; i++) { + expect(spy).toHaveBeenNthCalledWith(i + 1, ordered[i], LIST.indexOf(ordered[i]), LIST); + } + }); + + test("should reject if an iterator throws", async () => { + const spy = jest.fn(); + const err = new Error("test"); + + spy.mockResolvedValueOnce(true); + spy.mockRejectedValueOnce(err); + spy.mockResolvedValueOnce(false); + + await expect(filter(LIST, spy)) + .rejects.toBe(err); + }); + + test("should stop if an iterator throws", async () => { + const spy = jest.fn(n => n % 2 === 1); + const err = new Error("test"); + + const iterator = jest.fn((num: number, i: number) => { + return timeout(num).then(() => { + if (i === 2) { + return Promise.reject(err); + } + + return spy(num); + }); + }); + + await expect(filter(STOP_LIST, iterator)) + .rejects.toBe(err); + + + expect(spy).toBeCalledTimes(1); + expect(spy).toBeCalledWith(STOP_LIST[1]); + + expect(iterator).toBeCalledTimes(3); + expect(iterator).toHaveBeenNthCalledWith(1, STOP_LIST[0], 0, STOP_LIST); + expect(iterator).toHaveBeenNthCalledWith(2, STOP_LIST[1], 1, STOP_LIST); + expect(iterator).toHaveBeenNthCalledWith(3, STOP_LIST[2], 2, STOP_LIST); + }); + +}); diff --git a/tests/filterSeries.spec.ts b/tests/filterSeries.spec.ts new file mode 100644 index 0000000..a4f2766 --- /dev/null +++ b/tests/filterSeries.spec.ts @@ -0,0 +1,69 @@ +import filterSeries from "../lib/filterSeries"; + +import timeout from "./helpers/timeout"; +import sort from "./helpers/sort"; + +const LIST = [15, 1, 8]; +const STOP_LIST = [251, 1, 50]; + +describe("promizr.filterSeries()", () => { + + test("should returns the filtered array", async () => { + const spy = jest.fn(num => num % 2 === 1); + + const res = await filterSeries(LIST, num => timeout(num).then(() => spy(num))); + + expect(res).toEqual(LIST.filter(n => n % 2 === 1)); + }); + + test("should call each provided methods with given values in series", async () => { + const spy = jest.fn(); + + await filterSeries(LIST, (num, index, list) => timeout(num).then(() => spy(num, index, list))); + + expect(spy).toHaveBeenCalledTimes(LIST.length); + + for (let i = 0; i < LIST.length; i++) { + expect(spy).toHaveBeenNthCalledWith(i + 1, LIST[i], i, LIST); + } + }); + + test("should reject if an iterator throws", async () => { + const spy = jest.fn(); + const err = new Error("test"); + + spy.mockResolvedValueOnce(true); + spy.mockRejectedValueOnce(err); + spy.mockResolvedValueOnce(false); + + await expect(filterSeries(LIST, spy)) + .rejects.toBe(err); + }); + + test("should stop if an iterator throws", async () => { + const spy = jest.fn(n => n % 2 === 1); + const err = new Error("test"); + + const iterator = jest.fn((num: number, i: number) => { + return timeout(num).then(() => { + if (i === 1) { + return Promise.reject(err); + } + + return spy(num); + }); + }); + + await expect(filterSeries(STOP_LIST, iterator)) + .rejects.toBe(err); + + + expect(spy).toBeCalledTimes(1); + expect(spy).toBeCalledWith(STOP_LIST[0]); + + expect(iterator).toBeCalledTimes(2); + expect(iterator).toHaveBeenNthCalledWith(1, STOP_LIST[0], 0, STOP_LIST); + expect(iterator).toHaveBeenNthCalledWith(2, STOP_LIST[1], 1, STOP_LIST); + }); + +}); diff --git a/tests/find.spec.ts b/tests/find.spec.ts new file mode 100644 index 0000000..73be930 --- /dev/null +++ b/tests/find.spec.ts @@ -0,0 +1,95 @@ +import find from "../lib/find"; + +import timeout from "./helpers/timeout"; +import sort from "./helpers/sort"; + +const LIST = [15, 1, 8]; +const STOP_LIST = [251, 1, 50]; + +describe("promizr.find()", () => { + + test("should return first found result filtered by iterator", async () => { + const spy = jest.fn(num => num % 2 === 1); + + const result = await find(LIST, num => timeout(num).then(() => spy(num))); + + expect(result).toBe(LIST[1]); + }); + + test("should call each provided methods with given values in parallel", async () => { + const spy = jest.fn((...args) => false); + + await find(LIST, (num, i, list) => timeout(num).then(() => spy(num, i, list))); + + expect(spy).toHaveBeenCalledTimes(3); + + const ordered = sort(LIST); + for (let i = 0; i < LIST.length; i++) { + expect(spy).toHaveBeenNthCalledWith(i + 1, ordered[i], LIST.indexOf(ordered[i]), LIST); + } + }); + + test("should return undefined if no result found", async () => { + const spy = jest.fn(num => false); + + const result = await find(LIST, num => timeout(num).then(() => spy(num))) + + expect(spy).toHaveBeenCalledTimes(3); + + expect(result).toBeUndefined(); + }); + + test("should stop if an item is found", async () => { + const spy = jest.fn(num => num % 2 === 1); + const iterator = jest.fn(num => timeout(num).then(() => spy(num))); + + await find(STOP_LIST, iterator); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(STOP_LIST[1]); + + expect(iterator).toHaveBeenCalledTimes(3) + expect(iterator).toHaveBeenNthCalledWith(1, STOP_LIST[0], 0, STOP_LIST); + expect(iterator).toHaveBeenNthCalledWith(2, STOP_LIST[1], 1, STOP_LIST); + expect(iterator).toHaveBeenNthCalledWith(3, STOP_LIST[2], 2, STOP_LIST); + }); + + test("should reject if an iterator throws", async () => { + const spy = jest.fn(); + const err = new Error("test"); + + spy.mockResolvedValueOnce(false); + spy.mockRejectedValueOnce(err); + spy.mockResolvedValueOnce(false); + + await expect(find(LIST, spy)) + .rejects.toBe(err); + }); + + test("should stop if an iterator throws", async () => { + const spy = jest.fn(num => false); + const err = new Error("test"); + + const iterator = jest.fn(num => { + return timeout(num).then(() => { + if (num === STOP_LIST[2]) { + throw err; + } + + return spy(num); + }); + }); + + await expect(find(STOP_LIST, iterator)) + .rejects.toBe(err); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(STOP_LIST[1]); + + expect(iterator).toHaveBeenCalledTimes(3) + expect(iterator).toHaveBeenNthCalledWith(1, STOP_LIST[0], 0, STOP_LIST); + expect(iterator).toHaveBeenNthCalledWith(2, STOP_LIST[1], 1, STOP_LIST); + expect(iterator).toHaveBeenNthCalledWith(3, STOP_LIST[2], 2, STOP_LIST); + }); + +}); diff --git a/tests/findSeries.spec.ts b/tests/findSeries.spec.ts new file mode 100644 index 0000000..c6c3843 --- /dev/null +++ b/tests/findSeries.spec.ts @@ -0,0 +1,90 @@ +import findSeries from "../lib/findSeries"; + +import timeout from "./helpers/timeout"; + +const LIST = [15, 1, 8]; + +describe("promizr.findSeries()", () => { + + test("should return first found result filtered by iterator", async () => { + const spy = jest.fn(num => num % 2 === 1); + + const result = await findSeries(LIST, num => timeout(num).then(() => spy(num))); + + expect(result).toBe(LIST[0]); + expect(spy).toHaveBeenCalledTimes(1); + }); + + test("should call each provided methods with given values in series", async () => { + const spy = jest.fn((...args) => false); + + await findSeries(LIST, (num, i, list) => timeout(num).then(() => spy(num, i, list))); + + expect(spy).toHaveBeenCalledTimes(3); + + for (let i = 0; i < LIST.length; i++) { + expect(spy).toHaveBeenNthCalledWith(i + 1, LIST[i], i, LIST); + } + }); + + test("should return undefined if no result found", async () => { + const spy = jest.fn(num => false); + + const result = await findSeries(LIST, num => timeout(num).then(() => spy(num))) + + expect(spy).toHaveBeenCalledTimes(3); + + expect(result).toBeUndefined(); + }); + + test("should stop if an item is found", async () => { + const spy = jest.fn(num => num % 2 === 1); + const iterator = jest.fn(num => timeout(num).then(() => spy(num))); + + await findSeries(LIST, iterator); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(LIST[0]); + + expect(iterator).toHaveBeenCalledTimes(1) + expect(iterator).toHaveBeenNthCalledWith(1, LIST[0], 0, LIST); + }); + + test("should reject if an iterator throws", async () => { + const spy = jest.fn(); + const err = new Error("test"); + + spy.mockResolvedValueOnce(false); + spy.mockRejectedValueOnce(err); + spy.mockResolvedValueOnce(false); + + await expect(findSeries(LIST, spy)) + .rejects.toBe(err); + }); + + test("should stop if an iterator throws", async () => { + const spy = jest.fn(num => false); + const err = new Error("test"); + + const iterator = jest.fn(num => { + return timeout(num).then(() => { + if (num === LIST[1]) { + throw err; + } + + return spy(num); + }); + }); + + await expect(findSeries(LIST, iterator)) + .rejects.toBe(err); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(LIST[0]); + + expect(iterator).toHaveBeenCalledTimes(2) + expect(iterator).toHaveBeenNthCalledWith(1, LIST[0], 0, LIST); + expect(iterator).toHaveBeenNthCalledWith(2, LIST[1], 1, LIST); + }); + +}); diff --git a/tests/forEach.spec.ts b/tests/forEach.spec.ts new file mode 100644 index 0000000..353be83 --- /dev/null +++ b/tests/forEach.spec.ts @@ -0,0 +1,10 @@ +import each from "../lib/each"; +import forEach from "../lib/forEach"; + +describe("promizr.forEach()", () => { + + test("should be an alias of each", async () => { + expect(forEach).toBe(each); + }); + +}); diff --git a/tests/forEachLimit.spec.ts b/tests/forEachLimit.spec.ts new file mode 100644 index 0000000..1fa0f73 --- /dev/null +++ b/tests/forEachLimit.spec.ts @@ -0,0 +1,10 @@ +import eachLimit from "../lib/eachLimit"; +import forEachLimit from "../lib/forEachLimit"; + +describe("promizr.forEachLimit()", () => { + + test("should be an alias of eachLimit", async () => { + expect(forEachLimit).toBe(eachLimit); + }); + +}); diff --git a/tests/forEachSeries.spec.ts b/tests/forEachSeries.spec.ts new file mode 100644 index 0000000..daa7a61 --- /dev/null +++ b/tests/forEachSeries.spec.ts @@ -0,0 +1,10 @@ +import eachSeries from "../lib/eachSeries"; +import forEachSeries from "../lib/forEachSeries"; + +describe("promizr.forEachSeries()", () => { + + test("should be an alias of eachSeries", async () => { + expect(forEachSeries).toBe(eachSeries); + }); + +}); diff --git a/tests/forever.spec.ts b/tests/forever.spec.ts new file mode 100644 index 0000000..3b6c463 --- /dev/null +++ b/tests/forever.spec.ts @@ -0,0 +1,28 @@ +import forever from "../lib/forever"; + +import timeout from "./helpers/timeout"; + +const RUNS = 10; + +describe("promizr.forever", () => { + + test("should call the method until forever until it throws", async () => { + const err = new Error("test"); + + let i = 0; + + const spy = jest.fn(() => { + if (++i === RUNS) { + throw err; + } + + return timeout(i); + }); + + await expect(forever(spy)) + .rejects.toBe(err); + + expect(spy).toHaveBeenCalledTimes(RUNS); + }); + +}); diff --git a/tests/helpers/ProgressContext.ts b/tests/helpers/ProgressContext.ts new file mode 100644 index 0000000..7e27c19 --- /dev/null +++ b/tests/helpers/ProgressContext.ts @@ -0,0 +1,90 @@ +import ProgressPromise, { ProgressPromiseDeferred } from "../../lib/ProgressPromise"; + +export type ProgressPromiseable = T | PromiseLike | ProgressPromise; +export type ProgressContextDeferred = Omit, "progress"> & { progress?: (val: P) => void | undefined }; + +import defer from "../../lib/defer"; + +export default class ProgressContext { + private deferreds: ProgressContextDeferred[] = []; + + public constructor(public count: number) { + this.reset(); + } + + public promises(): Array | ProgressPromise> { + return this.deferreds.map(d => d.promise); + } + + public addFake(): this { + const dfd = defer(); + this.deferreds.push({ + resolve: dfd.resolve, + reject: dfd.reject, + progress: undefined, + promise: dfd.promise + }); + + return this; + } + + public resolve(): void; + public resolve(index: number): void; + public resolve(index: number, value: number): void; + public resolve(index?: number, value?: number): void { + if (typeof index === "undefined") { + this.deferreds.forEach(d => d.resolve()); + return; + } + + const defer = this.deferreds[index]; + if (defer) { + defer.resolve(value); + } + } + + public reject(): void; + public reject(index: number): void; + public reject(index: number, value: number): void; + public reject(index?: number, reason?: Error | any): void { + if (typeof index === "undefined") { + this.deferreds.forEach(d => d.reject(reason)); + return; + } + + const defer = this.deferreds[index]; + if (defer) { + defer.reject(reason); + } + } + + public progress(index: number): void; + public progress(index: number, value: number): void; + public progress(index: number, value?: number): void { + if (typeof value === "undefined") { + this.deferreds.forEach(d => d.progress && d.progress(index)); + return; + } + + const defer = this.deferreds[index]; + if (defer && defer.progress) { + defer.progress(value); + } + } + + public all(): ProgressPromise> { + return ProgressPromise.all(this.promises()) as ProgressPromise>; + } + + public race(): ProgressPromise> { + return ProgressPromise.race(this.promises()) as ProgressPromise>; + } + + public reset(): void { + this.deferreds = new Array(this.count); + + for (let i = 0; i < this.count; i++) { + this.deferreds[i] = ProgressPromise.defer(); + } + } +} diff --git a/tests/helpers/createDeferreds.ts b/tests/helpers/createDeferreds.ts new file mode 100644 index 0000000..53c27e1 --- /dev/null +++ b/tests/helpers/createDeferreds.ts @@ -0,0 +1,14 @@ +import type { Deferred } from "../../lib/_types"; + +import defer from "../../lib/defer"; + +import timeout from "./timeout"; + +export default function createDeferreds(list: T[]): Array> { + return list.map(() => { + const dfd = defer(); + dfd.promise = dfd.promise.then(() => timeout(1)); + + return dfd; + }); +} diff --git a/tests/helpers/createExecutorObject.ts b/tests/helpers/createExecutorObject.ts new file mode 100644 index 0000000..c363095 --- /dev/null +++ b/tests/helpers/createExecutorObject.ts @@ -0,0 +1,14 @@ + +export default function createExecutorObject(list: T[], mapper: (key: T) => () => Promise, includeNotCallable?: boolean): Record { + const result: Record = {}; + + list.forEach(val => { + result[`property-${val}`] = mapper(val); + }); + + if (includeNotCallable) { + result["not-called"] = "value"; + } + + return result; +} diff --git a/tests/helpers/sort.ts b/tests/helpers/sort.ts new file mode 100644 index 0000000..6eeca89 --- /dev/null +++ b/tests/helpers/sort.ts @@ -0,0 +1,3 @@ +export default function sort(list: number[]): number[] { + return [...list].sort((a, b) => a - b); +} \ No newline at end of file diff --git a/tests/helpers/timeout.ts b/tests/helpers/timeout.ts new file mode 100644 index 0000000..389435c --- /dev/null +++ b/tests/helpers/timeout.ts @@ -0,0 +1,3 @@ +export default function timeout(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); +} diff --git a/tests/immediate.spec.ts b/tests/immediate.spec.ts new file mode 100644 index 0000000..40e37ec --- /dev/null +++ b/tests/immediate.spec.ts @@ -0,0 +1,24 @@ +import immediate from "../lib/immediate"; + +jest.useFakeTimers(); + +describe("promizr.immediate()", () => { + + test("should return a Promise that resolves on next tick", async () => { + const resolved = jest.fn(); + const promise = immediate().then(resolved); + + expect(setImmediate).toHaveBeenCalledTimes(1); + expect(setImmediate).toHaveBeenCalledWith(expect.any(Function)); + + expect(resolved).not.toHaveBeenCalled(); + + jest.runAllImmediates(); + + await expect(promise).resolves.toBeUndefined(); + + expect(resolved).toHaveBeenCalledTimes(1); + expect(resolved).toHaveBeenCalledWith(undefined); + }); + +}); diff --git a/tests/log.spec.ts b/tests/log.spec.ts new file mode 100644 index 0000000..84fb597 --- /dev/null +++ b/tests/log.spec.ts @@ -0,0 +1,63 @@ +import log from "../lib/log"; + +import timeout from "./helpers/timeout"; + +type Spy any> = jest.SpyInstance, Parameters>; + +describe("promizr.log()", () => { + let consoleLogSpy: Spy; + let consoleErrorSpy: Spy; + beforeEach(() => { + consoleLogSpy = jest.spyOn(console, "log"); + consoleLogSpy.mockReturnValue(); + + consoleErrorSpy = jest.spyOn(console, "error"); + consoleErrorSpy.mockReturnValue(); + }); + afterEach(() => { + consoleLogSpy.mockRestore(); + consoleErrorSpy.mockRestore(); + }); + + test("should call the given task with passed arguments", async () => { + const task = jest.fn((...args) => timeout(10).then(() => "result")); + + await log(task, "arg1", true); + + expect(task).toHaveBeenCalledTimes(1); + expect(task).toHaveBeenCalledWith("arg1", true); + }); + + test("should return the result of the task", async () => { + const task = jest.fn((...args) => timeout(10).then(() => "result")); + + const res = await log(task, "arg1", true); + + expect(res).toBe("result"); + }); + + test("should call console.log with the result of the task if it succeeds", async () => { + await log((...args) => timeout(10).then(() => "result"), "arg1", true); + + expect(consoleLogSpy).toHaveBeenCalledTimes(1); + expect(consoleLogSpy).toHaveBeenCalledWith("result"); + }); + + test("should throw with the Error of the task if it throws", async () => { + const err = new Error("test"); + + await expect(log((...args) => timeout(10).then(() => { throw err; }), "arg1", true)) + .rejects.toBe(err); + }); + + test("should call console.error with the result of the task if it throws", async () => { + const err = new Error("test"); + + await expect(log((...args) => timeout(10).then(() => { throw err; }), "arg1", true)) + .rejects.toBe(err); + + expect(consoleErrorSpy).toHaveBeenCalledTimes(1); + expect(consoleErrorSpy).toHaveBeenCalledWith(err); + }); + +}); diff --git a/tests/map.spec.ts b/tests/map.spec.ts new file mode 100644 index 0000000..40c05a3 --- /dev/null +++ b/tests/map.spec.ts @@ -0,0 +1,71 @@ +import map from "../lib/map"; + +import timeout from "./helpers/timeout"; +import sort from "./helpers/sort"; + +const LIST = [15, 1, 8]; +const STOP_LIST = [251, 1, 50]; + +describe("promizr.map()", () => { + + test("should returns the mapped array", async () => { + const spy = jest.fn(num => num * num); + + const res = await map(LIST, num => timeout(num).then(() => spy(num))); + + expect(res).toEqual(LIST.map(n => n * n)); + }); + + test("should call each provided methods with given values in parallel", async () => { + const spy = jest.fn(); + + await map(LIST, (num, index, list) => timeout(num).then(() => spy(num, index, list))); + + expect(spy).toHaveBeenCalledTimes(LIST.length); + + const ordered = sort(LIST); + for (let i = 0; i < LIST.length; i++) { + expect(spy).toHaveBeenNthCalledWith(i + 1, ordered[i], LIST.indexOf(ordered[i]), LIST); + } + }); + + test("should reject if an iterator throws", async () => { + const spy = jest.fn(); + const err = new Error("test"); + + spy.mockResolvedValueOnce(undefined); + spy.mockRejectedValueOnce(err); + spy.mockResolvedValueOnce(undefined); + + await expect(map(LIST, spy)) + .rejects.toBe(err); + }); + + test("should stop if an iterator throws", async () => { + const spy = jest.fn(); + const err = new Error("test"); + + const iterator = jest.fn((num: number, i: number) => { + return timeout(num).then(() => { + if (i === 2) { + return Promise.reject(err); + } + + spy(num); + }); + }); + + await expect(map(STOP_LIST, iterator)) + .rejects.toBe(err); + + + expect(spy).toBeCalledTimes(1); + expect(spy).toBeCalledWith(STOP_LIST[1]); + + expect(iterator).toBeCalledTimes(3); + expect(iterator).toHaveBeenNthCalledWith(1, STOP_LIST[0], 0, STOP_LIST); + expect(iterator).toHaveBeenNthCalledWith(2, STOP_LIST[1], 1, STOP_LIST); + expect(iterator).toHaveBeenNthCalledWith(3, STOP_LIST[2], 2, STOP_LIST); + }); + +}); diff --git a/tests/mapLimit.spec.ts b/tests/mapLimit.spec.ts new file mode 100644 index 0000000..7e1e3a0 --- /dev/null +++ b/tests/mapLimit.spec.ts @@ -0,0 +1,138 @@ +import mapLimit from "../lib/mapLimit"; +import QueueError from "../lib/QueueError"; +import createDeferreds from "./helpers/createDeferreds"; + +import timeout from "./helpers/timeout"; + +const LIST = [15, 1, 8, 25, 5]; +const LIMIT = 3; +const TOTAL = LIST.length; + +const BIG_LIST = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; + +describe("promizr.mapLimit()", () => { + + test("should returns a Promise which resolves when all tasks are done", async () => { + const spy = jest.fn((n, i, l) => timeout(n).then(() => n * n)); + + const promise = mapLimit(LIST, LIMIT, spy); + + expect(promise).toEqual(expect.any(Promise)); + + const res = await promise; + + for (const num of LIST) { + expect(res).toContain(num * num); + } + + expect(spy).toHaveBeenCalledTimes(TOTAL); + + for (let i = 0; i < TOTAL; i++) { + expect(spy).toHaveBeenCalledWith(LIST[i], i, LIST); + } + }); + + test("should reject immediately if any item reject", async () => { + const err = new Error("test"); + const spy = jest.fn(() => { throw err; }); + + await expect(mapLimit(LIST, LIMIT, () => timeout(1).then(spy))) + .rejects.toBe(err); + + expect(spy).toHaveBeenCalledTimes(1); + }); + + test("should wait to reject QueueError if waitToReject is set to true", async () => { + const spy = jest.fn(n => n % 2 === 0 ? Promise.reject(new Error("test")) : Promise.resolve(n)); + + const iterator = (n: number) => timeout(1).then(() => spy(n)); + + await expect(mapLimit(LIST, LIMIT, iterator, { waitToReject: true })) + .rejects.toBeInstanceOf(QueueError); + + expect(spy).toHaveBeenCalledTimes(TOTAL); + }); + + test("should only launch specified limit number of task simultaneously", async () => { + const spy = jest.fn(() => timeout(20)); + + mapLimit(LIST, LIMIT, spy); + + await timeout(10); + + expect(spy).toHaveBeenCalledTimes(LIMIT); + }); + + test("should continue to limit number of task simultaneously when promises are resolved", async () => { + const dfds = createDeferreds(BIG_LIST) + const spy = jest.fn((n) => dfds[n].promise); + + mapLimit(BIG_LIST, LIMIT, spy); + + expect(spy).not.toHaveBeenCalled(); + + await timeout(10); + + expect(spy).toHaveBeenCalledTimes(LIMIT); + + for (let i = 0, len = LIMIT; i < len; i++) { + dfds[i].resolve(); + } + + await dfds[LIMIT - 1].promise; + await timeout(10); + + expect(spy).toHaveBeenCalledTimes(LIMIT * 2); + + for (let i = LIMIT, len = LIMIT * 3; i < len; i++) { + dfds[i].resolve(); + } + + await dfds[LIMIT * 2 - 1].promise; + await timeout(10); + + expect(spy).toHaveBeenCalledTimes(LIMIT * 4); + }); + + test("should not continue to execute works if exception occurred", async () => { + const err = new Error("This is an error !"); + const dfds = createDeferreds(BIG_LIST) + const spy = jest.fn(n => dfds[n].promise); + + const promise = mapLimit(BIG_LIST, LIMIT, spy); + + expect(spy).not.toHaveBeenCalled(); + + await timeout(10); + + expect(spy).toHaveBeenCalledTimes(LIMIT); + + for (let i = 0, len = LIMIT; i < len; i++) { + dfds[i].reject(err); + } + + await expect(promise) + .rejects.toBe(err); + + expect(spy).toHaveBeenCalledTimes(LIMIT); + }); + + test("should continue to execute works even if exception occurred and stopOnError is false", async () => { + const err = new Error("test"); + const spy = jest.fn(n => { + return timeout(n).then(() => { + throw err; + }); + }); + + await expect(mapLimit(BIG_LIST, LIMIT, spy, { stopOnError: false })) + .rejects.toBe(err); + + expect(spy).toHaveBeenCalledTimes(LIMIT); + + await timeout(100); + + expect(spy).toHaveBeenCalledTimes(BIG_LIST.length); + }); + +}); diff --git a/tests/mapSeries.spec.ts b/tests/mapSeries.spec.ts new file mode 100644 index 0000000..a7d69ff --- /dev/null +++ b/tests/mapSeries.spec.ts @@ -0,0 +1,68 @@ +import mapSeries from "../lib/mapSeries"; + +import timeout from "./helpers/timeout"; + +const LIST = [15, 1, 8]; +const STOP_LIST = [251, 1, 50]; + +describe("promizr.mapSeries()", () => { + + test("should returns the mapped array", async () => { + const spy = jest.fn(num => num * num); + + const res = await mapSeries(LIST, num => timeout(num).then(() => spy(num))); + + expect(res).toEqual(LIST.map(n => n * n)); + }); + + test("should call each provided methods with given values in series", async () => { + const spy = jest.fn(); + + await mapSeries(LIST, (num, index, list) => timeout(num).then(() => spy(num, index, list))); + + expect(spy).toHaveBeenCalledTimes(LIST.length); + + for (let i = 0; i < LIST.length; i++) { + expect(spy).toHaveBeenNthCalledWith(i + 1, LIST[i], i, LIST); + } + }); + + test("should reject if an iterator throws", async () => { + const spy = jest.fn(); + const err = new Error("test"); + + spy.mockResolvedValueOnce(undefined); + spy.mockRejectedValueOnce(err); + spy.mockResolvedValueOnce(undefined); + + await expect(mapSeries(LIST, spy)) + .rejects.toBe(err); + }); + + test("should stop if an iterator throws", async () => { + const spy = jest.fn(); + const err = new Error("test"); + + const iterator = jest.fn((num: number, i: number) => { + return timeout(num).then(() => { + if (i === 1) { + return Promise.reject(err); + } + + spy(num); + }); + }); + + await expect(mapSeries(STOP_LIST, iterator)) + .rejects.toBe(err); + + + expect(spy).toBeCalledTimes(1); + expect(spy).toBeCalledWith(STOP_LIST[0]); + + expect(iterator).toBeCalledTimes(2); + expect(iterator).toHaveBeenNthCalledWith(1, STOP_LIST[0], 0, STOP_LIST); + expect(iterator).toHaveBeenNthCalledWith(2, STOP_LIST[1], 1, STOP_LIST); + }); + +}); diff --git a/tests/memoize.spec.ts b/tests/memoize.spec.ts new file mode 100644 index 0000000..d886f10 --- /dev/null +++ b/tests/memoize.spec.ts @@ -0,0 +1,110 @@ +import * as crypto from "crypto"; + +import memoize from "../lib/memoize"; + +import timeout from "./helpers/timeout"; + +describe("promizr.memoize()", () => { + + test("should returns a function that executes the task when called", async () => { + const spy = jest.fn(); + const fn = memoize(spy); + + expect(spy).not.toHaveBeenCalled(); + + await fn(); + + expect(spy).toHaveBeenCalledTimes(1); + }); + + test("should pass arguments to the task", async () => { + const spy = jest.fn(); + + await memoize(spy)("arg"); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith("arg"); + }); + + test("should return the result of the task", async () => { + const spy = jest.fn(arg => timeout(20).then(() => `prefix-${arg}`)); + + const res = await memoize(spy)("arg"); + + expect(res).toBe("prefix-arg"); + }); + + test("should memoize the call and do not call the function twice", async () => { + const spy = jest.fn(arg => timeout(20).then(() => `prefix-${arg}`)); + const fn = memoize(spy); + + const res1 = await fn("arg1"); + const res2 = await fn("arg2"); + + expect(spy).toHaveBeenCalledTimes(1); + expect(res1).toBe(res2); + expect(res1).toBe("prefix-arg1"); + }); + + test("should memoize the call and hash parameters if hash = true", async () => { + const spy = jest.fn((arg, force) => timeout(20).then(() => `prefix-${arg}-${force}`)); + const fn = memoize(spy, true); + + const res1 = await fn("arg1", true); + const res2 = await fn("arg2", false); + const res3 = await fn("arg2", false); + const res4 = await fn("arg2", true); + + expect(spy).toHaveBeenCalledTimes(3); + + expect(res1).not.toBe(res2); + expect(res2).toBe(res3); + expect(res3).not.toBe(res4); + + expect(res1).toBe("prefix-arg1-true"); + expect(res2).toBe("prefix-arg2-false"); + expect(res4).toBe("prefix-arg2-true"); + }); + + test("should memoize the call and use custom hash function if provided", async () => { + const spy = jest.fn((arg, force) => timeout(20).then(() => `prefix-${arg}-${force}`)); + const hash = jest.fn(customHash); + + const fn = memoize(spy, hash); + + const args = [ + [Buffer.from("arg1"), true], + [Buffer.from("arg2"), false], + [Buffer.from("arg2"), false], + [Buffer.from("arg2"), true] + ] as const; + + const res1 = await fn(...args[0]); + const res2 = await fn(...args[1]); + const res3 = await fn(...args[2]); + const res4 = await fn(...args[3]); + + expect(spy).toHaveBeenCalledTimes(3); + expect(hash).toHaveBeenCalledTimes(4); + + expect(res1).not.toBe(res2); + expect(res2).toBe(res3); + expect(res3).not.toBe(res4); + + expect(res1).toBe("prefix-arg1-true"); + expect(res2).toBe("prefix-arg2-false"); + expect(res4).toBe("prefix-arg2-true"); + + for (let i = 0; i < args.length; i++) { + expect(hash).toHaveBeenNthCalledWith(i + 1, args[i]); + } + }); + +}); + +function customHash(args: unknown[]): string { + return args.map(arg => { + const buffer = Buffer.isBuffer(arg) ? arg : Buffer.from(String(arg)); + return crypto.createHash("sha1").update(buffer).digest("hex"); + }).join("/"); +} diff --git a/tests/nextTick.spec.ts b/tests/nextTick.spec.ts new file mode 100644 index 0000000..481f427 --- /dev/null +++ b/tests/nextTick.spec.ts @@ -0,0 +1,24 @@ +import nextTick from "../lib/nextTick"; + +jest.useFakeTimers(); + +describe("promizr.nextTick()", () => { + + test("should return a Promise that resolves on next tick", () => { + const callback = jest.fn(); + const res = nextTick(callback); + + expect(res).toBeUndefined(); + + expect(setImmediate).toHaveBeenCalledTimes(1); + expect(setImmediate).toHaveBeenCalledWith(callback); + + expect(callback).not.toHaveBeenCalled(); + + jest.runAllImmediates(); + + expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledWith(); + }); + +}); diff --git a/tests/parallel.spec.ts b/tests/parallel.spec.ts new file mode 100644 index 0000000..608ef3b --- /dev/null +++ b/tests/parallel.spec.ts @@ -0,0 +1,128 @@ +import parallel from "../lib/parallel"; + +import timeout from "./helpers/timeout"; +import sort from "./helpers/sort"; +import createExecutorObject from "./helpers/createExecutorObject"; + +const LIST = [15, 1, 8]; +const STOP_LIST = [251, 1, 50]; + +describe("promizr.parallel()", () => { + + describe("with an array", () => { + + test("should call each provided methods with given values in parallel", async () => { + const spy = jest.fn(); + + await parallel(LIST.map(num => () => timeout(num).then(() => spy(num)))); + + expect(spy).toHaveBeenCalledTimes(3); + + const ordered = sort(LIST); + for (let i = 0; i < LIST.length; i++) { + expect(spy).toHaveBeenNthCalledWith(i + 1, ordered[i]); + } + }); + + test("should return an array with each values returned by executors", async () => { + const spy = jest.fn(num => num * num); + const executors = LIST.map(num => () => timeout(num).then(() => spy(num))); + + const results = await parallel(executors); + + expect(spy).toHaveBeenCalledTimes(3); + + expect(results.length).toBe(3); + expect(results[0]).toBe(LIST[0] * LIST[0]); + expect(results[1]).toBe(LIST[1] * LIST[1]); + expect(results[2]).toBe(LIST[2] * LIST[2]); + + }); + + test("should stop if an executor throws", async () => { + const spy = jest.fn(); + const err = new Error("test"); + + const executors = STOP_LIST.map(num => jest.fn(() => { + return timeout(num).then(() => { + if (num === STOP_LIST[2]) { + throw err; + } + + spy(num); + }); + })); + + await expect(parallel(executors)) + .rejects.toBe(err); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(STOP_LIST[1]); + + expect(executors[0]).toHaveBeenCalledTimes(1); + expect(executors[1]).toHaveBeenCalledTimes(1); + expect(executors[2]).toHaveBeenCalledTimes(1); + + }); + + }); + + describe("whith an object argument", () => { + + test("should call each provided methods with given values in parallel", async () => { + const spy = jest.fn(); + const executors = createExecutorObject(LIST, num => () => timeout(num).then(() => spy(num)), true); + + await parallel(executors); + + expect(spy).toHaveBeenCalledTimes(3); + + const ordered = sort(LIST); + for (let i = 0; i < LIST.length; i++) { + expect(spy).toHaveBeenNthCalledWith(i + 1, ordered[i]); + } + }); + + test("should return an object with each values returned by executors", async () => { + const spy = jest.fn(num => num * num); + const executors = createExecutorObject(LIST, num => () => timeout(num).then(() => spy(num))); + + const results = await parallel(executors); + expect(spy).toHaveBeenCalledTimes(3); + + expect(Object.keys(results).length).toBe(3); + + expect(results[`property-${LIST[0]}`]).toBe(LIST[0] * LIST[0]); + expect(results[`property-${LIST[1]}`]).toBe(LIST[1] * LIST[1]); + expect(results[`property-${LIST[2]}`]).toBe(LIST[2] * LIST[2]); + + }); + + test("should stop if an executor throws", async () => { + const spy = jest.fn(); + const err = new Error("test"); + + const executors = createExecutorObject(STOP_LIST, num => jest.fn(() => { + return timeout(num).then(() => { + if (num === STOP_LIST[2]) { + throw err; + } + + spy(num); + }); + })); + + await expect(parallel(executors)) + .rejects.toBe(err); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(STOP_LIST[1]); + + expect(executors[`property-${STOP_LIST[0]}`]).toHaveBeenCalledTimes(1); + expect(executors[`property-${STOP_LIST[1]}`]).toHaveBeenCalledTimes(1); + expect(executors[`property-${STOP_LIST[2]}`]).toHaveBeenCalledTimes(1); + }); + + }); + +}); diff --git a/tests/parallelLimit.spec.ts b/tests/parallelLimit.spec.ts new file mode 100644 index 0000000..19db3d9 --- /dev/null +++ b/tests/parallelLimit.spec.ts @@ -0,0 +1,281 @@ +import parallelLimit from "../lib/parallelLimit"; +import QueueError from "../lib/QueueError"; + +import timeout from "./helpers/timeout"; +import createExecutorObject from "./helpers/createExecutorObject"; +import createDeferreds from "./helpers/createDeferreds"; + +const LIST = [15, 1, 8, 25, 5]; +const LIMIT = 3; +const TOTAL = LIST.length; + +const BIG_LIST = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; + +describe("promizr.parallelLimit()", () => { + + describe("with an array", () => { + + test("should returns a Promise which resolves when all tasks are done", async () => { + const spies = LIST.map(n => jest.fn(() => timeout(n).then(() => n * n))); + + const promise = parallelLimit(spies, LIMIT); + + expect(promise).toEqual(expect.any(Promise)); + + const res = await promise; + + for (const num of LIST) { + expect(res).toContain(num * num); + } + + for (const spy of spies) { + expect(spy).toHaveBeenCalledTimes(1); + } + }); + + test("should reject immediately if any item reject", async () => { + const err = new Error("test"); + const spy = jest.fn(() => { throw err; }); + + const tasks = LIST.map((n, i) => () => timeout(1).then(spy)); + + await expect(parallelLimit(tasks, LIMIT)) + .rejects.toBe(err); + + expect(spy).toHaveBeenCalledTimes(1); + }); + + test("should wait to reject QueueError if waitToReject is set to true", async () => { + const spy = jest.fn(n => n % 2 === 0 ? Promise.reject(new Error("test")) : Promise.resolve(n)); + const tasks = LIST.map((n) => () => timeout(1).then(() => spy(n))); + + await expect(parallelLimit(tasks, LIMIT, { waitToReject: true })) + .rejects.toBeInstanceOf(QueueError); + + expect(spy).toHaveBeenCalledTimes(TOTAL); + }); + + test("should only launch specified limit number of task simultaneously", async () => { + const spy = jest.fn(() => timeout(20)); + const tasks = LIST.map(() => spy); + + parallelLimit(tasks, LIMIT); + + await timeout(10); + + expect(spy).toHaveBeenCalledTimes(LIMIT); + }); + + test("should continue to limit number of task simultaneously when promises are resolved", async () => { + const dfds = createDeferreds(BIG_LIST); + + const spy = jest.fn((n) => dfds[n].promise); + const tasks = BIG_LIST.map((n) => () => spy(n)); + + parallelLimit(tasks, LIMIT); + + expect(spy).not.toHaveBeenCalled(); + + await timeout(10); + + expect(spy).toHaveBeenCalledTimes(LIMIT); + + for (let i = 0, len = LIMIT; i < len; i++) { + dfds[i].resolve(); + } + + await dfds[LIMIT - 1].promise; + await timeout(10); + + expect(spy).toHaveBeenCalledTimes(LIMIT * 2); + + for (let i = LIMIT, len = LIMIT * 3; i < len; i++) { + dfds[i].resolve(); + } + + await dfds[LIMIT * 2 - 1].promise; + await timeout(10); + + expect(spy).toHaveBeenCalledTimes(LIMIT * 4); + }); + + test("should not continue to execute works if exception occurred", async () => { + const err = new Error("This is an error !"); + const dfds = createDeferreds(BIG_LIST); + + const spy = jest.fn(n => dfds[n].promise); + const tasks = BIG_LIST.map((n) => () => spy(n)); + + const promise = parallelLimit(tasks, LIMIT); + + expect(spy).not.toHaveBeenCalled(); + + await timeout(10); + + expect(spy).toHaveBeenCalledTimes(LIMIT); + + for (let i = 0, len = LIMIT; i < len; i++) { + dfds[i].reject(err); + } + + await expect(promise) + .rejects.toBe(err); + + expect(spy).toHaveBeenCalledTimes(LIMIT); + }); + + test("should continue to execute works even if exception occurred and stopOnError is false", async () => { + const err = new Error("test"); + const spy = jest.fn(n => { + return timeout(n).then(() => { + throw err; + }); + }); + + const tasks = BIG_LIST.map((n) => () => spy(n)); + + await expect(parallelLimit(tasks, LIMIT, { stopOnError: false })) + .rejects.toBe(err); + + expect(spy).toHaveBeenCalledTimes(LIMIT); + + await timeout(100); + + expect(spy).toHaveBeenCalledTimes(BIG_LIST.length); + }); + + }); + + describe("with an array", () => { + + test("should returns a Promise which resolves when all tasks are done", async () => { + const spy = jest.fn(n => async () => n * n); + const obj = createExecutorObject(LIST, spy); + + const promise = parallelLimit(obj, LIMIT); + + expect(promise).toEqual(expect.any(Promise)); + + const res = await promise; + + for (const num of LIST) { + expect(res).toHaveProperty(`property-${num}`, num * num); + } + + expect(spy).toHaveBeenCalledTimes(TOTAL); + }); + + test("should reject immediately if any item reject", async () => { + const err = new Error("test"); + const spy = jest.fn(() => { throw err; }); + + const obj = createExecutorObject(LIST, () => () => timeout(1).then(spy)); + + await expect(parallelLimit(obj, LIMIT)) + .rejects.toBe(err); + + expect(spy).toHaveBeenCalledTimes(1); + }); + + test("should wait to reject QueueError if waitToReject is set to true", async () => { + const spy = jest.fn(n => n % 2 === 0 ? Promise.reject(new Error("test")) : Promise.resolve(n)); + const obj = createExecutorObject(LIST, (n) => () => timeout(1).then(() => spy(n))); + + await expect(parallelLimit(obj, LIMIT, { waitToReject: true })) + .rejects.toBeInstanceOf(QueueError); + + expect(spy).toHaveBeenCalledTimes(TOTAL); + }); + + test("should only launch specified limit number of task simultaneously", async () => { + const spy = jest.fn(() => timeout(20)); + const obj = createExecutorObject(LIST, () => spy); + + parallelLimit(obj, LIMIT); + + await timeout(10); + + expect(spy).toHaveBeenCalledTimes(LIMIT); + }); + + test("should continue to limit number of task simultaneously when promises are resolved", async () => { + const dfds = createDeferreds(BIG_LIST); + + const spy = jest.fn((n) => dfds[n].promise); + const obj = createExecutorObject(BIG_LIST, (n) => () => spy(n)); + + parallelLimit(obj, LIMIT); + + expect(spy).not.toHaveBeenCalled(); + + await timeout(10); + + expect(spy).toHaveBeenCalledTimes(LIMIT); + + for (let i = 0, len = LIMIT; i < len; i++) { + dfds[i].resolve(); + } + + await dfds[LIMIT - 1].promise; + await timeout(10); + + expect(spy).toHaveBeenCalledTimes(LIMIT * 2); + + for (let i = LIMIT, len = LIMIT * 3; i < len; i++) { + dfds[i].resolve(); + } + + await dfds[LIMIT * 2 - 1].promise; + await timeout(10); + + expect(spy).toHaveBeenCalledTimes(LIMIT * 4); + }); + + test("should not continue to execute works if exception occurred", async () => { + const err = new Error("This is an error !"); + const dfds = createDeferreds(BIG_LIST); + + const spy = jest.fn(n => dfds[n].promise); + const obj = createExecutorObject(BIG_LIST, (n) => () => spy(n)); + + const promise = parallelLimit(obj, LIMIT); + + expect(spy).not.toHaveBeenCalled(); + + await timeout(10); + + expect(spy).toHaveBeenCalledTimes(LIMIT); + + for (let i = 0, len = LIMIT; i < len; i++) { + dfds[i].reject(err); + } + + await expect(promise) + .rejects.toBe(err); + + expect(spy).toHaveBeenCalledTimes(LIMIT); + }); + + test("should continue to execute works even if exception occurred and stopOnError is false", async () => { + const err = new Error("test"); + const spy = jest.fn(n => { + return timeout(n).then(() => { + throw err; + }); + }); + + const obj = createExecutorObject(BIG_LIST, (n) => () => spy(n)); + + await expect(parallelLimit(obj, LIMIT, { stopOnError: false })) + .rejects.toBe(err); + + expect(spy).toHaveBeenCalledTimes(LIMIT); + + await timeout(100); + + expect(spy).toHaveBeenCalledTimes(BIG_LIST.length); + }); + + }); + +}); diff --git a/tests/partial.spec.ts b/tests/partial.spec.ts new file mode 100644 index 0000000..19f3b2c --- /dev/null +++ b/tests/partial.spec.ts @@ -0,0 +1,67 @@ +import partial from "../lib/partial"; + +describe("promizr.partial()", () => { + + test("should create a function which call task with combined arguments", async () => { + const spy = jest.fn((arg: string, test: boolean, arg2: string, count: number) => `${arg}-${test}-${arg2}-${count}`); + const fn = partial(spy, "arg", true); + + expect(spy).not.toHaveBeenCalled(); + + await fn("arg2", 2); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith("arg", true, "arg2", 2); + }); + + test("should resolve with the task result if sync", async () => { + const spy = jest.fn((arg: string, test: boolean, arg2: string, count: number) => `${arg}-${test}-${arg2}-${count}`); + + const promise = partial(spy, "arg", true)("arg2", 2); + + expect(spy).toHaveBeenCalledTimes(1); + expect(promise).toEqual(expect.any(Promise)); + + await expect(promise) + .resolves.toBe("arg-true-arg2-2"); + }); + + test("should resolve with the task resolution if async", async () => { + const spy = jest.fn(async (arg: string, test: boolean, arg2: string, count: number) => `${arg}-${test}-${arg2}-${count}`); + + const promise = partial(spy, "arg", true)("arg2", 2); + + expect(spy).toHaveBeenCalledTimes(1); + expect(promise).toEqual(expect.any(Promise)); + + await expect(promise) + .resolves.toBe("arg-true-arg2-2"); + }); + + test("should reject Promise if task throws", async () => { + const err = new Error("test"); + const spy = jest.fn(() => { throw err; }); + + const promise = partial(spy)(); + + expect(spy).toHaveBeenCalledTimes(1); + expect(promise).toEqual(expect.any(Promise)); + + await expect(promise) + .rejects.toBe(err); + }); + + test("should reject Promise if task rejects", async () => { + const err = new Error("test"); + const spy = jest.fn(async () => { throw err; }); + + const promise = partial(spy)(); + + expect(spy).toHaveBeenCalledTimes(1); + expect(promise).toEqual(expect.any(Promise)); + + await expect(promise) + .rejects.toBe(err); + }); + +}); diff --git a/tests/partialOn.spec.ts b/tests/partialOn.spec.ts new file mode 100644 index 0000000..6c5cc84 --- /dev/null +++ b/tests/partialOn.spec.ts @@ -0,0 +1,79 @@ +import partialOn from "../lib/partialOn"; + +describe("promizr.partialOn()", () => { + const owner = {}; + + test("should create a function which combine task with given arguments", async () => { + const spy = jest.fn((arg: string, test: boolean, arg2: string, count: number) => `${arg}-${test}-${arg2}-${count}`); + const fn = partialOn(owner, spy, "arg", true); + + expect(spy).not.toHaveBeenCalled(); + + await fn("arg2", 2); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith("arg", true, "arg2", 2); + }); + + test("should use owner as this context when calling task", async () => { + const spy = jest.fn(function (this: unknown, arg: string, test: boolean, arg2: string, count: number): string { + expect(this).toBe(owner); + return `${arg}-${test}-${arg2}-${count}`; + }); + + await partialOn(owner, spy, "arg", true)("arg2", 2); + + expect(spy).toHaveBeenCalledTimes(1); + }); + + test("should resolve with the task result if sync", async () => { + const spy = jest.fn((arg: string, test: boolean, arg2: string, count: number) => `${arg}-${test}-${arg2}-${count}`); + + const promise = partialOn(owner, spy, "arg", true)("arg2", 2); + + expect(spy).toHaveBeenCalledTimes(1); + expect(promise).toEqual(expect.any(Promise)); + + await expect(promise) + .resolves.toBe("arg-true-arg2-2"); + }); + + test("should resolve with the task resolution if async", async () => { + const spy = jest.fn(async (arg: string, test: boolean, arg2: string, count: number) => `${arg}-${test}-${arg2}-${count}`); + + const promise = partialOn(owner, spy, "arg", true)("arg2", 2); + + expect(spy).toHaveBeenCalledTimes(1); + expect(promise).toEqual(expect.any(Promise)); + + await expect(promise) + .resolves.toBe("arg-true-arg2-2"); + }); + + test("should reject Promise if task throws", async () => { + const err = new Error("test"); + const spy = jest.fn(() => { throw err; }); + + const promise = partialOn(owner, spy)(); + + expect(spy).toHaveBeenCalledTimes(1); + expect(promise).toEqual(expect.any(Promise)); + + await expect(promise) + .rejects.toBe(err); + }); + + test("should reject Promise if task rejects", async () => { + const err = new Error("test"); + const spy = jest.fn(async () => { throw err; }); + + const promise = partialOn(owner, spy)(); + + expect(spy).toHaveBeenCalledTimes(1); + expect(promise).toEqual(expect.any(Promise)); + + await expect(promise) + .rejects.toBe(err); + }); + +}); diff --git a/tests/promisify.spec.ts b/tests/promisify.spec.ts new file mode 100644 index 0000000..5ed8ff3 --- /dev/null +++ b/tests/promisify.spec.ts @@ -0,0 +1,115 @@ +import promisify from "../lib/promisify"; + +describe("promizr.promisify()", () => { + + describe("with no owner", () => { + + test("should return a function that calls the inner function with a generated callback", async () => { + const spy = jest.fn(nodeStyleFunction); + + const fn = promisify(spy); + + expect(spy).not.toHaveBeenCalled(); + + await fn("result", false, null); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith("result", false, null, expect.any(Function)); + }); + + test("should return the result of the callback function", async () => { + const res = await promisify(nodeStyleFunction)("result", false, null); + expect(res).toBe("result"); + }); + + test("should return the result of the callback function", async () => { + const res = await promisify(nodeStyleFunction)("result", false, null); + expect(res).toBe("result"); + }); + + test("should return an array of the callback function results if it has multiple results", async () => { + const res = await promisify(nodeStyleFunction)("result", true, null); + expect(res).toEqual(["result", true]); + }); + + test("should return void if the callback function has no result", async () => { + const res = await promisify(nodeStyleFunction)(undefined, true, null); + expect(res).toBeUndefined(); + }); + + test("should throw if the callback function returns an error", async () => { + const err = new Error("test"); + + await expect(promisify(nodeStyleFunction)("result", false, err)) + .rejects.toBe(err); + }); + }); + + describe("with owner", () => { + const owner = {}; + + const thisNodeStyleFunction = function (this: unknown, ...args: Parameters): void { + expect(this).toBe(owner); + nodeStyleFunction(...args); + }; + + test("should return a function that calls the inner function with a generated callback", async () => { + const spy = jest.fn(thisNodeStyleFunction); + + const fn = promisify(owner, spy); + + expect(spy).not.toHaveBeenCalled(); + + await fn("result", false, null); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith("result", false, null, expect.any(Function)); + }); + + test("should return the result of the callback function", async () => { + const res = await promisify(owner, thisNodeStyleFunction)("result", false, null); + expect(res).toBe("result"); + }); + + test("should return the result of the callback function", async () => { + const res = await promisify(owner, thisNodeStyleFunction)("result", false, null); + expect(res).toBe("result"); + }); + + test("should return an array of the callback function results if it has multiple results", async () => { + const res = await promisify(owner, thisNodeStyleFunction)("result", true, null); + expect(res).toEqual(["result", true]); + }); + + test("should return void if the callback function has no result", async () => { + const res = await promisify(owner, thisNodeStyleFunction)(undefined, true, null); + expect(res).toBeUndefined(); + }); + + test("should throw if the callback function returns an error", async () => { + const err = new Error("test"); + + await expect(promisify(owner, thisNodeStyleFunction)("result", false, err)) + .rejects.toBe(err); + }); + }); + +}); + +function nodeStyleFunction(res: string | undefined, multi: boolean | null | undefined, throws: Error | null | undefined, cb: (err?: Error | null, ...args: any[]) => void): void { + if (throws) { + setImmediate(() => cb(throws)); + return; + } + + if (typeof res === "undefined") { + setImmediate(() => cb()); + } + + if (multi) { + setImmediate(() => cb(null, res, true)); + return; + } + + setImmediate(() => cb(null, res)); +} diff --git a/tests/promizr.spec.ts b/tests/promizr.spec.ts new file mode 100644 index 0000000..f9a082d --- /dev/null +++ b/tests/promizr.spec.ts @@ -0,0 +1,33 @@ +import * as path from "path"; +import { promises as fs } from "fs"; + +import * as promizr from "../"; + +const LIB_DIR = path.join(__dirname, "..", "lib"); + +describe("promizr", () => { + let MODULES: string[]; + + beforeAll(async () => { + const files = await fs.readdir(LIB_DIR); + MODULES = files.filter(f => !f.startsWith("_")); + }); + + test("should export all modules in lib", async () => { + for (const moduleFile of MODULES) { + const key = path.basename(moduleFile, ".ts"); + const module = await import(path.join(LIB_DIR, moduleFile)); + + expect(promizr).toHaveProperty(key, module.default); + } + }); + + test("should have a test for each module", async () => { + for (const moduleFile of MODULES) { + const key = path.basename(moduleFile, ".ts"); + const stat = await fs.stat(path.join(__dirname, `${key}.spec.ts`)); + expect(stat.isFile()).toBe(true); + } + }); + +}); diff --git a/tests/promizr/ProgressPromise.ts b/tests/promizr/ProgressPromise.ts deleted file mode 100644 index 2d04625..0000000 --- a/tests/promizr/ProgressPromise.ts +++ /dev/null @@ -1,182 +0,0 @@ -/// - -import * as sinon from "sinon"; -import * as promizr from "promizr"; -import ProgressContext = require("./helpers/ProgressContext"); - -describe("ProgressPromise class", () => { - - describe("contructor", () => { - - it("should call executor arguments with three functions", () => { - const - spy = sinon.spy(), - instance = new promizr.ProgressPromise(spy); - - sinon.assert.calledOnce(spy); - sinon.assert.calledWithMatch(spy, sinon.match.func, sinon.match.func, sinon.match.func); - }); - - it("should resolve when first function is called", done => { - let resolve: Function; - - const - arg = 1, - spy = sinon.spy(), - promise = new promizr.ProgressPromise(r => resolve = r).then(spy); - - sinon.assert.notCalled(spy); - - promise.then(() => { - sinon.assert.calledOnce(spy); - sinon.assert.calledWithExactly(spy, arg); - }).then(done, done); - - resolve(arg); - }); - - it("should reject when second function is called", done => { - let reject: Function; - - const - err = new Error("This is an error"), - spy = sinon.spy(), - promise = new promizr.ProgressPromise((r1, r2) => reject = r2).catch(spy); - - sinon.assert.notCalled(spy); - - promise.then(() => { - sinon.assert.calledOnce(spy); - sinon.assert.calledWithExactly(spy, err); - }).then(done, done); - - reject(err); - }); - - it("should reject if executor function throw an error", done => { - const - err = new Error("This is an error"), - spy = sinon.spy(), - promise = new promizr.ProgressPromise(() => { throw err; }).catch(spy); - - sinon.assert.notCalled(spy); - - promise.then(() => { - sinon.assert.calledOnce(spy); - sinon.assert.calledWithExactly(spy, err); - }).then(done, done); - }); - - }); - - describe("static methods", () => { - - describe("all", () => { - - it("should return an instance of ProgressPromise", () => { - const result = promizr.ProgressPromise.all([]); - result.should.be.instanceOf(promizr.ProgressPromise); - }); - - it("should report progresses of each inner ProgressPromises", () => { - const - spy = sinon.spy(), - ctx = new ProgressContext(3), - promise = promizr.ProgressPromise.all(ctx.promises()).progress(spy); - - ctx.progress(0, 1); - - sinon.assert.calledOnce(spy); - sinon.assert.calledWithMatch(spy, sinon.match([1, undefined, undefined])); - - ctx.progress(1, 1); - - sinon.assert.calledTwice(spy); - sinon.assert.calledWithMatch(spy, sinon.match([1, 1, undefined])); - - ctx.progress(2, 1); - - sinon.assert.calledThrice(spy); - sinon.assert.calledWithMatch(spy, sinon.match([1, 1, 1])); - }); - - it("should report progresses of each inner ProgressPromises and undefined for others", () => { - const - spy = sinon.spy(), - ctx = new ProgressContext(3).addFake(), - promise = promizr.ProgressPromise.all(ctx.promises()).progress(spy); - - ctx.progress(0, 1); - - sinon.assert.calledOnce(spy); - sinon.assert.calledWithMatch(spy, sinon.match([1, undefined, undefined, undefined])); - - ctx.progress(1, 1); - - sinon.assert.calledTwice(spy); - sinon.assert.calledWithMatch(spy, sinon.match([1, 1, undefined, undefined])); - - ctx.progress(2, 1); - - sinon.assert.calledThrice(spy); - sinon.assert.calledWithMatch(spy, sinon.match([1, 1, 1, undefined])); - }); - - }); - - describe("race", () => { - - it("should return an instance of ProgressPromise", () => { - const result = promizr.ProgressPromise.race([]); - result.should.be.instanceOf(promizr.ProgressPromise); - }); - - it("should report progresses of each inner ProgressPromises", () => { - const - spy = sinon.spy(), - ctx = new ProgressContext(3), - promise = promizr.ProgressPromise.race(ctx.promises()).progress(spy); - - ctx.progress(0, 1); - - sinon.assert.calledOnce(spy); - sinon.assert.calledWithMatch(spy, sinon.match([1, undefined, undefined])); - - ctx.progress(1, 1); - - sinon.assert.calledTwice(spy); - sinon.assert.calledWithMatch(spy, sinon.match([1, 1, undefined])); - - ctx.progress(2, 1); - - sinon.assert.calledThrice(spy); - sinon.assert.calledWithMatch(spy, sinon.match([1, 1, 1])); - }); - - it("should report progresses of each inner ProgressPromises and undefined for others", () => { - const - spy = sinon.spy(), - ctx = new ProgressContext(3).addFake(), - promise = promizr.ProgressPromise.race(ctx.promises()).progress(spy); - - ctx.progress(0, 1); - - sinon.assert.calledOnce(spy); - sinon.assert.calledWithMatch(spy, sinon.match([1, undefined, undefined, undefined])); - - ctx.progress(1, 1); - - sinon.assert.calledTwice(spy); - sinon.assert.calledWithMatch(spy, sinon.match([1, 1, undefined, undefined])); - - ctx.progress(2, 1); - - sinon.assert.calledThrice(spy); - sinon.assert.calledWithMatch(spy, sinon.match([1, 1, 1, undefined])); - }); - - }); - - }); - -}); diff --git a/tests/promizr/collections.ts b/tests/promizr/collections.ts deleted file mode 100644 index eaa2e3c..0000000 --- a/tests/promizr/collections.ts +++ /dev/null @@ -1,1052 +0,0 @@ -/// - -import * as sinon from "sinon"; -import * as promizr from "promizr"; -import * as common from "./helpers/common"; - -var list = [15, 1, 8], - stopList = [251, 1, 50], - - reduceTotal = list[0] + list[1] + list[2] + 1, - reduceResult = 1; - -describe("Promizr Collections Methods", () => { - - describe("each", () => { - - describe("parallel function", () => { - - it("should call each provided methods with given values in parallel", done => { - var spy = sinon.spy(), - iterator = num => promizr.timeout(num).then(() => spy(num)); - - promizr.each(list, iterator).then(() => { - sinon.assert.calledThrice(spy); - - spy.getCall(0).args[0].should.equal(list[1]); - spy.getCall(1).args[0].should.equal(list[2]); - spy.getCall(2).args[0].should.equal(list[0]); - }).then(done, done); - }); - - it("should stop if an iterator throws", done => { - var spy = sinon.spy(), - err = new Error("test"), - - iterator = sinon.spy(num => { - return promizr.timeout(num).then(() => { - if (num === stopList[2]) { - return Promise.reject(err); - } - - spy(num); - }); - }); - - promizr.each(stopList, iterator).catch(e => { - e.should.equal(err); - - sinon.assert.calledOnce(spy); - sinon.assert.calledWithExactly(spy, stopList[1]); - - sinon.assert.calledThrice(iterator); - sinon.assert.calledWithExactly(iterator, stopList[0], 0, stopList); - sinon.assert.calledWithExactly(iterator, stopList[1], 1, stopList); - sinon.assert.calledWithExactly(iterator, stopList[2], 2, stopList); - }).then(done, done); - }); - - }); - - describe("series function", () => { - - it("should call each provided methods with given values in series", done => { - var spy = sinon.spy(), - iterator = num => promizr.timeout(num).then(() => spy(num)); - - promizr.eachSeries(list, iterator).then(() => { - sinon.assert.calledThrice(spy); - - spy.getCall(0).args[0].should.equal(list[0]); - spy.getCall(1).args[0].should.equal(list[1]); - spy.getCall(2).args[0].should.equal(list[2]); - }).then(done, done); - }); - - it("should stop if an iterator throws", done => { - var spy = sinon.spy(), - err = new Error("test"), - - iterator = sinon.spy(num => { - if (num === list[1]) { - return Promise.reject(err); - } - - return promizr.timeout(num).then(() => spy(num)); - }); - - promizr.eachSeries(list, iterator).catch(e => { - e.should.equal(err); - - sinon.assert.calledOnce(spy); - sinon.assert.calledWithExactly(spy, list[0]); - - sinon.assert.calledTwice(iterator); - sinon.assert.calledWithExactly(iterator, list[0], 0, list); - sinon.assert.calledWithExactly(iterator, list[1], 1, list); - }).then(done, done); - }); - - }); - - }); - - describe("map", () => { - - describe("parallel function", () => { - - it("should call each provided methods with given values in parallel", done => { - var spy = sinon.spy(num => num * num), - iterator = num => promizr.timeout(num).then(() => spy(num)); - - promizr.map(list, iterator).then(results => { - sinon.assert.calledThrice(spy); - - spy.getCall(0).args[0].should.equal(list[1]); - spy.getCall(1).args[0].should.equal(list[2]); - spy.getCall(2).args[0].should.equal(list[0]); - }).then(done, done); - }); - - it("should return mapped array with each values applied by iterator", done => { - var spy = sinon.spy(num => num * num), - iterator = num => promizr.timeout(num).then(() => spy(num)); - - promizr.map(list, iterator).then(results => { - sinon.assert.calledThrice(spy); - - results.length.should.equal(3); - results[0].should.equal(list[0] * list[0]); - results[1].should.equal(list[1] * list[1]); - results[2].should.equal(list[2] * list[2]); - }).then(done, done); - }); - - it("should stop if an iterator throws", done => { - var spy = sinon.spy(num => num * num), - err = new Error("test"), - - iterator = sinon.spy(num => { - return promizr.timeout(num).then(() => { - if (num === stopList[2]) { - return Promise.reject(err); - } - - spy(num); - }); - }); - - promizr.map(stopList, iterator).catch(e => { - e.should.equal(err); - - sinon.assert.calledOnce(spy); - sinon.assert.calledWithExactly(spy, stopList[1]); - - sinon.assert.calledThrice(iterator); - sinon.assert.calledWithExactly(iterator, stopList[0], 0, stopList); - sinon.assert.calledWithExactly(iterator, stopList[1], 1, stopList); - sinon.assert.calledWithExactly(iterator, stopList[2], 2, stopList); - }).then(done, done); - }); - - }); - - describe("series function", () => { - - it("should call each provided methods with given values in series", done => { - var spy = sinon.spy(num => num * num), - iterator = num => promizr.timeout(num).then(() => spy(num)); - - promizr.mapSeries(list, iterator).then(() => { - sinon.assert.calledThrice(spy); - - spy.getCall(0).args[0].should.equal(list[0]); - spy.getCall(1).args[0].should.equal(list[1]); - spy.getCall(2).args[0].should.equal(list[2]); - }).then(done, done); - }); - - it("should return mapped array with each values applied by iterator", done => { - var spy = sinon.spy(num => num * num), - iterator = num => promizr.timeout(num).then(() => spy(num)); - - promizr.mapSeries(list, iterator).then(results => { - sinon.assert.calledThrice(spy); - - results.length.should.equal(3); - results[0].should.equal(list[0] * list[0]); - results[1].should.equal(list[1] * list[1]); - results[2].should.equal(list[2] * list[2]); - }).then(done, done); - }); - - it("should stop if an iterator throws", done => { - var spy = sinon.spy(num => num * num), - err = new Error("test"), - iterator = sinon.spy(num => { - if (num === list[1]) { - return Promise.reject(err); - } - - return promizr.timeout(num).then(() => spy(num)); - }); - - promizr.mapSeries(list, iterator).catch(e => { - e.should.equal(err); - - sinon.assert.calledOnce(spy); - sinon.assert.calledWithExactly(spy, list[0]); - - sinon.assert.calledTwice(iterator); - sinon.assert.calledWithExactly(iterator, list[0], 0, list); - sinon.assert.calledWithExactly(iterator, list[1], 1, list); - }).then(done, done); - }); - - }); - - }); - - describe("filter", () => { - - describe("parallel function", () => { - - it("should call each provided methods with given values in parallel", done => { - var spy = sinon.spy(num => num % 2 === 1), - iterator = num => promizr.timeout(num).then(() => spy(num)); - - promizr.filter(list, iterator).then(results => { - sinon.assert.calledThrice(spy); - - spy.getCall(0).args[0].should.equal(list[1]); - spy.getCall(1).args[0].should.equal(list[2]); - spy.getCall(2).args[0].should.equal(list[0]); - }).then(done, done); - }); - - it("should return array with given values filtered by iterator", done => { - var spy = sinon.spy(num => num % 2 === 1), - iterator = num => promizr.timeout(num).then(() => spy(num)); - - promizr.filter(list, iterator).then(results => { - sinon.assert.calledThrice(spy); - - results.length.should.equal(2); - results[0].should.equal(list[1]); - results[1].should.equal(list[0]); - }).then(done, done); - }); - - it("should stop if an iterator throws", done => { - var spy = sinon.spy(num => num % 2 === 1), - err = new Error("test"), - - iterator = sinon.spy(num => { - return promizr.timeout(num).then(() => { - if (num === stopList[2]) { - return Promise.reject(err); - } - - spy(num); - }); - }); - - promizr.filter(stopList, iterator).catch(e => { - e.should.equal(err); - - sinon.assert.calledOnce(spy); - sinon.assert.calledWithExactly(spy, stopList[1]); - - sinon.assert.calledThrice(iterator); - sinon.assert.calledWithExactly(iterator, stopList[0], 0, stopList); - sinon.assert.calledWithExactly(iterator, stopList[1], 1, stopList); - sinon.assert.calledWithExactly(iterator, stopList[2], 2, stopList); - }).then(done, done); - }); - - }); - - describe("series function", () => { - - it("should call each provided methods with given values in series", done => { - var spy = sinon.spy(num => num % 2 === 1), - iterator = num => promizr.timeout(num).then(() => spy(num)); - - promizr.filterSeries(list, iterator).then(() => { - sinon.assert.calledThrice(spy); - - spy.getCall(0).args[0].should.equal(list[0]); - spy.getCall(1).args[0].should.equal(list[1]); - spy.getCall(2).args[0].should.equal(list[2]); - }).then(done, done); - }); - - it("should return array with given values filtered by iterator", done => { - var spy = sinon.spy(num => num % 2 === 1), - iterator = num => promizr.timeout(num).then(() => spy(num)); - - promizr.filterSeries(list, iterator).then(results => { - sinon.assert.calledThrice(spy); - - results.length.should.equal(2); - results[0].should.equal(list[0]); - results[1].should.equal(list[1]); - }).then(done, done); - }); - - it("should stop if an iterator throws", done => { - var spy = sinon.spy(num => num % 2 === 1), - err = new Error("test"), - iterator = sinon.spy(num => { - if (num === list[1]) { - return Promise.reject(err); - } - - return promizr.timeout(num).then(() => spy(num)); - }); - - promizr.filterSeries(list, iterator).catch(e => { - e.should.equal(err); - - sinon.assert.calledOnce(spy); - sinon.assert.calledWithExactly(spy, list[0]); - - sinon.assert.calledTwice(iterator); - sinon.assert.calledWithExactly(iterator, list[0], 0, list); - sinon.assert.calledWithExactly(iterator, list[1], 1, list); - }).then(done, done); - }); - - }); - - }); - - describe("reject", () => { - - describe("parallel function", () => { - - it("should call each provided methods with given values in parallel", done => { - var spy = sinon.spy(num => num % 2 === 0), - iterator = num => promizr.timeout(num).then(() => spy(num)); - - promizr.reject(list, iterator).then(results => { - sinon.assert.calledThrice(spy); - - spy.getCall(0).args[0].should.equal(list[1]); - spy.getCall(1).args[0].should.equal(list[2]); - spy.getCall(2).args[0].should.equal(list[0]); - }).then(done, done); - }); - - it("should return array with given values filtered by iterator", done => { - var spy = sinon.spy(num => num % 2 === 0), - iterator = num => promizr.timeout(num).then(() => spy(num)); - - promizr.reject(list, iterator).then(results => { - sinon.assert.calledThrice(spy); - - results.length.should.equal(2); - results[0].should.equal(list[1]); - results[1].should.equal(list[0]); - }).then(done, done); - }); - - it("should stop if an iterator throws", done => { - var spy = sinon.spy(num => num % 2 === 0), - err = new Error("test"), - - iterator = sinon.spy(num => { - return promizr.timeout(num).then(() => { - if (num === stopList[2]) { - return Promise.reject(err); - } - - spy(num); - }); - }); - - promizr.reject(stopList, iterator).catch(e => { - e.should.equal(err); - - sinon.assert.calledOnce(spy); - sinon.assert.calledWithExactly(spy, stopList[1]); - - sinon.assert.calledThrice(iterator); - sinon.assert.calledWithExactly(iterator, stopList[0], 0, stopList); - sinon.assert.calledWithExactly(iterator, stopList[1], 1, stopList); - sinon.assert.calledWithExactly(iterator, stopList[2], 2, stopList); - }).then(done, done); - }); - - }); - - describe("series function", () => { - - it("should call each provided methods with given values in series", done => { - var spy = sinon.spy(num => num % 2 === 0), - iterator = num => promizr.timeout(num).then(() => spy(num)); - - promizr.rejectSeries(list, iterator).then(() => { - sinon.assert.calledThrice(spy); - - spy.getCall(0).args[0].should.equal(list[0]); - spy.getCall(1).args[0].should.equal(list[1]); - spy.getCall(2).args[0].should.equal(list[2]); - }).then(done, done); - }); - - it("should return array with given values filtered by iterator", done => { - var spy = sinon.spy(num => num % 2 === 0), - iterator = num => promizr.timeout(num).then(() => spy(num)); - - promizr.rejectSeries(list, iterator).then(results => { - sinon.assert.calledThrice(spy); - - results.length.should.equal(2); - results[0].should.equal(list[0]); - results[1].should.equal(list[1]); - }).then(done, done); - }); - - it("should stop if an iterator throws", done => { - var spy = sinon.spy(num => num % 2 === 0), - err = new Error("test"), - iterator = sinon.spy(num => { - if (num === list[1]) { - return Promise.reject(err); - } - - return promizr.timeout(num).then(() => spy(num)); - }); - - promizr.rejectSeries(list, iterator).catch(e => { - e.should.equal(err); - - sinon.assert.calledOnce(spy); - sinon.assert.calledWithExactly(spy, list[0]); - - sinon.assert.calledTwice(iterator); - sinon.assert.calledWithExactly(iterator, list[0], 0, list); - sinon.assert.calledWithExactly(iterator, list[1], 1, list); - }).then(done, done); - }); - - }); - - }); - - describe("reduce", () => { - - describe("left function", () => { - - it("should call each provided methods with given values in series", done => { - var spy = sinon.spy((a, b) => a - b), - iterator = (a, b) => promizr.timeout(b).then(() => spy(a, b)); - - promizr.reduce(list, reduceTotal, iterator).then(result => { - sinon.assert.calledThrice(spy); - - spy.getCall(0).args[0].should.equal(reduceTotal); - spy.getCall(0).args[1].should.equal(list[0]); - - spy.getCall(1).args[0].should.equal(reduceTotal - list[0]); - spy.getCall(1).args[1].should.equal(list[1]); - - spy.getCall(2).args[0].should.equal(reduceTotal - list[0] - list[1]); - spy.getCall(2).args[1].should.equal(list[2]); - }).then(done, done); - }); - - it("should return reduced value using iterator", done => { - var spy = sinon.spy((a, b) => a - b), - iterator = (a: number, b: number) => promizr.timeout(b).then(() => spy(a, b)); - - promizr.reduce(list, reduceTotal, iterator).then(result => { - sinon.assert.calledThrice(spy); - - result.should.equal(reduceResult); - }).then(done, done); - }); - - it("should stop if an iterator throws", done => { - var spy = sinon.spy((a, b) => a - b), - err = new Error("test"), - - iterator = sinon.spy((a, b) => { - if (b === list[1]) { - return Promise.reject(err); - } - - return promizr.timeout(b).then(() => spy(a, b)); - }); - - promizr.reduce(list, reduceTotal, iterator).catch(e => { - e.should.equal(err); - - sinon.assert.calledOnce(spy); - sinon.assert.calledWithExactly(spy, reduceTotal, list[0]); - - sinon.assert.calledTwice(iterator); - sinon.assert.calledWithExactly(iterator, reduceTotal, list[0]); - sinon.assert.calledWithExactly(iterator, reduceTotal - list[0], list[1]); - }).then(done, done); - }); - - }); - - describe("right function", () => { - - it("should call each provided methods with given values in reverse order series", done => { - var spy = sinon.spy((a, b) => a - b), - iterator = (a, b) => promizr.timeout(b).then(() => spy(a, b)); - - promizr.reduceRight(list, reduceTotal, iterator).then(result => { - sinon.assert.calledThrice(spy); - - spy.getCall(0).args[0].should.equal(reduceTotal); - spy.getCall(0).args[1].should.equal(list[2]); - - spy.getCall(1).args[0].should.equal(reduceTotal - list[2]); - spy.getCall(1).args[1].should.equal(list[1]); - - spy.getCall(2).args[0].should.equal(reduceTotal - list[2] - list[1]); - spy.getCall(2).args[1].should.equal(list[0]); - }).then(done, done); - }); - - it("should return reduced value using iterator", done => { - var spy = sinon.spy((a, b) => a - b), - iterator = (a: number, b: number) => promizr.timeout(b).then(() => spy(a, b)); - - promizr.reduceRight(list, reduceTotal, iterator).then(result => { - sinon.assert.calledThrice(spy); - - result.should.equal(reduceResult); - }).then(done, done); - }); - - it("should stop if an iterator throws", done => { - var spy = sinon.spy((a, b) => a - b), - err = new Error("test"), - - iterator = sinon.spy((a, b) => { - if (b === list[1]) { - return Promise.reject(err); - } - - return promizr.timeout(b).then(() => spy(a, b)); - }); - - promizr.reduceRight(list, reduceTotal, iterator).catch(e => { - e.should.equal(err); - - sinon.assert.calledOnce(spy); - sinon.assert.calledWithExactly(spy, reduceTotal, list[2]); - - sinon.assert.calledTwice(iterator); - sinon.assert.calledWithExactly(iterator, reduceTotal, list[2]); - sinon.assert.calledWithExactly(iterator, reduceTotal - list[2], list[1]); - }).then(done, done); - }); - - }); - - }); - - describe("find", () => { - - describe("parallel function", () => { - - it("should call each provided methods with given values in parallel", done => { - var spy = sinon.spy(num => false), - iterator = num => promizr.timeout(num).then(() => spy(num)); - - promizr.find(list, iterator).then(results => { - sinon.assert.calledThrice(spy); - - spy.getCall(0).args[0].should.equal(list[1]); - spy.getCall(1).args[0].should.equal(list[2]); - spy.getCall(2).args[0].should.equal(list[0]); - }).then(done, done); - }); - - it("should return first found result filtered by iterator", done => { - var spy = sinon.spy(num => num % 2 === 1), - iterator = num => promizr.timeout(num).then(() => spy(num)); - - promizr.find(list, iterator).then(result => { - result.should.equal(list[1]); - }).then(done, done); - }); - - it("should return undefined if no result found", done => { - var spy = sinon.spy(num => false), - iterator = num => promizr.timeout(num).then(() => spy(num)); - - promizr.find(list, iterator).then(result => { - sinon.assert.calledThrice(spy); - - (typeof result).should.equal("undefined"); - }).then(done, done); - }); - - it("should stop if an item is found", done => { - var spy = sinon.spy(num => num % 2 === 1), - iterator = sinon.spy(num => promizr.timeout(num).then(() => spy(num))); - - promizr.find(stopList, iterator).then(e => { - sinon.assert.calledOnce(spy); - sinon.assert.calledWithExactly(spy, stopList[1]); - - sinon.assert.calledThrice(iterator); - sinon.assert.calledWithExactly(iterator, stopList[0], 0, stopList); - sinon.assert.calledWithExactly(iterator, stopList[1], 1, stopList); - sinon.assert.calledWithExactly(iterator, stopList[2], 2, stopList); - }).then(done, done); - }); - - it("should stop if an iterator throws", done => { - var spy = sinon.spy(num => false), - err = new Error("test"), - - iterator = sinon.spy(num => { - return promizr.timeout(num).then(() => { - if (num === stopList[2]) { - return Promise.reject(err); - } - - spy(num); - }); - }); - - promizr.find(stopList, iterator).catch(e => { - e.should.equal(err); - - sinon.assert.calledOnce(spy); - sinon.assert.calledWithExactly(spy, stopList[1]); - - sinon.assert.calledThrice(iterator); - sinon.assert.calledWithExactly(iterator, stopList[0], 0, stopList); - sinon.assert.calledWithExactly(iterator, stopList[1], 1, stopList); - sinon.assert.calledWithExactly(iterator, stopList[2], 2, stopList); - }).then(done, done); - }); - - }); - - describe("series function", () => { - - it("should call each provided methods with given values in series", done => { - var spy = sinon.spy(num => false), - iterator = num => promizr.timeout(num).then(() => spy(num)); - - promizr.findSeries(list, iterator).then(() => { - sinon.assert.calledThrice(spy); - - spy.getCall(0).args[0].should.equal(list[0]); - spy.getCall(1).args[0].should.equal(list[1]); - spy.getCall(2).args[0].should.equal(list[2]); - }).then(done, done); - }); - - it("should return first found result filtered by iterator", done => { - var spy = sinon.spy(num => num % 2 === 1), - iterator = num => promizr.timeout(num).then(() => spy(num)); - - promizr.findSeries(list, iterator).then(result => { - result.should.equal(list[0]); - }).then(done, done); - }); - - it("should return undefined if no result found", done => { - var spy = sinon.spy(num => false), - iterator = num => promizr.timeout(num).then(() => spy(num)); - - promizr.findSeries(list, iterator).then(result => { - sinon.assert.calledThrice(spy); - - (typeof result).should.equal("undefined"); - }).then(done, done); - }); - - it("should stop if an item is found", done => { - var spy = sinon.spy(num => num % 2 === 1), - iterator = sinon.spy(num => promizr.timeout(num).then(() => spy(num))); - - promizr.findSeries(list, iterator).then(e => { - sinon.assert.calledOnce(spy); - sinon.assert.calledWithExactly(spy, list[0]); - - sinon.assert.calledOnce(iterator); - sinon.assert.calledWithExactly(iterator, list[0], 0, list); - }).then(done, done); - }); - - it("should stop if an iterator throws", done => { - var spy = sinon.spy(num => false), - err = new Error("test"), - iterator = sinon.spy(num => { - if (num === list[1]) { - return Promise.reject(err); - } - - return promizr.timeout(num).then(() => spy(num)); - }); - - promizr.findSeries(list, iterator).catch(e => { - e.should.equal(err); - - sinon.assert.calledOnce(spy); - sinon.assert.calledWithExactly(spy, list[0]); - - sinon.assert.calledTwice(iterator); - sinon.assert.calledWithExactly(iterator, list[0], 0, list); - sinon.assert.calledWithExactly(iterator, list[1], 1, list); - }).then(done, done); - }); - - }); - - }); - - describe("sortBy", () => { - - it("should call each provided methods with given values in parallel", done => { - var spy = sinon.spy(num => num), - iterator = num => promizr.timeout(num).then(() => spy(num)); - - promizr.sortBy(list, iterator).then(results => { - sinon.assert.calledThrice(spy); - - spy.getCall(0).args[0].should.equal(list[1]); - spy.getCall(1).args[0].should.equal(list[2]); - spy.getCall(2).args[0].should.equal(list[0]); - }).then(done, done); - }); - - it("should return sorted array using iterator projection", done => { - var spy = sinon.spy(num => num), - iterator = num => promizr.timeout(num).then(() => spy(num)); - - promizr.sortBy(list, iterator).then(results => { - sinon.assert.calledThrice(spy); - - results.length.should.equal(3); - results[0].should.equal(list[1]); - results[1].should.equal(list[2]); - results[2].should.equal(list[0]); - }).then(done, done); - }); - - it("should stop if an iterator throws", done => { - var spy = sinon.spy(num => num * num), - err = new Error("test"), - iterator = sinon.spy(num => { - return promizr.timeout(num).then(() => { - if (num === stopList[2]) { - return Promise.reject(err); - } - - spy(num); - }); - }); - - promizr.sortBy(stopList, iterator).catch(e => { - e.should.equal(err); - - sinon.assert.calledOnce(spy); - sinon.assert.calledWithExactly(spy, stopList[1]); - - sinon.assert.calledThrice(iterator); - sinon.assert.calledWithExactly(iterator, stopList[0], 0, stopList); - sinon.assert.calledWithExactly(iterator, stopList[1], 1, stopList); - sinon.assert.calledWithExactly(iterator, stopList[2], 2, stopList); - }).then(done, done); - }); - - }); - - describe("some", () => { - - it("should call each provided methods with given values in parallel", done => { - var spy = sinon.spy(num => false), - iterator = num => promizr.timeout(num).then(() => spy(num)); - - promizr.some(list, iterator).then(results => { - sinon.assert.calledThrice(spy); - - spy.getCall(0).args[0].should.equal(list[1]); - spy.getCall(1).args[0].should.equal(list[2]); - spy.getCall(2).args[0].should.equal(list[0]); - }).then(done, done); - }); - - it("should return true if any result is found", done => { - var spy = sinon.spy(num => num % 2 === 1), - iterator = num => promizr.timeout(num).then(() => spy(num)); - - promizr.some(list, iterator).then(result => { - result.should.be.ok; - }).then(done, done); - }); - - it("should return false if no result is found", done => { - var spy = sinon.spy(num => false), - iterator = num => promizr.timeout(num).then(() => spy(num)); - - promizr.some(list, iterator).then(result => { - sinon.assert.calledThrice(spy); - result.should.not.be.ok; - }).then(done, done); - }); - - it("should stop if an item is found", done => { - var spy = sinon.spy(num => num % 2 === 1), - iterator = sinon.spy(num => promizr.timeout(num).then(() => spy(num))); - - promizr.some(stopList, iterator).then(e => { - sinon.assert.calledOnce(spy); - sinon.assert.calledWithExactly(spy, stopList[1]); - - sinon.assert.calledThrice(iterator); - sinon.assert.calledWithExactly(iterator, stopList[0], 0, stopList); - sinon.assert.calledWithExactly(iterator, stopList[1], 1, stopList); - sinon.assert.calledWithExactly(iterator, stopList[2], 2, stopList); - }).then(done, done); - }); - - it("should stop if an iterator throws", done => { - var spy = sinon.spy(num => false), - err = new Error("test"), - - iterator = sinon.spy(num => { - return promizr.timeout(num).then(() => { - if (num === stopList[2]) { - return Promise.reject(err); - } - - spy(num); - }); - }); - - promizr.some(stopList, iterator).catch(e => { - e.should.equal(err); - - sinon.assert.calledOnce(spy); - sinon.assert.calledWithExactly(spy, stopList[1]); - - sinon.assert.calledThrice(iterator); - sinon.assert.calledWithExactly(iterator, stopList[0], 0, stopList); - sinon.assert.calledWithExactly(iterator, stopList[1], 1, stopList); - sinon.assert.calledWithExactly(iterator, stopList[2], 2, stopList); - }).then(done, done); - }); - - }); - - describe("every", () => { - - it("should call each provided methods with given values in parallel", done => { - var spy = sinon.spy(num => true), - iterator = num => promizr.timeout(num).then(() => spy(num)); - - promizr.every(list, iterator).then(results => { - sinon.assert.calledThrice(spy); - - spy.getCall(0).args[0].should.equal(list[1]); - spy.getCall(1).args[0].should.equal(list[2]); - spy.getCall(2).args[0].should.equal(list[0]); - }).then(done, done); - }); - - it("should return true if all items match given iterator", done => { - var spy = sinon.spy(num => true), - iterator = num => promizr.timeout(num).then(() => spy(num)); - - promizr.every(list, iterator).then(result => { - sinon.assert.calledThrice(spy); - result.should.be.ok; - }).then(done, done); - }); - - it("should return false if any item does not match given iterator", done => { - var spy = sinon.spy(num => num % 2 === 0), - iterator = num => promizr.timeout(num).then(() => spy(num)); - - promizr.every(stopList, iterator).then(result => { - sinon.assert.calledOnce(spy); - result.should.not.be.ok; - }).then(done, done); - }); - - it("should stop if any item does not match given iterator", done => { - var spy = sinon.spy(num => num % 2 === 0), - iterator = sinon.spy(num => promizr.timeout(num).then(() => spy(num))); - - promizr.every(stopList, iterator).then(e => { - sinon.assert.calledOnce(spy); - sinon.assert.calledWithExactly(spy, stopList[1]); - - sinon.assert.calledThrice(iterator); - sinon.assert.calledWithExactly(iterator, stopList[0], 0, stopList); - sinon.assert.calledWithExactly(iterator, stopList[1], 1, stopList); - sinon.assert.calledWithExactly(iterator, stopList[2], 2, stopList); - }).then(done, done); - }); - - it("should stop if an iterator throws", done => { - var spy = sinon.spy(num => true), - err = new Error("test"), - - iterator = sinon.spy(num => { - return promizr.timeout(num).then(() => { - if (num === stopList[2]) { - return Promise.reject(err); - } - - return spy(num); - }); - }); - - promizr.every(stopList, iterator).catch(e => { - e.should.equal(err); - - sinon.assert.calledOnce(spy); - sinon.assert.calledWithExactly(spy, stopList[1]); - - sinon.assert.calledThrice(iterator); - sinon.assert.calledWithExactly(iterator, stopList[0], 0, stopList); - sinon.assert.calledWithExactly(iterator, stopList[1], 1, stopList); - sinon.assert.calledWithExactly(iterator, stopList[2], 2, stopList); - }).then(done, done); - }); - - }); - - describe("concat", () => { - - describe("parallel function", () => { - - it("should call each provided methods with given values in parallel", done => { - var spy = sinon.spy(num => [num]), - iterator = num => promizr.timeout(num).then(() => spy(num)); - - promizr.concat(list, iterator).then(results => { - sinon.assert.calledThrice(spy); - - spy.getCall(0).args[0].should.equal(list[1]); - spy.getCall(1).args[0].should.equal(list[2]); - spy.getCall(2).args[0].should.equal(list[0]); - }).then(done, done); - }); - - it("should return flattened array with arrays returned by iterator", done => { - var spy = sinon.spy(num => [num]), - iterator = num => promizr.timeout(num).then(() => spy(num)); - - promizr.concat(list, iterator).then(results => { - sinon.assert.calledThrice(spy); - - results.length.should.equal(3); - results[0].should.equal(list[0]); - results[1].should.equal(list[1]); - results[2].should.equal(list[2]); - }).then(done, done); - }); - - it("should stop if an iterator throws", done => { - var spy = sinon.spy(num => [num]), - err = new Error("test"), - - iterator = sinon.spy(num => { - return promizr.timeout(num).then(() => { - if (num === stopList[2]) { - return Promise.reject(err); - } - - spy(num); - }); - }); - - promizr.concat(stopList, iterator).catch(e => { - e.should.equal(err); - - sinon.assert.calledOnce(spy); - sinon.assert.calledWithExactly(spy, stopList[1]); - - sinon.assert.calledThrice(iterator); - sinon.assert.calledWithExactly(iterator, stopList[0], 0, stopList); - sinon.assert.calledWithExactly(iterator, stopList[1], 1, stopList); - sinon.assert.calledWithExactly(iterator, stopList[2], 2, stopList); - }).then(done, done); - }); - - }); - - describe("series function", () => { - - it("should call each provided methods with given values in series", done => { - var spy = sinon.spy(num => [num]), - iterator = num => promizr.timeout(num).then(() => spy(num)); - - promizr.concatSeries(list, iterator).then(() => { - sinon.assert.calledThrice(spy); - - spy.getCall(0).args[0].should.equal(list[0]); - spy.getCall(1).args[0].should.equal(list[1]); - spy.getCall(2).args[0].should.equal(list[2]); - }).then(done, done); - }); - - it("should return array with given values filtered by iterator", done => { - var spy = sinon.spy(num => [num]), - iterator = num => promizr.timeout(num).then(() => spy(num)); - - promizr.concatSeries(list, iterator).then(results => { - sinon.assert.calledThrice(spy); - - results.length.should.equal(3); - results[0].should.equal(list[0]); - results[1].should.equal(list[1]); - results[2].should.equal(list[2]); - }).then(done, done); - }); - - it("should stop if an iterator throws", done => { - var spy = sinon.spy(num => [num]), - err = new Error("test"), - iterator = sinon.spy(num => { - if (num === list[1]) { - return Promise.reject(err); - } - - return promizr.timeout(num).then(() => spy(num)); - }); - - promizr.concatSeries(list, iterator).catch(e => { - e.should.equal(err); - - sinon.assert.calledOnce(spy); - sinon.assert.calledWithExactly(spy, list[0]); - - sinon.assert.calledTwice(iterator); - sinon.assert.calledWithExactly(iterator, list[0], 0, list); - sinon.assert.calledWithExactly(iterator, list[1], 1, list); - }).then(done, done); - }); - - }); - - }); - -}); diff --git a/tests/promizr/config.ts b/tests/promizr/config.ts deleted file mode 100644 index c92f799..0000000 --- a/tests/promizr/config.ts +++ /dev/null @@ -1,37 +0,0 @@ -/// - -requirejs.config({ - //baseUrl: "../", - - paths: { - "promise": "../../dist/polyfill", - "promizr": "../../dist/promizr", - "mocha": "../../bower_components/mocha/mocha", - "should": "../../bower_components/should/should", - "sinon": "../../bower_components/sinon/index" - }, - - shim: { - mocha: { - exports: "mocha" - } - } -}); - -(window).root = window; - -(window).console = window.console || function () { return; }; -(window).notrack = true; - -var tests = [ - "collections", - "flow", - //"nextTick", - "ProgressPromise", - "queue", - //"utils" -]; - -require(tests, function () { - mocha.run(); -}); diff --git a/tests/promizr/flow.ts b/tests/promizr/flow.ts deleted file mode 100644 index 2addce2..0000000 --- a/tests/promizr/flow.ts +++ /dev/null @@ -1,1091 +0,0 @@ -/// - -import * as sinon from "sinon"; -import * as promizr from "promizr"; -import * as common from "./helpers/common"; - -var list = [15, 1, 8], - stopList = [251, 1, 50]; - -describe("Promizr Flow Methods", () => { - - describe("series function", () => { - - describe("whith a list argument", () => { - - it("should call each provided methods with given values in series", done => { - var spy = sinon.spy(), - executors = list.map(num => () => promizr.timeout(num).then(() => spy(num))); - - promizr.series(executors).then(() => { - sinon.assert.calledThrice(spy); - - spy.getCall(0).args[0].should.equal(list[0]); - spy.getCall(1).args[0].should.equal(list[1]); - spy.getCall(2).args[0].should.equal(list[2]); - }).then(done, done); - }); - - it("should return an array with each values returned by executors", done => { - var spy = sinon.spy(num => num * num), - executors = list.map(num => () => promizr.timeout(num).then(() => spy(num))); - - promizr.series(executors).then(results => { - sinon.assert.calledThrice(spy); - - results.length.should.equal(3); - results[0].should.equal(list[0] * list[0]); - results[1].should.equal(list[1] * list[1]); - results[2].should.equal(list[2] * list[2]); - }).then(done, done); - }); - - it("should stop if an executor throws", done => { - var spy = sinon.spy(), - - executors = list.map(num => sinon.spy(() => { - if (num === list[1]) { - return Promise.reject(common.testError); - } - - return promizr.timeout(num).then(() => spy(num)); - })); - - promizr.series(executors).catch(e => { - e.should.equal(common.testError); - - sinon.assert.calledOnce(spy); - sinon.assert.calledWithExactly(spy, list[0]); - - sinon.assert.calledOnce(executors[0]); - sinon.assert.calledOnce(executors[1]); - sinon.assert.notCalled(executors[2]); - }).then(done, done); - }); - - }); - - describe("whith an object argument", () => { - - it("should call each provided methods with given values in series", done => { - var spy = sinon.spy(), - executors = common.createExecutorObject(list, num => () => promizr.timeout(num).then(() => spy(num))); - - promizr.series(executors).then(() => { - sinon.assert.calledThrice(spy); - - sinon.assert.calledWithExactly(spy, list[0]); - sinon.assert.calledWithExactly(spy, list[1]); - sinon.assert.calledWithExactly(spy, list[2]); - - }).then(done, done); - }); - - it("should return an object with each values returned by executors", done => { - var spy = sinon.spy(num => num * num), - executors = common.createExecutorObject(list, num => () => promizr.timeout(num).then(() => spy(num))); - - promizr.series(executors).then(results => { - sinon.assert.calledThrice(spy); - - Object.keys(results).length.should.equal(3); - results[list[0].toString()].should.equal(list[0] * list[0]); - results[list[1].toString()].should.equal(list[1] * list[1]); - results[list[2].toString()].should.equal(list[2] * list[2]); - }).then(done, done); - }); - - it("should stop if an executor throws", done => { - var spy = sinon.spy(), - count = 0, - - executors = common.createExecutorObject(list, num => sinon.spy(() => { - if (count++ > 0) { - return Promise.reject(common.testError); - } - - return promizr.timeout(num).then(() => spy(num)); - })); - - promizr.series(executors).catch(e => { - e.should.equal(common.testError); - sinon.assert.calledOnce(spy); - }).then(done, done); - }); - - }); - - }); - - describe("parallel function", () => { - - describe("whith a list argument", () => { - - it("should call each provided methods with given values in parallel", done => { - var spy = sinon.spy(), - executors = list.map(num => () => promizr.timeout(num).then(() => spy(num))); - - promizr.parallel(executors).then(() => { - sinon.assert.calledThrice(spy); - - spy.getCall(0).args[0].should.equal(list[1]); - spy.getCall(1).args[0].should.equal(list[2]); - spy.getCall(2).args[0].should.equal(list[0]); - }).then(done, done); - }); - - it("should return an array with each values returned by executors", done => { - var spy = sinon.spy(num => num * num), - executors = list.map(num => () => promizr.timeout(num).then(() => spy(num))); - - promizr.parallel(executors).then(results => { - sinon.assert.calledThrice(spy); - - results.length.should.equal(3); - results[0].should.equal(list[0] * list[0]); - results[1].should.equal(list[1] * list[1]); - results[2].should.equal(list[2] * list[2]); - }).then(done, done); - }); - - it("should stop if an executor throws", done => { - var spy = sinon.spy(), - - executors = stopList.map(num => sinon.spy(() => { - return promizr.timeout(num).then(() => { - if (num === stopList[2]) { - return Promise.reject(common.testError); - } - - spy(num); - }); - })); - - promizr.parallel(executors).catch(e => { - e.should.equal(common.testError); - - sinon.assert.calledOnce(spy); - sinon.assert.calledWithExactly(spy, stopList[1]); - - sinon.assert.calledOnce(executors[0]); - sinon.assert.calledOnce(executors[1]); - sinon.assert.calledOnce(executors[2]); - }).then(done, done); - }); - - }); - - describe("whith an object argument", () => { - - it("should call each provided methods with given values in parallel", done => { - var spy = sinon.spy(), - executors = common.createExecutorObject(list, num => () => promizr.timeout(num).then(() => spy(num))); - - promizr.parallel(executors).then(() => { - sinon.assert.calledThrice(spy); - - spy.getCall(0).args[0].should.equal(list[1]); - spy.getCall(1).args[0].should.equal(list[2]); - spy.getCall(2).args[0].should.equal(list[0]); - }).then(done, done); - }); - - it("should return an object with each values returned by executors", done => { - var spy = sinon.spy(num => num * num), - executors = common.createExecutorObject(list, num => () => promizr.timeout(num).then(() => spy(num))); - - promizr.parallel(executors).then(results => { - sinon.assert.calledThrice(spy); - - Object.keys(results).length.should.equal(3); - results[list[0].toString()].should.equal(list[0] * list[0]); - results[list[1].toString()].should.equal(list[1] * list[1]); - results[list[2].toString()].should.equal(list[2] * list[2]); - }).then(done, done); - }); - - it("should stop if an executor throws", done => { - var spy = sinon.spy(), - - executors = common.createExecutorObject(stopList, num => sinon.spy(() => { - return promizr.timeout(num).then(() => { - if (num === stopList[2]) { - return Promise.reject(common.testError); - } - - spy(num); - }); - })); - - promizr.parallel(executors).catch(e => { - e.should.equal(common.testError); - - sinon.assert.calledOnce(spy); - sinon.assert.calledWithExactly(spy, stopList[1]); - - sinon.assert.calledOnce(executors[stopList[0].toString()] as sinon.SinonSpy); - sinon.assert.calledOnce(executors[stopList[1].toString()] as sinon.SinonSpy); - sinon.assert.calledOnce(executors[stopList[2].toString()] as sinon.SinonSpy); - }).then(done, done); - }); - - }); - - }); - - describe("whilst function", () => { - - it("should call provided method while given test pass", done => { - var spy = sinon.spy(), - i = 0, - - test = sinon.spy(() => list[i] % 2 !== 0), - executor = () => { - var num = list[i++]; - return promizr.timeout(num).then(() => spy(num)); - }; - - promizr.whilst(test, executor).then(() => { - sinon.assert.calledTwice(spy); - sinon.assert.calledThrice(test); - - spy.getCall(0).args[0].should.equal(list[0]); - spy.getCall(1).args[0].should.equal(list[1]); - }).then(done, done); - }); - - it("should always call provided method after given test", done => { - var i = 0, - - test = sinon.spy(() => list[i] % 2 !== 0), - executor = sinon.spy(() => promizr.timeout(list[i++])); - - promizr.whilst(test, executor).then(() => { - sinon.assert.calledTwice(executor); - sinon.assert.calledThrice(test); - - executor.getCall(0).calledAfter(test.getCall(0)).should.be.ok; - executor.getCall(1).calledAfter(test.getCall(1)).should.be.ok; - executor.getCall(1).calledBefore(test.getCall(2)).should.be.ok; - }).then(done, done); - }); - - it("should stop if an executor throws", done => { - var spy = sinon.spy(), - i = 0, - - test = sinon.spy(() => true), - executor = sinon.spy(() => { - var num = list[i++]; - if (num === list[1]) { - return Promise.reject(common.testError); - } - - return promizr.timeout(num).then(() => spy(num)); - }); - - promizr.whilst(test, executor).catch(e => { - e.should.equal(common.testError); - - sinon.assert.calledOnce(spy); - sinon.assert.calledWithExactly(spy, list[0]); - - sinon.assert.calledTwice(test); - sinon.assert.calledTwice(executor); - }).then(done, done); - }); - - }); - - describe("doWhilst function", () => { - - it("should call provided method while given test pass", done => { - var spy = sinon.spy(num => num), - i = 0, - - test = sinon.spy(res => res !== list[1]), - executor = () => { - var num = list[i++]; - return promizr.timeout(num).then(() => spy(num)); - }; - - promizr.doWhilst(executor, test).then(() => { - sinon.assert.calledTwice(spy); - sinon.assert.calledTwice(test); - - spy.getCall(0).args[0].should.equal(list[0]); - spy.getCall(1).args[0].should.equal(list[1]); - }).then(done, done); - }); - - it("should pass the result of the task to the test function", done => { - var spy = sinon.spy(num => num), - i = 0, - - test = sinon.spy(res => res !== list[1]), - executor = () => { - var num = list[i++]; - return promizr.timeout(num).then(() => spy(num)); - }; - - promizr.doWhilst(executor, test).then(() => { - sinon.assert.calledTwice(spy); - sinon.assert.calledTwice(test); - - test.getCall(0).args[0].should.equal(list[0]); - test.getCall(1).args[0].should.equal(list[1]); - }).then(done, done); - }); - - it("should always call provided method before given test", done => { - var i = 0, - test = sinon.spy(res => list[i++] !== list[1]), - executor = sinon.spy(() => promizr.timeout(list[i])); - - promizr.doWhilst(executor, test).then(() => { - sinon.assert.calledTwice(executor); - sinon.assert.calledTwice(test); - - executor.getCall(0).calledBefore(test.getCall(0)).should.be.ok; - executor.getCall(1).calledBefore(test.getCall(1)).should.be.ok; - }).then(done, done); - }); - - it("should stop if an executor throws", done => { - var spy = sinon.spy(), - i = 0, - - test = sinon.spy(() => true), - executor = sinon.spy(() => { - var num = list[i++]; - if (num === list[1]) { - return Promise.reject(common.testError); - } - - return promizr.timeout(num).then(() => spy(num)); - }); - - promizr.doWhilst(executor, test).catch(e => { - e.should.equal(common.testError); - - sinon.assert.calledOnce(spy); - sinon.assert.calledWithExactly(spy, list[0]); - - sinon.assert.calledOnce(test); - sinon.assert.calledTwice(executor); - }).then(done, done); - }); - - }); - - describe("until function", () => { - - it("should call provided method while given test pass", done => { - var spy = sinon.spy(), - i = 0, - - test = sinon.spy(() => list[i] % 2 === 0), - executor = () => { - var num = list[i++]; - return promizr.timeout(num).then(() => spy(num)); - }; - - promizr.until(test, executor).then(() => { - sinon.assert.calledTwice(spy); - sinon.assert.calledThrice(test); - - spy.getCall(0).args[0].should.equal(list[0]); - spy.getCall(1).args[0].should.equal(list[1]); - }).then(done, done); - }); - - it("should always call provided method after given test", done => { - var i = 0, - - test = sinon.spy(() => list[i] % 2 === 0), - executor = sinon.spy(() => promizr.timeout(list[i++])); - - promizr.until(test, executor).then(() => { - sinon.assert.calledTwice(executor); - sinon.assert.calledThrice(test); - - executor.getCall(0).calledAfter(test.getCall(0)).should.be.ok; - executor.getCall(1).calledAfter(test.getCall(1)).should.be.ok; - executor.getCall(1).calledBefore(test.getCall(2)).should.be.ok; - }).then(done, done); - }); - - it("should stop if an executor throws", done => { - var spy = sinon.spy(), - i = 0, - - test = sinon.spy(() => false), - executor = sinon.spy(() => { - var num = list[i++]; - if (num === list[1]) { - return Promise.reject(common.testError); - } - - return promizr.timeout(num).then(() => spy(num)); - }); - - promizr.until(test, executor).catch(e => { - e.should.equal(common.testError); - - sinon.assert.calledOnce(spy); - sinon.assert.calledWithExactly(spy, list[0]); - - sinon.assert.calledTwice(test); - sinon.assert.calledTwice(executor); - }).then(done, done); - }); - - }); - - describe("doUntil function", () => { - - it("should call provided method while given test pass", done => { - var spy = sinon.spy(num => num), - i = 0, - - test = sinon.spy(res => res === list[1]), - executor = () => { - var num = list[i++]; - return promizr.timeout(num).then(() => spy(num)); - }; - - promizr.doUntil(executor, test).then(() => { - sinon.assert.calledTwice(spy); - sinon.assert.calledTwice(test); - - spy.getCall(0).args[0].should.equal(list[0]); - spy.getCall(1).args[0].should.equal(list[1]); - }).then(done, done); - }); - - it("should pass the result of the task to the test function", done => { - var spy = sinon.spy(num => num), - i = 0, - - test = sinon.spy(res => res === list[1]), - executor = () => { - var num = list[i++]; - return promizr.timeout(num).then(() => spy(num)); - }; - - promizr.doUntil(executor, test).then(() => { - sinon.assert.calledTwice(spy); - sinon.assert.calledTwice(test); - - test.getCall(0).args[0].should.equal(list[0]); - test.getCall(1).args[0].should.equal(list[1]); - }).then(done, done); - }); - - it("should always call provided method before given test", done => { - var i = 0, - test = sinon.spy(res => list[i++] === list[1]), - executor = sinon.spy(() => promizr.timeout(list[i])); - - promizr.doUntil(executor, test).then(() => { - sinon.assert.calledTwice(executor); - sinon.assert.calledTwice(test); - - executor.getCall(0).calledBefore(test.getCall(0)).should.be.ok; - executor.getCall(1).calledBefore(test.getCall(1)).should.be.ok; - }).then(done, done); - }); - - it("should stop if an executor throws", done => { - var spy = sinon.spy(), - i = 0, - - test = sinon.spy(() => false), - executor = sinon.spy(() => { - var num = list[i++]; - if (num === list[1]) { - return Promise.reject(common.testError); - } - - return promizr.timeout(num).then(() => spy(num)); - }); - - promizr.doUntil(executor, test).catch(e => { - e.should.equal(common.testError); - - sinon.assert.calledOnce(spy); - sinon.assert.calledWithExactly(spy, list[0]); - - sinon.assert.calledOnce(test); - sinon.assert.calledTwice(executor); - }).then(done, done); - }); - - }); - - describe("forever function", () => { - - it("should call the method until forever until it throws", done => { - var i = 0, - spy = sinon.spy(() => { - if (i === 2) { - throw common.testError; - } - - return promizr.timeout(i++); - }); - - promizr.forever(spy).catch(() => { - sinon.assert.calledThrice(spy); - }).then(done, done); - }); - - }); - - describe("waterfall function", () => { - - it("should call each task with previous task result as argument", done => { - var spy = sinon.spy((a, b) => a + b), - executors = list.map(a => b => promizr.timeout(a).then(() => spy(a, b || 0))); - - promizr.waterfall(executors).then(() => { - sinon.assert.calledThrice(spy); - - spy.getCall(0).args[0].should.equal(list[0]); - spy.getCall(1).args[0].should.equal(list[1]); - spy.getCall(2).args[0].should.equal(list[2]); - - spy.getCall(0).args[1].should.equal(0); - spy.getCall(1).args[1].should.equal(list[0]); - spy.getCall(2).args[1].should.equal(list[0] + list[1]); - }).then(done, done); - }); - - it("should return the result of the last task", done => { - var spy = sinon.spy((a, b) => a + b), - executors = list.map(a => b => promizr.timeout(a).then(() => spy(a, b || 0))); - - promizr.waterfall(executors).then(result => { - result.should.equal(list[0] + list[1] + list[2]); - }).then(done, done); - }); - - it("should stop if an executor throws", done => { - var spy = sinon.spy(), - i = 0, - - executors = list.map(num => sinon.spy(() => { - if (i++ === 1) { - return Promise.reject(common.testError); - } - - return promizr.timeout(num).then(() => spy(num)); - })); - - promizr.waterfall(executors).catch(e => { - e.should.equal(common.testError); - - sinon.assert.calledOnce(spy); - sinon.assert.calledWithExactly(spy, list[0]); - - sinon.assert.calledOnce(executors[0]); - sinon.assert.calledOnce(executors[1]); - sinon.assert.notCalled(executors[2]); - }).then(done, done); - }); - - }); - - describe("compose function", () => { - - it("should return a promised function which should transfer its arguments and owner to the last given task", done => { - var task = sinon.spy((...args: number[]) => args.reduce((a, b) => a + b, 0)), - owner = {}; - - var fn = promizr.compose(task); - - sinon.assert.notCalled(task); - - fn.apply(owner, list).then(result => { - sinon.assert.calledOnce(task); - sinon.assert.calledWithExactly(task, list[0], list[1], list[2]); - sinon.assert.calledOn(task, owner); - }).then(done, done); - }); - - describe("when result called", () => { - - it("should call each task with previous task result as argument", done => { - var spy = sinon.spy((a, b) => a + b), - executors = list.map(a => b => promizr.timeout(a).then(() => spy(a, b))); - - promizr.compose.apply(null, executors)(0).then(() => { - sinon.assert.calledThrice(spy); - - spy.getCall(0).args[0].should.equal(list[2]); - spy.getCall(1).args[0].should.equal(list[1]); - spy.getCall(2).args[0].should.equal(list[0]); - - spy.getCall(0).args[1].should.equal(0); - spy.getCall(1).args[1].should.equal(list[2]); - spy.getCall(2).args[1].should.equal(list[2] + list[1]); - }).then(done, done); - }); - - it("should return the result of the first given task", done => { - var task = sinon.spy((...args: number[]) => args.reduce((a, b) => a + b, 0)), - owner = {}; - - promizr.compose(task).apply(owner, list).then(result => { - result.should.equal(list[0] + list[1] + list[2]); - }).then(done, done); - }); - - it("should stop if an executor throws", done => { - var spy = sinon.spy(), - i = 0, - - executors = list.map(num => sinon.spy(() => { - if (i++ === 1) { - return Promise.reject(common.testError); - } - - return promizr.timeout(num).then(() => spy(num)); - })); - - promizr.compose.apply(null, executors)(0).catch(e => { - e.should.equal(common.testError); - - sinon.assert.calledOnce(spy); - sinon.assert.calledWithExactly(spy, list[2]); - - sinon.assert.calledOnce(executors[2]); - sinon.assert.calledOnce(executors[1]); - sinon.assert.notCalled(executors[0]); - }).then(done, done); - }); - - }); - - }); - - describe("seq function", () => { - - it("should return a promised function which should transfer its arguments and owner to the first given task", done => { - var task = sinon.spy((...args: number[]) => args.reduce((a, b) => a + b, 0)), - owner = {}; - - var fn = promizr.seq(task); - - sinon.assert.notCalled(task); - - fn.apply(owner, list).then(result => { - sinon.assert.calledOnce(task); - sinon.assert.calledWithExactly(task, list[0], list[1], list[2]); - sinon.assert.calledOn(task, owner); - }).then(done, done); - }); - - describe("when result called", () => { - - it("should call each task with previous task result as argument", done => { - var spy = sinon.spy((a, b) => a + b), - executors = list.map(a => b => promizr.timeout(a).then(() => spy(a, b))); - - promizr.seq.apply(null, executors)(0).then(() => { - sinon.assert.calledThrice(spy); - - spy.getCall(0).args[0].should.equal(list[0]); - spy.getCall(1).args[0].should.equal(list[1]); - spy.getCall(2).args[0].should.equal(list[2]); - - spy.getCall(0).args[1].should.equal(0); - spy.getCall(1).args[1].should.equal(list[0]); - spy.getCall(2).args[1].should.equal(list[0] + list[1]); - }).then(done, done); - }); - - it("should return the result of the last task", done => { - var task = sinon.spy((...args: number[]) => args.reduce((a, b) => a + b, 0)), - owner = {}; - - promizr.seq(task).apply(owner, list).then(result => { - result.should.equal(list[0] + list[1] + list[2]); - }).then(done, done); - }); - - it("should stop if an executor throws", done => { - var spy = sinon.spy(), - i = 0, - - executors = list.map(num => sinon.spy(() => { - if (i++ === 1) { - return Promise.reject(common.testError); - } - - return promizr.timeout(num).then(() => spy(num)); - })); - - promizr.seq.apply(null, executors)(0).catch(e => { - e.should.equal(common.testError); - - sinon.assert.calledOnce(spy); - sinon.assert.calledWithExactly(spy, list[0]); - - sinon.assert.calledOnce(executors[0]); - sinon.assert.calledOnce(executors[1]); - sinon.assert.notCalled(executors[2]); - }).then(done, done); - }); - - }); - - }); - - describe("applyEach function", () => { - - describe("whith a list of arguments", () => { - - it("should call each provided methods with given arguments in parallel", done => { - var spy = sinon.spy(), - executors = list.map(num => (arg) => promizr.timeout(num).then(() => spy(num, arg))); - - (>promizr.applyEach(executors, list)).then(() => { - sinon.assert.calledThrice(spy); - - spy.getCall(0).args[0].should.equal(list[1]); - spy.getCall(1).args[0].should.equal(list[2]); - spy.getCall(2).args[0].should.equal(list[0]); - - sinon.assert.alwaysCalledWithMatch(spy, sinon.match.number, sinon.match.same(list)); - }).then(done, done); - }); - - it("should return an array with each values returned by executors", done => { - var spy = sinon.spy(num => num * num), - executors = list.map(num => () => promizr.timeout(num).then(() => spy(num))); - - (>promizr.applyEach(executors, list)).then(results => { - sinon.assert.calledThrice(spy); - - results.length.should.equal(3); - results[0].should.equal(list[0] * list[0]); - results[1].should.equal(list[1] * list[1]); - results[2].should.equal(list[2] * list[2]); - }).then(done, done); - }); - - it("should stop if an executor throws", done => { - var spy = sinon.spy(), - - executors = stopList.map(num => sinon.spy(() => { - return promizr.timeout(num).then(() => { - if (num === stopList[2]) { - return Promise.reject(common.testError); - } - - spy(num); - }); - })); - - (>promizr.applyEach(executors, stopList)).catch(e => { - e.should.equal(common.testError); - - sinon.assert.calledOnce(spy); - sinon.assert.calledWithExactly(spy, stopList[1]); - - sinon.assert.calledOnce(executors[0]); - sinon.assert.calledOnce(executors[1]); - sinon.assert.calledOnce(executors[2]); - }).then(done, done); - }); - - }); - - describe("whith no arguments", () => { - - it("should return a promised function which should transfer its arguments to all tasks", done => { - var spy = sinon.spy(() => promizr.timeout(1)), - executors = [spy, spy, spy], - owner = {}; - - var fn = >promizr.applyEach(executors); - - sinon.assert.notCalled(spy); - - fn.apply(owner, list).then(result => { - sinon.assert.calledThrice(spy); - sinon.assert.alwaysCalledWithExactly(spy, list[0], list[1], list[2]); - sinon.assert.alwaysCalledOn(spy, owner); - }).then(done, done); - }); - - }); - - }); - - describe("applyEachSeries function", () => { - - describe("whith a list of arguments", () => { - - it("should call each provided methods with given values in series", done => { - var spy = sinon.spy(), - executors = list.map(num => (arg) => promizr.timeout(num).then(() => spy(num, arg))); - - (>promizr.applyEachSeries(executors, list)).then(() => { - sinon.assert.calledThrice(spy); - - spy.getCall(0).args[0].should.equal(list[0]); - spy.getCall(1).args[0].should.equal(list[1]); - spy.getCall(2).args[0].should.equal(list[2]); - - sinon.assert.alwaysCalledWithMatch(spy, sinon.match.number, sinon.match.same(list)); - }).then(done, done); - }); - - it("should return an array with each values returned by executors", done => { - var spy = sinon.spy(num => num * num), - executors = list.map(num => () => promizr.timeout(num).then(() => spy(num))); - - (>promizr.applyEachSeries(executors, list)).then(results => { - sinon.assert.calledThrice(spy); - - results.length.should.equal(3); - results[0].should.equal(list[0] * list[0]); - results[1].should.equal(list[1] * list[1]); - results[2].should.equal(list[2] * list[2]); - }).then(done, done); - }); - - it("should stop if an executor throws", done => { - var spy = sinon.spy(), - - executors = list.map(num => sinon.spy(() => { - if (num === list[1]) { - return Promise.reject(common.testError); - } - - return promizr.timeout(num).then(() => spy(num)); - })); - - (>promizr.applyEachSeries(executors, list)).catch(e => { - e.should.equal(common.testError); - - sinon.assert.calledOnce(spy); - sinon.assert.calledWithExactly(spy, list[0]); - - sinon.assert.calledOnce(executors[0]); - sinon.assert.calledOnce(executors[1]); - sinon.assert.notCalled(executors[2]); - }).then(done, done); - }); - - }); - - describe("whith no arguments", () => { - - it("should return a promised function which should transfer its arguments to all tasks", done => { - var spy = sinon.spy(() => promizr.timeout(1)), - executors = [spy, spy, spy], - owner = {}; - - var fn = >promizr.applyEachSeries(executors); - - sinon.assert.notCalled(spy); - - fn.apply(owner, list).then(result => { - sinon.assert.calledThrice(spy); - sinon.assert.alwaysCalledWithExactly(spy, list[0], list[1], list[2]); - sinon.assert.alwaysCalledOn(spy, owner); - }).then(done, done); - }); - - }); - - }); - - describe("retry function", () => { - - it("should retry function if failed", done => { - var count = 0, - spy = sinon.spy(() => { - if (count++ === 0) { - throw common.testError; - } - }); - - promizr.retry(5, spy).then(() => { - sinon.assert.calledTwice(spy); - count.should.equal(2); - }).then(done, done); - }); - - it("should fail if function failed more than given times", done => { - var count = 0, - spy = sinon.spy(() => { - count++; - throw common.testError; - }); - - promizr.retry(5, spy).catch(e => { - e.should.equal(common.testError); - sinon.assert.callCount(spy, 5); - count.should.equal(5); - }).then(done, done); - }); - - }); - - describe("times function", () => { - - it("should call provided function a given number of times", done => { - var count = 0, - spy = sinon.spy(() => { - return list[count++]; - }); - - promizr.times(3, spy).then(results => { - sinon.assert.calledThrice(spy); - count.should.equal(3); - }).then(done, done); - }); - - it("should make all calls in parallel", done => { - var count = 0, - spy = sinon.spy(), - task = () => { - var num = list[count++]; - return promizr.timeout(num).then(() => spy(num)); - }; - - promizr.times(3, task).then(results => { - sinon.assert.calledThrice(spy); - count.should.equal(3); - - spy.getCall(0).args[0].should.equal(list[1]); - spy.getCall(1).args[0].should.equal(list[2]); - spy.getCall(2).args[0].should.equal(list[0]); - }).then(done, done); - }); - - it("should return an array with each values returned by tasks", done => { - var count = 0, - spy = sinon.spy(num => num * num), - task = () => { - var num = list[count++]; - return promizr.timeout(num).then(() => spy(num)); - }; - - promizr.times(3, task).then(results => { - sinon.assert.calledThrice(spy); - - results.length.should.equal(3); - results[0].should.equal(list[0] * list[0]); - results[1].should.equal(list[1] * list[1]); - results[2].should.equal(list[2] * list[2]); - }).then(done, done); - }); - - it("should stop if a task throws", done => { - var count = 0, - spy = sinon.spy(), - - task = sinon.spy(() => { - var num = stopList[count++]; - return promizr.timeout(num).then(() => { - if (num === stopList[2]) { - return Promise.reject(common.testError); - } - - spy(num); - }); - }); - - promizr.times(3, task).catch(e => { - e.should.equal(common.testError); - - sinon.assert.calledOnce(spy); - sinon.assert.calledWithExactly(spy, stopList[1]); - - sinon.assert.calledThrice(task); - }).then(done, done); - }); - - }); - - describe("timesSeries function", () => { - - it("should call provided function a given number of times", done => { - var count = 0, - spy = sinon.spy(() => { - return list[count++]; - }); - - promizr.timesSeries(3, spy).then(results => { - sinon.assert.calledThrice(spy); - count.should.equal(3); - }).then(done, done); - }); - - it("should make all calls in series", done => { - var count = 0, - spy = sinon.spy(), - task = () => { - var num = list[count++]; - return promizr.timeout(num).then(() => spy(num)); - }; - - promizr.timesSeries(3, task).then(results => { - sinon.assert.calledThrice(spy); - count.should.equal(3); - - spy.getCall(0).args[0].should.equal(list[0]); - spy.getCall(1).args[0].should.equal(list[1]); - spy.getCall(2).args[0].should.equal(list[2]); - }).then(done, done); - }); - - it("should return an array with each values returned by tasks", done => { - var count = 0, - spy = sinon.spy(num => num * num), - task = () => { - var num = list[count++]; - return promizr.timeout(num).then(() => spy(num)); - }; - - promizr.timesSeries(3, task).then(results => { - sinon.assert.calledThrice(spy); - - results.length.should.equal(3); - results[0].should.equal(list[0] * list[0]); - results[1].should.equal(list[1] * list[1]); - results[2].should.equal(list[2] * list[2]); - }).then(done, done); - }); - - it("should stop if a task throws", done => { - var count = 0, - spy = sinon.spy(), - - task = sinon.spy(() => { - var num = list[count++]; - if (num === list[1]) { - return Promise.reject(common.testError); - } - - return promizr.timeout(num).then(() => spy(num)); - }); - - promizr.timesSeries(3, task).catch(e => { - e.should.equal(common.testError); - - sinon.assert.calledOnce(spy); - sinon.assert.calledWithExactly(spy, list[0]); - - sinon.assert.calledTwice(task); - }).then(done, done); - }); - - }); - -}); diff --git a/tests/promizr/helpers/ProgressContext.ts b/tests/promizr/helpers/ProgressContext.ts deleted file mode 100644 index e6d3c76..0000000 --- a/tests/promizr/helpers/ProgressContext.ts +++ /dev/null @@ -1,90 +0,0 @@ -/// - -import * as promizr from "promizr"; - -class ProgressContext { - public deferreds: promizr.ProgressPromiseDeferred[]; - - public constructor(public count: number) { - this.reset(); - } - - public promises(): promizr.ProgressPromiseable[] { - return this.deferreds.map(d => d.promise); - } - - public addFake() { - const dfd = promizr.defer(); - this.deferreds.push({ - resolve: dfd.resolve, - reject: dfd.reject, - progress: undefined, - promise: dfd.promise - }); - - return this; - } - - public resolve(); - public resolve(index: number); - public resolve(index: number, value: number); - public resolve(index?: number, value?: number) { - if (arguments.length === 0) { - console.log("al"); - this.deferreds.forEach(d => d.resolve()); - return; - } - - const defer = this.deferreds[index]; - if (defer) { - defer.resolve(value); - } - } - - public reject(); - public reject(index: number); - public reject(index: number, value: number); - public reject(index?: number, reason?: Error|any) { - if (arguments.length === 0) { - this.deferreds.forEach(d => d.reject(reason)); - return; - } - - const defer = this.deferreds[index]; - if (defer) { - defer.reject(reason); - } - } - - public progress(index: number); - public progress(index: number, value: number); - public progress(index: number, value?: number) { - if (arguments.length === 1) { - this.deferreds.forEach(d => d.progress && d.progress(index)); - return; - } - - const defer = this.deferreds[index]; - if (defer && defer.progress) { - defer.progress(value); - } - } - - public all(): promizr.ProgressPromise { - return promizr.ProgressPromise.all(this.promises()); - } - - public race(): promizr.ProgressPromise { - return promizr.ProgressPromise.race(this.promises()); - } - - public reset() { - this.deferreds = new Array(this.count); - - for (let i = 0; i < this.count; i++) { - this.deferreds[i] = promizr.ProgressPromise.defer(); - } - } -} - -export = ProgressContext; diff --git a/tests/promizr/helpers/common.ts b/tests/promizr/helpers/common.ts deleted file mode 100644 index c1300c5..0000000 --- a/tests/promizr/helpers/common.ts +++ /dev/null @@ -1,77 +0,0 @@ -/// - -import { SinonSpy } from "sinon"; -import * as promizr from "promizr"; - -if (!Function.prototype.bind) { - Function.prototype.bind = function (oThis) { - if (typeof this !== "function") { - // closest thing possible to the ECMAScript 5 internal IsCallable function - throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); - } - - var args = Array.prototype.slice.call(arguments, 1), - toBind = this, - Noop = function () { return; }, - bound = function () { - return toBind.apply( - this instanceof Noop && oThis ? this : oThis, - args.concat(Array.prototype.slice.call(arguments)) - ); - }; - - Noop.prototype = this.prototype; - bound.prototype = new Noop(); - - return bound; - }; -} - -export var testError = new Error("test"); - -export function isUndefined(x: any): boolean { - return typeof x === "undefined"; -} - -export function noop(): () => void { - return function () { return; }; -} - -export function identity(val: T): T { - return val; -} - -export function createPromise(): Promise { - return new Promise(noop); -} - -export function createDeferreds(list: T[]): promizr.Deferred[] { - return list.map(() => { - const dfd = promizr.defer(); - dfd.promise = dfd.promise.then(promizr.timeout); - - return dfd; - }); -} - -export function cleanSpy(spy: SinonSpy) { - if (spy) { - if (spy.restore) { - spy.restore(); - } - - spy.reset(); - } -} - -export type StringOrNumber = string | number; - -export function createExecutorObject(list: T[], mapper: (key: T) => () => Promise): promizr.PromiseTaskExecutorObject { - var result: promizr.PromiseTaskExecutorObject = {}; - - list.forEach(val => { - result[val.toString()] = mapper(val); - }); - - return result; -} diff --git a/tests/promizr/index.html b/tests/promizr/index.html deleted file mode 100644 index 77b131c..0000000 --- a/tests/promizr/index.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - SPA Tools - All tests - - - -

- - - - - - - - diff --git a/tests/promizr/queue.ts b/tests/promizr/queue.ts deleted file mode 100644 index a8829bc..0000000 --- a/tests/promizr/queue.ts +++ /dev/null @@ -1,485 +0,0 @@ -/// - -import * as sinon from "sinon"; -import * as promizr from "promizr"; -import * as common from "./helpers/common"; - -const - list = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15], - total = list.length, - limit = 3; - -describe("Promizr Queue Methods", () => { - - describe("Queue class", () => { - - describe("constructor", () => { - - it("should not start queue if no list provided", done => { - const - spy = sinon.spy(), - instance = new promizr.Queue(spy, limit); - - promizr.timeout(10).then(() => { - sinon.assert.notCalled(spy); - }).then(done, done); - }); - - it("should start queue if list provided", done => { - const - spy = sinon.spy(), - instance = new promizr.Queue(spy, limit, list); - - promizr.timeout(20).then(() => { - sinon.assert.callCount(spy, list.length); - }).then(done, done); - }); - - it("should start queue asynchronously", () => { - const - spy = sinon.spy(), - instance = new promizr.Queue(spy, limit, list); - - sinon.assert.notCalled(spy); - }); - - }); - - describe("task limiting", () => { - - it("should only launch specified limit number of task simultaneously", done => { - const - spy = sinon.spy(common.createPromise), - instance = new promizr.Queue(spy, limit, list); - - promizr.timeout(10).then(() => { - sinon.assert.callCount(spy, limit); - }).then(done, done); - }); - - it("should continue to limit number of task simultaneously when promises are resolved", done => { - const - dfds = common.createDeferreds(list), - spy = sinon.spy(n => dfds[n].promise), - instance = new promizr.Queue(spy, limit, list); - - sinon.assert.notCalled(spy); - - promizr.timeout(10) - .then(() => { - sinon.assert.callCount(spy, limit); - - for (let i = 0, len = limit; i < len; i++) { - dfds[i].resolve(); - } - - return dfds[limit - 1].promise; - }) - .then(() => promizr.timeout(5)) - .then(() => { - sinon.assert.callCount(spy, limit * 2); - - for (let i = limit, len = limit * 3; i < len; i++) { - dfds[i].resolve(); - } - - return dfds[limit * 2 - 1].promise; - }) - .then(() => { - spy.callCount.should.be.below(limit * 3 + 1); - return dfds[limit * 3 - 1].promise; - }) - .then(() => promizr.timeout()) - .then(() => { - sinon.assert.callCount(spy, limit * 4); - }) - .then(done, done); - }); - - it("should continue to execute works even if exception occurred", done => { - const - err = new Error("This is an error !"), - dfds = common.createDeferreds(list), - spy = sinon.spy(n => dfds[n].promise), - instance = new promizr.Queue(spy, limit, list); - - sinon.assert.notCalled(spy); - - promizr.timeout(10) - .then(() => { - sinon.assert.callCount(spy, limit); - - for (let i = 0, len = limit; i < len; i++) { - dfds[i].reject(err); - } - - return dfds[limit - 1].promise.catch(common.noop); - }) - .then(() => promizr.timeout()) - .then(() => { - sinon.assert.callCount(spy, limit * 2); - - for (let i = limit, len = limit * 2; i < len; i++) { - dfds[i].reject(err); - } - - return dfds[limit * 2 - 1].promise.catch(common.noop); - }) - .then(() => promizr.timeout()) - .then(() => { - sinon.assert.callCount(spy, limit * 3); - }) - .then(done, done); - }); - - it("should not continue to execute works if exception occurred and stopOnError is set to true", done => { - const - err = new Error("This is an error !"), - dfds = common.createDeferreds(list), - spy = sinon.spy(n => dfds[n].promise), - instance = new promizr.Queue(spy, limit, list); - - sinon.assert.notCalled(spy); - - instance.stopOnError = true; - - promizr.timeout(10) - .then(() => { - sinon.assert.callCount(spy, limit); - - for (let i = 0, len = limit; i < len; i++) { - dfds[i].reject(err); - } - - return dfds[limit - 1].promise.catch(common.noop); - }) - .then(() => { - sinon.assert.callCount(spy, limit); - }) - .then(done, done); - }); - - }); - - describe("push", () => { - - it("should return a promise which is resolved when all workers are done", done => { - const - defer = promizr.defer(), - spy = sinon.spy(), - instance = new promizr.Queue(() => defer.promise, limit), - - promise = instance.push(list).then(spy); - - promizr.timeout(10) - .then(() => { - sinon.assert.notCalled(spy); - - defer.resolve(); - return promise; - }) - .then(() => { - sinon.assert.calledOnce(spy); - }) - .then(done, done); - }); - - it("should resolve with an array containing results of each workers", done => { - const instance = new promizr.Queue(common.identity, limit); - - instance.push(list).then(res => { - res.should.be.an.Array; - res.should.eql(list); - res.should.not.be.equal(list); - }).then(done, done); - }); - - it("should resolve with only one item if only one item is passed to pushed function", done => { - const instance = new promizr.Queue(common.identity, limit); - - instance.push(list[0]).then(res => { - res.should.not.be.an.Array; - res.should.equal(list[0]); - }).then(done, done); - }); - - it("should reject immediately if any item reject", done => { - const - spy = sinon.spy(n => Promise.reject(n)), - instance = new promizr.Queue(n => promizr.immediate().then(() => spy(n)), limit); - - instance.push(list).catch(err => { - sinon.assert.callCount(spy, limit); - - err.should.equal(list[0]); - }).then(done, done); - }); - - it("should wait to reject if waitToReject is set to true", done => { - const - spy = sinon.spy(n => Promise.reject(n)), - instance = new promizr.Queue(n => promizr.immediate().then(() => spy(n)), limit); - - instance.waitToReject = true; - - instance.push(list).catch((err: promizr.QueueError) => { - sinon.assert.callCount(spy, total); - - err.innerExceptions.should.eql(list); - }).then(done, done); - }); - - }); - - }); - - describe("PriorityQueue class", () => { - - describe("constructor", () => { - - it("should not start queue if no list provided", done => { - const - spy = sinon.spy(), - instance = new promizr.PriorityQueue(spy, limit); - - promizr.timeout(10).then(() => { - sinon.assert.notCalled(spy); - }).then(done, done); - }); - - it("should start queue if list provided", done => { - const - spy = sinon.spy(), - instance = new promizr.PriorityQueue(spy, limit, list); - - promizr.timeout(20).then(() => { - sinon.assert.callCount(spy, list.length); - }).then(done, done); - }); - - it("should start queue asynchronously", () => { - const - spy = sinon.spy(), - instance = new promizr.PriorityQueue(spy, limit, list); - - sinon.assert.notCalled(spy); - }); - - }); - - describe("task limiting", () => { - - it("should only launch specified limit number of task simultaneously", done => { - const - spy = sinon.spy(common.createPromise), - instance = new promizr.PriorityQueue(spy, limit, list); - - promizr.timeout(10).then(() => { - sinon.assert.callCount(spy, limit); - }).then(done, done); - }); - - it("should continue to limit number of task simultaneously when promises are resolved", done => { - const - dfds = common.createDeferreds(list), - spy = sinon.spy(n => dfds[n].promise), - instance = new promizr.PriorityQueue(spy, limit, list); - - sinon.assert.notCalled(spy); - - promizr.timeout(10) - .then(() => { - sinon.assert.callCount(spy, limit); - - for (let i = 0, len = limit; i < len; i++) { - dfds[i].resolve(); - } - - return dfds[limit - 1].promise; - }) - .then(() => promizr.timeout()) - .then(() => { - sinon.assert.callCount(spy, limit * 2); - - for (let i = limit, len = limit * 3; i < len; i++) { - dfds[i].resolve(); - } - - return dfds[limit * 2 - 1].promise; - }) - .then(() => { - spy.callCount.should.be.below(limit * 3 + 1); - return dfds[limit * 3 - 1].promise; - }) - .then(() => promizr.timeout()) - .then(() => { - sinon.assert.callCount(spy, limit * 4); - }) - .then(done, done); - }); - - it("should continue to execute works even if exception occurred", done => { - const - err = new Error("This is an error !"), - dfds = common.createDeferreds(list), - spy = sinon.spy(n => dfds[n].promise), - instance = new promizr.PriorityQueue(spy, limit, list); - - sinon.assert.notCalled(spy); - - promizr.timeout(10) - .then(() => { - sinon.assert.callCount(spy, limit); - - for (let i = 0, len = limit; i < len; i++) { - dfds[i].reject(err); - } - - return dfds[limit - 1].promise.catch(common.noop); - }) - .then(() => promizr.timeout()) - .then(() => { - sinon.assert.callCount(spy, limit * 2); - - for (let i = limit, len = limit * 2; i < len; i++) { - dfds[i].reject(err); - } - - return dfds[limit * 2 - 1].promise.catch(common.noop); - }) - .then(() => promizr.timeout()) - .then(() => { - sinon.assert.callCount(spy, limit * 3); - }) - .then(done, done); - }); - - it("should not continue to execute works if exception occurred and stopOnError is set to true", done => { - const - err = new Error("This is an error !"), - dfds = common.createDeferreds(list), - spy = sinon.spy(n => dfds[n].promise), - instance = new promizr.PriorityQueue(spy, limit, list); - - sinon.assert.notCalled(spy); - - instance.stopOnError = true; - - promizr.timeout(10) - .then(() => { - sinon.assert.callCount(spy, limit); - - for (let i = 0, len = limit; i < len; i++) { - dfds[i].reject(err); - } - - return dfds[limit - 1].promise.catch(common.noop); - }) - .then(() => { - sinon.assert.callCount(spy, limit); - }) - .then(done, done); - }); - - }); - - describe("task priority", () => { - - it("call tasks in priority order", done => { - const - spy = sinon.spy(), - spy2 = sinon.spy(), - spy3 = sinon.spy(), - instance = new promizr.PriorityQueue(promizr.immediate, limit); - - Promise.all([ - instance.push(5, list).then(spy), - instance.push(1, list).then(spy2), - instance.push(3, list).then(spy3) - ]).then(() => { - sinon.assert.calledOnce(spy); - sinon.assert.calledOnce(spy2); - sinon.assert.calledOnce(spy3); - - spy2.calledBefore(spy).should.be.ok; - spy2.calledBefore(spy3).should.be.ok; - - spy3.calledBefore(spy).should.be.ok; - }).then(done, done); - }); - - }); - - describe("push", () => { - - it("should return a promise which is resolved when all workers are done", done => { - const - defer = promizr.defer(), - spy = sinon.spy(), - instance = new promizr.PriorityQueue(() => defer.promise, limit), - - promise = instance.push(1, list).then(spy); - - promizr.timeout(10) - .then(() => { - sinon.assert.notCalled(spy); - - defer.resolve(); - return promise; - }) - .then(() => { - sinon.assert.calledOnce(spy); - }) - .then(done, done); - }); - - it("should resolve with an array containing results of each workers", done => { - const instance = new promizr.PriorityQueue(common.identity, limit); - - instance.push(1, list).then(res => { - res.should.be.an.Array; - res.should.eql(list); - res.should.not.be.equal(list); - }).then(done, done); - }); - - it("should resolve with only one item if only one item is passed to pushed function", done => { - const instance = new promizr.PriorityQueue(common.identity, limit); - - instance.push(1, list[0]).then(res => { - res.should.not.be.an.Array; - res.should.equal(list[0]); - }).then(done, done); - }); - - it("should reject immediately if any item reject", done => { - const - spy = sinon.spy(n => Promise.reject(n)), - instance = new promizr.PriorityQueue(n => promizr.immediate().then(() => spy(n)), limit); - - instance.push(1, list).catch(err => { - sinon.assert.callCount(spy, limit); - - err.should.equal(list[0]); - }).then(done, done); - }); - - it("should wait to reject if waitToReject is set to true", done => { - const - spy = sinon.spy(n => Promise.reject(n)), - instance = new promizr.PriorityQueue(n => promizr.immediate().then(() => spy(n)), limit); - - instance.waitToReject = true; - - instance.push(1, list).catch((err: promizr.QueueError) => { - sinon.assert.callCount(spy, total); - - err.innerExceptions.should.eql(list); - }).then(done, done); - }); - - }); - - }); - -}); diff --git a/tests/promizr/tests.d.ts b/tests/promizr/tests.d.ts deleted file mode 100644 index db6ec13..0000000 --- a/tests/promizr/tests.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -/// -/// -/// -/// -/// - -declare var require: any; -declare var requirejs: any; diff --git a/tests/reduce.spec.ts b/tests/reduce.spec.ts new file mode 100644 index 0000000..47c1579 --- /dev/null +++ b/tests/reduce.spec.ts @@ -0,0 +1,72 @@ +import reduce from "../lib/reduce"; + +import timeout from "./helpers/timeout"; + +const LIST = [15, 1, 8]; + +const REDUCE_TOTAL = LIST[0] + LIST[1] + LIST[2] + 1; +const REDUCE_RESULT = 1; + +describe("promizr.reduce()", () => { + + test("should return reduced value using iterator", async () => { + const spy = jest.fn((a, b) => a - b); + + const result = await reduce(LIST, REDUCE_TOTAL, (a, b) => timeout(b).then(() => spy(a, b))); + + expect(spy).toHaveBeenCalledTimes(3); + + expect(result).toBe(REDUCE_RESULT); + }); + + test("should call each provided methods with given values in series", async () => { + const spy = jest.fn((a, b, i, l) => a - b); + + await reduce(LIST, REDUCE_TOTAL, (a, b, i, l) => timeout(b).then(() => spy(a, b, i, l))); + + expect(spy).toHaveBeenCalledTimes(3); + + expect(spy).toHaveBeenNthCalledWith(1, REDUCE_TOTAL, LIST[0], 0, LIST); + expect(spy).toHaveBeenNthCalledWith(2, REDUCE_TOTAL - LIST[0], LIST[1], 1, LIST); + expect(spy).toHaveBeenNthCalledWith(3, REDUCE_TOTAL - LIST[0] - LIST[1], LIST[2], 2, LIST); + }); + + test("should reject if an iterator throws", async () => { + const spy = jest.fn(); + const err = new Error("test"); + + spy.mockResolvedValueOnce(1); + spy.mockRejectedValueOnce(err); + spy.mockResolvedValueOnce(2); + + await expect(reduce(LIST, REDUCE_TOTAL, spy)) + .rejects.toBe(err); + }); + + test("should stop if an iterator throws", async () => { + const spy = jest.fn((a, b) => a - b); + const err = new Error("test"); + + const iterator = jest.fn((a, b, i) => { + return timeout(b).then(() => { + if (i === 1) { + throw err; + } + + return spy(a, b); + }); + }); + + await expect(reduce(LIST, REDUCE_TOTAL, iterator)) + .rejects.toBe(err); + + + expect(spy).toBeCalledTimes(1); + expect(spy).toBeCalledWith(REDUCE_TOTAL, LIST[0]); + + expect(iterator).toBeCalledTimes(2); + expect(iterator).toHaveBeenNthCalledWith(1, REDUCE_TOTAL, LIST[0], 0, LIST); + expect(iterator).toHaveBeenNthCalledWith(2, REDUCE_TOTAL - LIST[0], LIST[1], 1, LIST); + }); + +}); diff --git a/tests/reduceRight.spec.ts b/tests/reduceRight.spec.ts new file mode 100644 index 0000000..1f9dac8 --- /dev/null +++ b/tests/reduceRight.spec.ts @@ -0,0 +1,72 @@ +import reduceRight from "../lib/reduceRight"; + +import timeout from "./helpers/timeout"; + +const LIST = [15, 1, 8]; + +const REDUCE_TOTAL = LIST[0] + LIST[1] + LIST[2] + 1; +const REDUCE_RESULT = 1; + +describe("promizr.reduceRight()", () => { + + test("should return reduced value using iterator", async () => { + const spy = jest.fn((a, b) => a - b); + + const result = await reduceRight(LIST, REDUCE_TOTAL, (a, b) => timeout(b).then(() => spy(a, b))); + + expect(spy).toHaveBeenCalledTimes(3); + + expect(result).toBe(REDUCE_RESULT); + }); + + test("should call each provreduceRightided methods with given values in series", async () => { + const spy = jest.fn((a, b, i, l) => a - b); + + await reduceRight(LIST, REDUCE_TOTAL, (a, b, i, l) => timeout(b).then(() => spy(a, b, i, l))); + + expect(spy).toHaveBeenCalledTimes(3); + + expect(spy).toHaveBeenNthCalledWith(1, REDUCE_TOTAL, LIST[2], 0, LIST); + expect(spy).toHaveBeenNthCalledWith(2, REDUCE_TOTAL - LIST[2], LIST[1], 1, LIST); + expect(spy).toHaveBeenNthCalledWith(3, REDUCE_TOTAL - LIST[2] - LIST[1], LIST[0], 2, LIST); + }); + + test("should reject if an iterator throws", async () => { + const spy = jest.fn(); + const err = new Error("test"); + + spy.mockResolvedValueOnce(1); + spy.mockRejectedValueOnce(err); + spy.mockResolvedValueOnce(2); + + await expect(reduceRight(LIST, REDUCE_TOTAL, spy)) + .rejects.toBe(err); + }); + + test("should stop if an iterator throws", async () => { + const spy = jest.fn((a, b) => a - b); + const err = new Error("test"); + + const iterator = jest.fn((a, b, i) => { + return timeout(b).then(() => { + if (i === 1) { + throw err; + } + + return spy(a, b); + }); + }); + + await expect(reduceRight(LIST, REDUCE_TOTAL, iterator)) + .rejects.toBe(err); + + + expect(spy).toBeCalledTimes(1); + expect(spy).toBeCalledWith(REDUCE_TOTAL, LIST[2]); + + expect(iterator).toBeCalledTimes(2); + expect(iterator).toHaveBeenNthCalledWith(1, REDUCE_TOTAL, LIST[2], 0, LIST); + expect(iterator).toHaveBeenNthCalledWith(2, REDUCE_TOTAL - LIST[2], LIST[1], 1, LIST); + }); + +}); diff --git a/tests/reject.spec.ts b/tests/reject.spec.ts new file mode 100644 index 0000000..50abfe3 --- /dev/null +++ b/tests/reject.spec.ts @@ -0,0 +1,71 @@ +import reject from "../lib/reject"; + +import timeout from "./helpers/timeout"; +import sort from "./helpers/sort"; + +const LIST = [15, 1, 8]; +const STOP_LIST = [251, 1, 50]; + +describe("promizr.reject()", () => { + + test("should returns the filtered array", async () => { + const spy = jest.fn(num => num % 2 === 0); + + const res = await reject(LIST, num => timeout(num).then(() => spy(num))); + + expect(res).toEqual(sort(LIST.filter(n => n % 2 === 1))); + }); + + test("should call each provided methods with given values in parallel", async () => { + const spy = jest.fn(); + + await reject(LIST, (num, index, list) => timeout(num).then(() => spy(num, index, list))); + + expect(spy).toHaveBeenCalledTimes(LIST.length); + + const ordered = sort(LIST); + for (let i = 0; i < LIST.length; i++) { + expect(spy).toHaveBeenNthCalledWith(i + 1, ordered[i], LIST.indexOf(ordered[i]), LIST); + } + }); + + test("should reject if an iterator throws", async () => { + const spy = jest.fn(); + const err = new Error("test"); + + spy.mockResolvedValueOnce(true); + spy.mockRejectedValueOnce(err); + spy.mockResolvedValueOnce(false); + + await expect(reject(LIST, spy)) + .rejects.toBe(err); + }); + + test("should stop if an iterator throws", async () => { + const spy = jest.fn(n => n % 2 === 0); + const err = new Error("test"); + + const iterator = jest.fn((num: number, i: number) => { + return timeout(num).then(() => { + if (i === 2) { + return Promise.reject(err); + } + + return spy(num); + }); + }); + + await expect(reject(STOP_LIST, iterator)) + .rejects.toBe(err); + + + expect(spy).toBeCalledTimes(1); + expect(spy).toBeCalledWith(STOP_LIST[1]); + + expect(iterator).toBeCalledTimes(3); + expect(iterator).toHaveBeenNthCalledWith(1, STOP_LIST[0], 0, STOP_LIST); + expect(iterator).toHaveBeenNthCalledWith(2, STOP_LIST[1], 1, STOP_LIST); + expect(iterator).toHaveBeenNthCalledWith(3, STOP_LIST[2], 2, STOP_LIST); + }); + +}); diff --git a/tests/rejectSeries.spec.ts b/tests/rejectSeries.spec.ts new file mode 100644 index 0000000..81441d7 --- /dev/null +++ b/tests/rejectSeries.spec.ts @@ -0,0 +1,68 @@ +import rejectSeries from "../lib/rejectSeries"; + +import timeout from "./helpers/timeout"; + +const LIST = [15, 1, 8]; +const STOP_LIST = [251, 1, 50]; + +describe("promizr.rejectSeries()", () => { + + test("should returns the filtered array", async () => { + const spy = jest.fn(num => num % 2 === 0); + + const res = await rejectSeries(LIST, num => timeout(num).then(() => spy(num))); + + expect(res).toEqual(LIST.filter(n => n % 2 === 1)); + }); + + test("should call each provided methods with given values in series", async () => { + const spy = jest.fn(); + + await rejectSeries(LIST, (num, index, list) => timeout(num).then(() => spy(num, index, list))); + + expect(spy).toHaveBeenCalledTimes(LIST.length); + + for (let i = 0; i < LIST.length; i++) { + expect(spy).toHaveBeenNthCalledWith(i + 1, LIST[i], i, LIST); + } + }); + + test("should reject if an iterator throws", async () => { + const spy = jest.fn(); + const err = new Error("test"); + + spy.mockResolvedValueOnce(true); + spy.mockRejectedValueOnce(err); + spy.mockResolvedValueOnce(false); + + await expect(rejectSeries(LIST, spy)) + .rejects.toBe(err); + }); + + test("should stop if an iterator throws", async () => { + const spy = jest.fn(n => n % 2 === 0); + const err = new Error("test"); + + const iterator = jest.fn((num: number, i: number) => { + return timeout(num).then(() => { + if (i === 1) { + return Promise.reject(err); + } + + return spy(num); + }); + }); + + await expect(rejectSeries(STOP_LIST, iterator)) + .rejects.toBe(err); + + + expect(spy).toBeCalledTimes(1); + expect(spy).toBeCalledWith(STOP_LIST[0]); + + expect(iterator).toBeCalledTimes(2); + expect(iterator).toHaveBeenNthCalledWith(1, STOP_LIST[0], 0, STOP_LIST); + expect(iterator).toHaveBeenNthCalledWith(2, STOP_LIST[1], 1, STOP_LIST); + }); + +}); diff --git a/tests/resolve.spec.ts b/tests/resolve.spec.ts new file mode 100644 index 0000000..4ee9bef --- /dev/null +++ b/tests/resolve.spec.ts @@ -0,0 +1,10 @@ +import resolve from "../lib/resolve"; + +describe("promizr.resolve()", () => { + + test("should be an alias to Promise.resolve", async () => { + const res = await resolve("result"); + expect(res).toBe("result"); + }); + +}); \ No newline at end of file diff --git a/tests/retry.spec.ts b/tests/retry.spec.ts new file mode 100644 index 0000000..6899b04 --- /dev/null +++ b/tests/retry.spec.ts @@ -0,0 +1,38 @@ +import retry from "../lib/retry"; + +describe("promizr.retry()", () => { + + test("should retry function if failed", async () => { + let count = 0; + + const spy = jest.fn(() => { + if (count++ < 2) { + throw new Error("test"); + } + }); + + await retry(5, spy); + + expect(spy).toHaveBeenCalledTimes(3); + expect(count).toBe(3); + }); + + test("should fail if function failed more than given times", async () => { + const err = new Error("test"); + + let count = 0; + + const spy = jest.fn(() => { + count++; + throw err; + }); + + await expect(retry(5, spy)) + .rejects.toBe(err); + + expect(spy).toHaveBeenCalledTimes(5); + expect(count).toBe(5); + + }); + +}); diff --git a/tests/seq.spec.ts b/tests/seq.spec.ts new file mode 100644 index 0000000..0a2063a --- /dev/null +++ b/tests/seq.spec.ts @@ -0,0 +1,78 @@ +import seq from "../lib/seq"; + +import timeout from "./helpers/timeout"; + +const LIST = [15, 1, 8]; + +describe("promizr.seq()", () => { + + test("should return a promised function which should transfer its arguments and owner to the first given task", async () => { + const task = jest.fn((...args: number[]) => args.reduce((a, b) => a + b, 0)); + const owner = {}; + + const fn = seq(task); + + expect(task).not.toHaveBeenCalled(); + + await fn.apply(owner, LIST); + + expect(task).toHaveBeenCalledTimes(1); + expect(task).toHaveBeenCalledWith(LIST[0], LIST[1], LIST[2]); + // sinon.assert.calledOn(task, owner); + }); + + describe("when result called", () => { + + test("should call each task with previous task result as argument", async () => { + const spy = jest.fn((a, b) => a + b); + const executors = LIST.map(a => (b: number) => timeout(a).then(() => spy(a, b))); + + await seq(...executors)(0); + + expect(spy).toHaveBeenCalledTimes(3); + + expect(spy).toHaveBeenNthCalledWith(1, LIST[0], 0); + expect(spy).toHaveBeenNthCalledWith(2, LIST[1], LIST[0]); + expect(spy).toHaveBeenNthCalledWith(3, LIST[2], LIST[0] + LIST[1]); + }); + + test("should return the result of the last given task", async () => { + const executors = LIST.map(a => (b: number) => timeout(a).then(() => a + b)); + + const result = await seq(...executors)(0); + expect(result).toBe(LIST[0] + LIST[1] + LIST[2]); + }); + + test("should resolved with void if empty array is passed", async () => { + const res = await seq()(); + expect(res).toBeUndefined(); + }); + + test("should stop if an executor throws", async () => { + const spy = jest.fn(); + const err = new Error("test"); + + let i = 0; + + const executors = LIST.map(num => jest.fn(() => { + if (i++ === 1) { + throw err; + } + + return timeout(num).then(() => spy(num)); + })); + + await expect(seq(...executors)()) + .rejects.toBe(err); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(LIST[0]); + + expect(executors[0]).toHaveBeenCalledTimes(1); + expect(executors[1]).toHaveBeenCalledTimes(1); + expect(executors[2]).not.toHaveBeenCalled() + }); + + }); + +}); diff --git a/tests/series.spec.ts b/tests/series.spec.ts new file mode 100644 index 0000000..31436ee --- /dev/null +++ b/tests/series.spec.ts @@ -0,0 +1,121 @@ +import series from "../lib/series"; + +import timeout from "./helpers/timeout"; +import createExecutorObject from "./helpers/createExecutorObject"; + +const LIST = [15, 1, 8]; + +describe("promizr.series()", () => { + + describe("with an array", () => { + + test("should call each provided methods with given values in series", async () => { + const spy = jest.fn(); + + await series(LIST.map(num => () => timeout(num).then(() => spy(num)))); + + expect(spy).toHaveBeenCalledTimes(3); + + for (let i = 0; i < LIST.length; i++) { + expect(spy).toHaveBeenNthCalledWith(i + 1, LIST[i]); + } + }); + + test("should return an array with each values returned by executors", async () => { + const spy = jest.fn(num => num * num); + const executors = LIST.map(num => () => timeout(num).then(() => spy(num))); + + const results = await series(executors); + + expect(spy).toHaveBeenCalledTimes(3); + + expect(results.length).toBe(3); + expect(results[0]).toBe(LIST[0] * LIST[0]); + expect(results[1]).toBe(LIST[1] * LIST[1]); + expect(results[2]).toBe(LIST[2] * LIST[2]); + + }); + + test("should stop if an executor throws", async () => { + const spy = jest.fn(); + const err = new Error("test"); + + const executors = LIST.map(num => jest.fn(() => { + if (num === LIST[1]) { + return Promise.reject(err); + } + + return timeout(num).then(() => spy(num)); + })); + + await expect(series(executors)) + .rejects.toBe(err); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(LIST[0]); + + expect(executors[0]).toHaveBeenCalledTimes(1); + expect(executors[1]).toHaveBeenCalledTimes(1); + expect(executors[2]).not.toHaveBeenCalled(); + + }); + + }); + + describe("whith an object argument", () => { + + test("should call each provided methods with given values in series", async () => { + const spy = jest.fn(); + const executors = createExecutorObject(LIST, num => () => timeout(num).then(() => spy(num)), true); + + await series(executors); + + expect(spy).toHaveBeenCalledTimes(3); + + for (let i = 0; i < LIST.length; i++) { + expect(spy).toHaveBeenNthCalledWith(i + 1, LIST[i]); + } + }); + + test("should return an object with each values returned by executors", async () => { + const spy = jest.fn(num => num * num); + const executors = createExecutorObject(LIST, num => () => timeout(num).then(() => spy(num))); + + const results = await series(executors); + expect(spy).toHaveBeenCalledTimes(3); + + expect(Object.keys(results).length).toBe(3); + + expect(results[`property-${LIST[0]}`]).toBe(LIST[0] * LIST[0]); + expect(results[`property-${LIST[1]}`]).toBe(LIST[1] * LIST[1]); + expect(results[`property-${LIST[2]}`]).toBe(LIST[2] * LIST[2]); + + }); + + test("should stop if an executor throws", async () => { + const spy = jest.fn(); + const err = new Error("test"); + + let count = 0; + + const executors = createExecutorObject(LIST, num => jest.fn(() => { + return timeout(num).then(() => { + if (count++ > 0) { + throw err; + } + + return spy(num); + }); + })); + + await expect(series(executors)) + .rejects.toBe(err); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(LIST[0]) + + }); + + }); + +}); diff --git a/tests/some.spec.ts b/tests/some.spec.ts new file mode 100644 index 0000000..78e011b --- /dev/null +++ b/tests/some.spec.ts @@ -0,0 +1,80 @@ +import some from "../lib/some"; + +import timeout from "./helpers/timeout"; +import sort from "./helpers/sort"; + +const LIST = [15, 1, 8]; +const STOP_LIST = [251, 1, 50]; + +describe("promizr.some()", () => { + + test("should call each provided methods with given values in parallel", async () => { + const spy = jest.fn((num, i, l) => false); + + await some(LIST, (num, i, list) => timeout(num).then(() => spy(num, i, list))); + expect(spy).toHaveBeenCalledTimes(3); + + const ordered = sort(LIST); + for (let i = 0; i < LIST.length; i++) { + expect(spy).toHaveBeenNthCalledWith(i + 1, ordered[i], LIST.indexOf(ordered[i]), LIST); + } + }); + + test("should return true if any result is found", async () => { + const spy = jest.fn(num => num % 2 === 1); + + const result = await some(LIST, num => timeout(num).then(() => spy(num))); + expect(spy).toHaveBeenCalledTimes(1); + expect(result).toBe(true); + }); + + test("should return false if no result is found", async () => { + const spy = jest.fn(num => false); + + const result = await some(LIST, num => timeout(num).then(() => spy(num))); + expect(spy).toHaveBeenCalledTimes(3); + expect(result).toBe(false); + }); + + test("should stop if an item is found", async () => { + const spy = jest.fn(num => num % 2 === 1); + const iterator = jest.fn(num => timeout(num).then(() => spy(num))); + + await some(STOP_LIST, iterator); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(STOP_LIST[1]); + + expect(iterator).toHaveBeenCalledTimes(3); + expect(iterator).toHaveBeenNthCalledWith(1, STOP_LIST[0], 0, STOP_LIST); + expect(iterator).toHaveBeenNthCalledWith(2, STOP_LIST[1], 1, STOP_LIST); + expect(iterator).toHaveBeenNthCalledWith(3, STOP_LIST[2], 2, STOP_LIST); + }); + + test("should stop if an iterator throws", async () => { + const spy = jest.fn(num => false); + const err = new Error("test"); + + const iterator = jest.fn(num => { + return timeout(num).then(() => { + if (num === STOP_LIST[2]) { + return Promise.reject(err); + } + + return spy(num); + }); + }); + + await expect(some(STOP_LIST, iterator)) + .rejects.toBe(err); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(STOP_LIST[1]); + + expect(iterator).toHaveBeenCalledTimes(3); + expect(iterator).toHaveBeenNthCalledWith(1, STOP_LIST[0], 0, STOP_LIST); + expect(iterator).toHaveBeenNthCalledWith(2, STOP_LIST[1], 1, STOP_LIST); + expect(iterator).toHaveBeenNthCalledWith(3, STOP_LIST[2], 2, STOP_LIST); + }); + +}); diff --git a/tests/sortBy.spec.ts b/tests/sortBy.spec.ts new file mode 100644 index 0000000..905c7d7 --- /dev/null +++ b/tests/sortBy.spec.ts @@ -0,0 +1,69 @@ +import sortBy from "../lib/sortBy"; + +import timeout from "./helpers/timeout"; +import sort from "./helpers/sort"; + +const LIST = [15, 1, 8]; +const LIST_WITH_DUPLICATES = [15, 1, 8, 8]; +const STOP_LIST = [251, 1, 50]; + +describe("sortBy", () => { + + test("should call each provided methods with given values in parallel", async () => { + const spy = jest.fn((num, i, l) => num); + + await sortBy(LIST, (num, i, list) => timeout(num).then(() => spy(num, i, list))); + expect(spy).toHaveBeenCalledTimes(3); + + const ordered = sort(LIST); + for (let i = 0; i < LIST.length; i++) { + expect(spy).toHaveBeenNthCalledWith(i + 1, ordered[i], LIST.indexOf(ordered[i]), LIST); + } + }); + + test("should work with equals values", async () => { + const spy = jest.fn((num, i, l) => num); + + await sortBy(LIST_WITH_DUPLICATES, (num, i, list) => timeout(num).then(() => spy(num, i, list))); + expect(spy).toHaveBeenCalledTimes(4); + }); + + test("should return sorted array using iterator projection", async () => { + const spy = jest.fn(num => num); + + const results = await sortBy(LIST, num => timeout(num).then(() => spy(num))); + expect(spy).toHaveBeenCalledTimes(3); + + expect(results).toHaveLength(3); + expect(results[0]).toBe(LIST[1]); + expect(results[1]).toBe(LIST[2]); + expect(results[2]).toBe(LIST[0]); + }); + + test("should stop if an iterator throws", async () => { + const spy = jest.fn(num => num * num); + const err = new Error("test"); + + const iterator = jest.fn(num => { + return timeout(num).then(() => { + if (num === STOP_LIST[2]) { + return Promise.reject(err); + } + + spy(num); + }); + }); + + await expect(sortBy(STOP_LIST, iterator)) + .rejects.toBe(err); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(STOP_LIST[1]); + + expect(iterator).toHaveBeenCalledTimes(3); + expect(iterator).toHaveBeenNthCalledWith(1, STOP_LIST[0], 0, STOP_LIST); + expect(iterator).toHaveBeenNthCalledWith(2, STOP_LIST[1], 1, STOP_LIST); + expect(iterator).toHaveBeenNthCalledWith(3, STOP_LIST[2], 2, STOP_LIST); + }); + +}); diff --git a/tests/tap.spec.ts b/tests/tap.spec.ts new file mode 100644 index 0000000..b24659a --- /dev/null +++ b/tests/tap.spec.ts @@ -0,0 +1,44 @@ +import tap from "../lib/tap"; + +import timeout from "./helpers/timeout"; + +describe("promizr.tap()", () => { + + test("should returns a function that executes the task when called", async () => { + const spy = jest.fn(); + + const fn = tap(spy); + + expect(spy).not.toHaveBeenCalled(); + + await fn("arg"); + + expect(spy).toHaveBeenCalledTimes(1); + }); + + test("should the function returns the given argument", async () => { + const argument = { arg: "value" }; + + const res = await tap(timeout, 10)(argument); + + expect(res).toBe(argument); + }); + + test("should the function apply passed args to task", async () => { + const spy = jest.fn((num, arg) => timeout(num)); + + await tap(spy, 10, "other")("arg"); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(10, "other"); + }); + + test("should stop if task throws", async () => { + const err = new Error("test"); + const spy = jest.fn(() => { throw err; }); + + await expect(tap(spy)("arg")) + .rejects.toBe(err); + }); + +}); diff --git a/tests/tapOn.spec.ts b/tests/tapOn.spec.ts new file mode 100644 index 0000000..83635a3 --- /dev/null +++ b/tests/tapOn.spec.ts @@ -0,0 +1,122 @@ +import tapOn from "../lib/tapOn"; + +import timeout from "./helpers/timeout"; + +describe("promizr.tapOn()", () => { + + describe("with task as string", () => { + + test("should returns a function that executes the task when called", async () => { + const owner = { + "task": jest.fn() + }; + + const fn = tapOn(owner, "task"); + + expect(owner.task).not.toHaveBeenCalled(); + + await fn("arg"); + + expect(owner.task).toHaveBeenCalledTimes(1); + }); + + test("should use the owner as this context when the task is called", async () => { + const owner = { + "task": jest.fn(function (this: unknown) { + expect(this).toBe(owner); + }) + }; + + await tapOn(owner, "task")("arg"); + + expect(owner.task).toHaveBeenCalledTimes(1); + }); + + test("should the function returns the given argument", async () => { + const owner = { + "task": jest.fn() + }; + + const argument = { arg: "value" }; + + const res = await tapOn(owner.task, timeout, 10)(argument); + + expect(res).toBe(argument); + }); + + test("should the function apply passed args to task", async () => { + const owner = { + "task": jest.fn((num, arg) => timeout(num)) + }; + + await tapOn(owner, "task", 10, "other")("arg"); + + expect(owner.task).toHaveBeenCalledTimes(1); + expect(owner.task).toHaveBeenCalledWith(10, "other"); + }); + + test("should stop if task throws", async () => { + const err = new Error("test"); + const owner = { + "task": jest.fn(() => { throw err; }) + }; + + await expect(tapOn(owner, "task")("arg")) + .rejects.toBe(err); + }); + + }); + + describe("with task as function", () => { + const owner = {}; + + test("should returns a function that executes the task when called", async () => { + const spy = jest.fn(); + + const fn = tapOn(owner, spy); + + expect(spy).not.toHaveBeenCalled(); + + await fn("arg"); + + expect(spy).toHaveBeenCalledTimes(1); + }); + + test("should use the owner as this context when the task is called", async () => { + const spy = jest.fn(function (this: unknown) { + expect(this).toBe(owner); + }); + + await tapOn(owner, spy)("arg"); + + expect(spy).toHaveBeenCalledTimes(1); + }); + + test("should the function returns the given argument", async () => { + const argument = { arg: "value" }; + + const res = await tapOn(owner, timeout, 10)(argument); + + expect(res).toBe(argument); + }); + + test("should the function apply passed args to task", async () => { + const spy = jest.fn((num, arg) => timeout(num)); + + await tapOn(owner, spy, 10, "other")("arg"); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(10, "other"); + }); + + test("should stop if task throws", async () => { + const err = new Error("test"); + const spy = jest.fn(() => { throw err; }); + + await expect(tapOn(owner, spy)("arg")) + .rejects.toBe(err); + }); + + }); + +}); diff --git a/tests/timeout.spec.ts b/tests/timeout.spec.ts new file mode 100644 index 0000000..b489824 --- /dev/null +++ b/tests/timeout.spec.ts @@ -0,0 +1,24 @@ +import timeout from "../lib/timeout"; + +jest.useFakeTimers(); + +describe("promizr.timeout()", () => { + + test("should return a Promise that resolves when timer is done", async () => { + const resolved = jest.fn(); + const promise = timeout(10).then(resolved); + + expect(setTimeout).toHaveBeenCalledTimes(1); + expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 10); + + expect(resolved).not.toHaveBeenCalled(); + + jest.runAllTimers(); + + await expect(promise).resolves.toBeUndefined(); + + expect(resolved).toHaveBeenCalledTimes(1); + expect(resolved).toHaveBeenCalledWith(undefined); + }); + +}); diff --git a/tests/times.spec.ts b/tests/times.spec.ts new file mode 100644 index 0000000..ff68f4b --- /dev/null +++ b/tests/times.spec.ts @@ -0,0 +1,87 @@ +import times from "../lib/times"; + +import timeout from "./helpers/timeout"; +import sort from "./helpers/sort"; + +const LIST = [15, 1, 8]; +const STOP_LIST = [251, 1, 50]; + +describe("promizr.times()", () => { + + test("should call provided function a given number of times", async () => { + let count = 0; + const spy = jest.fn(() => LIST[count++]); + + await times(3, spy); + + expect(spy).toHaveBeenCalledTimes(3); + expect(count).toBe(3); + }); + + test("should make all calls in parallel", async () => { + let count = 0; + + const spy = jest.fn(); + const task = () => { + const num = LIST[count++]; + return timeout(num).then(() => spy(num)); + }; + + await times(3, task); + + expect(spy).toHaveBeenCalledTimes(3); + expect(count).toBe(3); + + const ordered = sort(LIST); + for (let i = 0; i < LIST.length; i++) { + expect(spy).toHaveBeenNthCalledWith(i + 1, ordered[i]); + } + }); + + test("should return an array with each values returned by tasks", async () => { + let count = 0; + + const spy = jest.fn(num => num * num); + const task = () => { + const num = LIST[count++]; + return timeout(num).then(() => spy(num)); + }; + + const results = await times(3, task); + expect(spy).toHaveBeenCalledTimes(3); + + expect(results).toHaveLength(3); + expect(results[0]).toBe(LIST[0] * LIST[0]); + expect(results[1]).toBe(LIST[1] * LIST[1]); + expect(results[2]).toBe(LIST[2] * LIST[2]); + + }); + + test("should stop if a task throws", async () => { + let count = 0; + + const err = new Error("test"); + const spy = jest.fn(); + + const task = jest.fn(() => { + const num = STOP_LIST[count++]; + + return timeout(num).then(() => { + if (num === STOP_LIST[2]) { + throw err; + } + + spy(num); + }); + }); + + await expect(times(3, task)) + .rejects.toBe(err); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(STOP_LIST[1]); + + expect(task).toHaveBeenCalledTimes(3); + }); + +}); diff --git a/tests/timesSeries.spec.ts b/tests/timesSeries.spec.ts new file mode 100644 index 0000000..693ba35 --- /dev/null +++ b/tests/timesSeries.spec.ts @@ -0,0 +1,84 @@ +import timesSeries from "../lib/timesSeries"; + +import timeout from "./helpers/timeout"; + +const LIST = [15, 1, 8]; + +describe("promizr.timesSeries()", () => { + + test("should call provided function a given number of times", async () => { + let count = 0; + const spy = jest.fn(() => LIST[count++]); + + await timesSeries(3, spy); + + expect(spy).toHaveBeenCalledTimes(3); + expect(count).toBe(3); + }); + + test("should make all calls in series", async () => { + let count = 0; + + const spy = jest.fn(); + const task = () => { + const num = LIST[count++]; + return timeout(num).then(() => spy(num)); + }; + + await timesSeries(3, task); + + expect(spy).toHaveBeenCalledTimes(3); + expect(count).toBe(3); + + for (let i = 0; i < LIST.length; i++) { + expect(spy).toHaveBeenNthCalledWith(i + 1, LIST[i]); + } + }); + + test("should return an array with each values returned by tasks", async () => { + let count = 0; + + const spy = jest.fn(num => num * num); + const task = () => { + const num = LIST[count++]; + return timeout(num).then(() => spy(num)); + }; + + const results = await timesSeries(3, task); + expect(spy).toHaveBeenCalledTimes(3); + + expect(results).toHaveLength(3); + expect(results[0]).toBe(LIST[0] * LIST[0]); + expect(results[1]).toBe(LIST[1] * LIST[1]); + expect(results[2]).toBe(LIST[2] * LIST[2]); + + }); + + test("should stop if a task throws", async () => { + let count = 0; + + const err = new Error("test"); + const spy = jest.fn(); + + const task = jest.fn(() => { + const num = LIST[count++]; + + return timeout(num).then(() => { + if (num === LIST[1]) { + throw err; + } + + spy(num); + }); + }); + + await expect(timesSeries(3, task)) + .rejects.toBe(err); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(LIST[0]); + + expect(task).toHaveBeenCalledTimes(2); + }); + +}); diff --git a/tests/uncallbackify.spec.ts b/tests/uncallbackify.spec.ts new file mode 100644 index 0000000..0c0a5ce --- /dev/null +++ b/tests/uncallbackify.spec.ts @@ -0,0 +1,107 @@ +import uncallbackify from "../lib/uncallbackify"; + +describe("promizr.uncallbackify()", () => { + + describe("with no owner", () => { + + test("should call the inner function with the generated callbacks", async () => { + const spy = jest.fn(multiCallbackFunction); + + const fn = uncallbackify(spy, "result", false, null); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith("result", false, null, expect.any(Function), expect.any(Function)); + }); + + test("should return the result of the callback function", async () => { + const res = await uncallbackify(multiCallbackFunction, "result", false, null); + expect(res).toBe("result"); + }); + + test("should return the result of the callback function", async () => { + const res = await uncallbackify(multiCallbackFunction, "result", false, null); + expect(res).toBe("result"); + }); + + test("should return an array of the callback function results if it has multiple results", async () => { + const res = await uncallbackify(multiCallbackFunction, "result", true, null); + expect(res).toEqual(["result", true]); + }); + + test("should return void if the callback function has no result", async () => { + const res = await uncallbackify(multiCallbackFunction, undefined, true, null); + expect(res).toBeUndefined(); + }); + + test("should throw if the callback function returns an error", async () => { + const err = new Error("test"); + + await expect(uncallbackify(multiCallbackFunction, "result", false, err)) + .rejects.toBe(err); + }); + }); + + describe("with owner", () => { + const owner = {}; + + const thisMultiCallbackFunction = function (this: unknown, ...args: Parameters): void { + expect(this).toBe(owner); + multiCallbackFunction(...args); + }; + + test("should return a function that calls the inner function with the generated callbacks", async () => { + const spy = jest.fn(thisMultiCallbackFunction); + + const fn = uncallbackify(owner, spy, "result", false, null); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith("result", false, null, expect.any(Function), expect.any(Function)); + }); + + test("should return the result of the callback function", async () => { + const res = await uncallbackify(owner, thisMultiCallbackFunction, "result", false, null); + expect(res).toBe("result"); + }); + + test("should return the result of the callback function", async () => { + const res = await uncallbackify(owner, thisMultiCallbackFunction, "result", false, null); + expect(res).toBe("result"); + }); + + test("should return an array of the callback function results if it has multiple results", async () => { + const res = await uncallbackify(owner, thisMultiCallbackFunction, "result", true, null); + expect(res).toEqual(["result", true]); + }); + + test("should return void if the callback function has no result", async () => { + const res = await uncallbackify(owner, thisMultiCallbackFunction, undefined, true, null); + expect(res).toBeUndefined(); + }); + + test("should throw if the callback function returns an error", async () => { + const err = new Error("test"); + + await expect(uncallbackify(owner, thisMultiCallbackFunction, "result", false, err)) + .rejects.toBe(err); + }); + }); + +}); + +function multiCallbackFunction(res: string | undefined, multi: boolean | null | undefined, throws: Error | null | undefined, successCallback: (...args: any[]) => void, errorCallback: (err: Error) => void): void { + if (throws) { + setImmediate(() => errorCallback(throws)); + return; + } + + if (typeof res === "undefined") { + setImmediate(() => successCallback()); + } + + if (multi) { + setImmediate(() => successCallback(res, true)); + return; + } + + setImmediate(() => successCallback(res)); +} diff --git a/tests/until.spec.ts b/tests/until.spec.ts new file mode 100644 index 0000000..f35f722 --- /dev/null +++ b/tests/until.spec.ts @@ -0,0 +1,95 @@ +import until from "../lib/until"; + +import timeout from "./helpers/timeout"; + +const LIST = [15, 1, 8]; + +describe("promizr.until()", () => { + + test("should call provided method while given test pass", async () => { + const spy = jest.fn(); + let i = 0; + + const test = jest.fn(() => LIST[i] % 2 === 0); + const executor = () => { + const num = LIST[i++]; + return timeout(num).then(() => spy(num)); + }; + + await until(test, executor); + + expect(spy).toHaveBeenCalledTimes(2); + expect(test).toHaveBeenCalledTimes(3); + + expect(spy).toHaveBeenNthCalledWith(1, LIST[0]); + expect(spy).toHaveBeenNthCalledWith(2, LIST[1]); + + }); + + test("should accept a Promise as test result", async () => { + let i = 0; + + const test = jest.fn(() => Promise.resolve(LIST[i] % 2 === 0)); + const executor = jest.fn(() => timeout(LIST[i++])); + + await until(test, executor); + + expect(executor).toHaveBeenCalledTimes(2); + expect(test).toHaveBeenCalledTimes(3); + }); + + test("should stop if an executor throws", async () => { + const spy = jest.fn(); + const err = new Error("test"); + + let i = 0; + + const test = jest.fn(() => false); + const executor = jest.fn(() => { + const num = LIST[i++]; + if (num === LIST[1]) { + throw err; + } + + return timeout(num).then(() => spy(num)); + }); + + await expect(until(test, executor)) + .rejects.toBe(err); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(LIST[0]); + + expect(test).toHaveBeenCalledTimes(2); + expect(executor).toHaveBeenCalledTimes(2); + }); + + test("should stop if a test throws", async () => { + const spy = jest.fn(); + const err = new Error("test"); + + let i = 0; + + const test = jest.fn(() => { + if (i === 1) { + throw err; + } + return false; + }); + + const executor = jest.fn(() => { + const num = LIST[i++]; + return timeout(num).then(() => spy(num)); + }); + + await expect(until(test, executor)) + .rejects.toBe(err); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(LIST[0]); + + expect(test).toHaveBeenCalledTimes(2); + expect(executor).toHaveBeenCalledTimes(1); + }); + +}); diff --git a/tests/waterfall.spec.ts b/tests/waterfall.spec.ts new file mode 100644 index 0000000..fcae317 --- /dev/null +++ b/tests/waterfall.spec.ts @@ -0,0 +1,57 @@ +import waterfall from "../lib/waterfall"; + +import timeout from "./helpers/timeout"; + +const LIST = [15, 1, 8] as const; + +describe("waterfall function", () => { + + test("should call each task with previous task result as argument", async () => { + const spy = jest.fn((a, b) => a + b); + const executors = LIST.map(a => (b: number | undefined) => timeout(a).then(() => spy(a, b || 0))); + + await waterfall(executors); + + expect(spy).toHaveBeenCalledTimes(3); + + expect(spy).toHaveBeenNthCalledWith(1, LIST[0], 0); + expect(spy).toHaveBeenNthCalledWith(2, LIST[1], LIST[0]); + expect(spy).toHaveBeenNthCalledWith(3, LIST[2], LIST[0] + LIST[1]); + }); + + test("should return the result of the last task", async () => { + const spy = jest.fn((a, b) => a + b); + const executors = LIST.map(a => (b: number | undefined) => timeout(a).then(() => spy(a, b || 0))); + + const result = await waterfall(executors); + + expect(result).toBe(LIST[0] + LIST[1] + LIST[2]); + }); + + test("should stop if an executor throws", async () => { + const spy = jest.fn(); + const err = new Error("test"); + + let i = 0; + + const executors = LIST.map(num => jest.fn(() => { + if (i++ === 1) { + throw err; + } + + return timeout(num).then(() => spy(num)); + })); + + await expect(waterfall(executors)) + .rejects.toBe(err); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(LIST[0]); + + expect(executors[0]).toHaveBeenCalledTimes(1); + expect(executors[1]).toHaveBeenCalledTimes(1); + expect(executors[2]).not.toHaveBeenCalled(); + + }); + +}); \ No newline at end of file diff --git a/tests/whilst.spec.ts b/tests/whilst.spec.ts new file mode 100644 index 0000000..d40cc80 --- /dev/null +++ b/tests/whilst.spec.ts @@ -0,0 +1,95 @@ +import whilst from "../lib/whilst"; + +import timeout from "./helpers/timeout"; + +const LIST = [15, 1, 8]; + +describe("promizr.whilst()", () => { + + test("should call provided method while given test pass", async () => { + const spy = jest.fn(); + let i = 0; + + const test = jest.fn(() => LIST[i] % 2 === 1); + const executor = () => { + const num = LIST[i++]; + return timeout(num).then(() => spy(num)); + }; + + await whilst(test, executor); + + expect(spy).toHaveBeenCalledTimes(2); + expect(test).toHaveBeenCalledTimes(3); + + expect(spy).toHaveBeenNthCalledWith(1, LIST[0]); + expect(spy).toHaveBeenNthCalledWith(2, LIST[1]); + + }); + + test("should accept a Promise as test result", async () => { + let i = 0; + + const test = jest.fn(() => Promise.resolve(LIST[i] % 2 !== 0)); + const executor = jest.fn(() => timeout(LIST[i++])); + + await whilst(test, executor); + + expect(executor).toHaveBeenCalledTimes(2); + expect(test).toHaveBeenCalledTimes(3); + }); + + test("should stop if an executor throws", async () => { + const spy = jest.fn(); + const err = new Error("test"); + + let i = 0; + + const test = jest.fn(() => true); + const executor = jest.fn(() => { + const num = LIST[i++]; + if (num === LIST[1]) { + throw err; + } + + return timeout(num).then(() => spy(num)); + }); + + await expect(whilst(test, executor)) + .rejects.toBe(err); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(LIST[0]); + + expect(test).toHaveBeenCalledTimes(2); + expect(executor).toHaveBeenCalledTimes(2); + }); + + test("should stop if a test throws", async () => { + const spy = jest.fn(); + const err = new Error("test"); + + let i = 0; + + const test = jest.fn(() => { + if (i === 1) { + throw err; + } + return true; + }); + + const executor = jest.fn(() => { + const num = LIST[i++]; + return timeout(num).then(() => spy(num)); + }); + + await expect(whilst(test, executor)) + .rejects.toBe(err); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(LIST[0]); + + expect(test).toHaveBeenCalledTimes(2); + expect(executor).toHaveBeenCalledTimes(1); + }); + +}); diff --git a/tsconfig.json b/tsconfig.json index e242a7a..9174e4d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,7 @@ ], "strict": true, "declaration": true, + "esModuleInterop": true, "sourceMap": false }, "exclude": [