From 43680fa6eb2b68f789300f0f8a0f9e1e69ee802b Mon Sep 17 00:00:00 2001 From: Alexis Mineaud Date: Mon, 16 Jan 2023 13:46:59 +0100 Subject: [PATCH 01/20] Allow adding identifiers of each fns additions. It allows having unique fns indexed by identifiers. --- .github/workflows/github-actions.yml | 2 +- package.json | 4 +- .../composers/__tests__/withLogs.test.ts | 4 +- src/AsyncProcess/composers/withLogs.ts | 6 +- src/AsyncProcess/index.ts | 70 ++++---- src/__tests__/AsyncProcess.test.ts | 159 +++++++++++++----- src/types/index.ts | 8 +- 7 files changed, 167 insertions(+), 86 deletions(-) diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index f72bbb2..2ffb0b4 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -21,6 +21,6 @@ jobs: with: node-version: ${{ matrix.node-version }} - run: npm ci - - run: npm run check + - run: npm run check:code - run: npm test - run: npm run build --if-present diff --git a/package.json b/package.json index 26dc157..1ddc33c 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,9 @@ "types": "dist/types/index.d.ts", "scripts": { "build": "rm -rf dist && mkdir dist && tsc --project tsconfig.build.json", - "check": "tsc --noEmit", + "check:code": "tsc --noEmit", "lint": "eslint .", - "prepublishOnly": "npm run check && npm run lint && npm t && npm run build", + "prepublishOnly": "npm run check:code && npm run lint && npm t && npm run build", "test": "jest" }, "devDependencies": { diff --git a/src/AsyncProcess/composers/__tests__/withLogs.test.ts b/src/AsyncProcess/composers/__tests__/withLogs.test.ts index 4d0fb09..2664eb7 100644 --- a/src/AsyncProcess/composers/__tests__/withLogs.test.ts +++ b/src/AsyncProcess/composers/__tests__/withLogs.test.ts @@ -28,7 +28,7 @@ describe('Composers', () => { const logger = jest.fn() await getAsyncProcessTestInstance('loadFoo') - .do(() => doSomething()) + .do(doSomething) .compose(withLogs(logger)) .start() @@ -65,7 +65,7 @@ describe('Composers', () => { const logger3 = jest.fn() await getAsyncProcessTestInstance('loadFoo') - .do(() => doSomething()) + .do(doSomething) .compose(withLogs(logger1)) .compose(withLogs(logger2, 'secondLogger')) .compose(withLogs(logger3, 'thirdLogger')) diff --git a/src/AsyncProcess/composers/withLogs.ts b/src/AsyncProcess/composers/withLogs.ts index 917d1bc..53451dc 100644 --- a/src/AsyncProcess/composers/withLogs.ts +++ b/src/AsyncProcess/composers/withLogs.ts @@ -36,12 +36,12 @@ export function withLogs( ]) .onStart(() => { logger('Start %s', asyncProcess.identifier) - }) + }, identifier) .onSuccess(() => { logger('Success %s', asyncProcess.identifier) - }) + }, identifier) .onError(err => { logger('Error %s: %o', asyncProcess.identifier, err) - }) + }, identifier) } } diff --git a/src/AsyncProcess/index.ts b/src/AsyncProcess/index.ts index 28299b7..e6a0e18 100644 --- a/src/AsyncProcess/index.ts +++ b/src/AsyncProcess/index.ts @@ -22,10 +22,10 @@ export class AsyncProcess { private _error: Maybe = null private _fns: IAsyncProcessFns = { - asyncFns: new Set(), - onStartFns: new Set(), - onSuccessFns: new Set(), - onErrorFns: new Set() + asyncFns: new Map(), + onStartFns: new Map(), + onSuccessFns: new Map(), + onErrorFns: new Map() } private _predicateFns: Set> = new Set() @@ -60,8 +60,8 @@ export class AsyncProcess { /** * Save the async process function(s). */ - do(asyncFns: AsyncFns): this { - this._fns.asyncFns = new Set(ensureArray(asyncFns)) + do(asyncFns: AsyncFns, identifier = 'do'): this { + this._fns.asyncFns.set(identifier, new Set(ensureArray(asyncFns))) return this } @@ -76,33 +76,24 @@ export class AsyncProcess { /** * Save functions to execute before starting the async process. */ - onStart(asyncFns: AsyncFns): this { - new Set(ensureArray(asyncFns)).forEach(fn => { - this._fns.onStartFns.add(fn) - }) - + onStart(asyncFns: AsyncFns, identifier = 'onStart'): this { + this._fns.onStartFns.set(identifier, new Set(ensureArray(asyncFns))) return this } /** * Save functions to execute after the async process if succeeded. */ - onSuccess(asyncFns: AsyncFns): this { - new Set(ensureArray(asyncFns)).forEach(fn => { - this._fns.onSuccessFns.add(fn) - }) - + onSuccess(asyncFns: AsyncFns, identifier = 'onSuccess'): this { + this._fns.onSuccessFns.set(identifier, new Set(ensureArray(asyncFns))) return this } /** * Save functions to execute after the async process if failed. */ - onError(asyncFns: AsyncErrorFns): this { - new Set(ensureArray(asyncFns)).forEach(fn => { - this._fns.onErrorFns.add(fn) - }) - + onError(asyncFns: AsyncErrorFns, identifier = 'onError'): this { + this._fns.onErrorFns.set(identifier, new Set(ensureArray(asyncFns))) return this } @@ -114,19 +105,22 @@ export class AsyncProcess { asyncProcess: AsyncProcess ) => AsyncProcess ): this { - const fnTypes: Array = [ - 'asyncFns', - 'onStartFns', - 'onSuccessFns', - 'onErrorFns' - ] - const asyncProcessFns = asyncProcessComposer(this).fns - fnTypes.forEach(fnType => { - asyncProcessFns[fnType].forEach(fn => { - this._fns[fnType].add(fn as AsyncFn) - }) + asyncProcessFns.asyncFns.forEach((fns, identifier) => { + this._fns.asyncFns.set(identifier, fns) + }) + + asyncProcessFns.onStartFns.forEach((fns, identifier) => { + this._fns.onStartFns.set(identifier, fns) + }) + + asyncProcessFns.onSuccessFns.forEach((fns, identifier) => { + this._fns.onSuccessFns.set(identifier, fns) + }) + + asyncProcessFns.onErrorFns.forEach((fns, identifier) => { + this._fns.onErrorFns.set(identifier, fns) }) return this @@ -200,8 +194,12 @@ export class AsyncProcess { /** * Execute sequentially async functions. */ - private _execAsyncFns(asyncFns: Set): Promise { + private _execAsyncFns(asyncFns: Map>): Promise { return Array.from(asyncFns.values()) + .reduce( + (acc, fns_) => acc.concat(Array.from(fns_.values())), + [] + ) .reduce( (promise, nextFn) => promise.then(() => nextFn()), Promise.resolve(null) @@ -214,9 +212,13 @@ export class AsyncProcess { */ private _execAsyncErrorFns( err: Error, - asyncErrorFns: Set + asyncErrorFns: Map> ): Promise { return Array.from(asyncErrorFns.values()) + .reduce( + (acc, fns_) => acc.concat(Array.from(fns_.values())), + [] + ) .reduce( (promise, nextFn) => promise.then(() => nextFn(err)), Promise.resolve(null) diff --git a/src/__tests__/AsyncProcess.test.ts b/src/__tests__/AsyncProcess.test.ts index 50ee95b..0dbe34e 100644 --- a/src/__tests__/AsyncProcess.test.ts +++ b/src/__tests__/AsyncProcess.test.ts @@ -43,7 +43,7 @@ describe('AsyncProcess', () => { const onErrorFn = jest.fn() const asyncProcess = getAsyncProcessTestInstance('loadFoo') - .do(() => fetchData()) + .do(fetchData) .onStart(onStartFns) .onSuccess(onSuccessFns) .onError(onErrorFn) @@ -86,7 +86,7 @@ describe('AsyncProcess', () => { const onErrorFn = jest.fn() const asyncProcess = getAsyncProcessTestInstance('loadFoo') - .do(() => fetchData()) + .do(fetchData) .onStart(onStartFns) .onSuccess(onSuccessFns) .onError(onErrorFn) @@ -105,9 +105,7 @@ describe('AsyncProcess', () => { }) } - const asyncProcess = getAsyncProcessTestInstance('loadFoo').do(() => - fetchData() - ) + const asyncProcess = getAsyncProcessTestInstance('loadFoo').do(fetchData) const doSomethingAfterAsyncProcess = jest.fn() @@ -132,7 +130,7 @@ describe('AsyncProcess', () => { const onErrorFn = jest.fn() const asyncProcess = getAsyncProcessTestInstance('loadFoo') - .do(() => fetchData()) + .do(fetchData) .onStart(onStartFns) .onSuccess(onSuccessFns) .onError(onErrorFn) @@ -164,7 +162,7 @@ describe('AsyncProcess', () => { const onErrorFn = jest.fn() const asyncProcess = getAsyncProcessTestInstance('loadFoo') - .do(() => fetchData()) + .do(fetchData) .onError(onErrorFn) await asyncProcess.start() @@ -181,9 +179,7 @@ describe('AsyncProcess', () => { }) } - const asyncProcess = getAsyncProcessTestInstance('loadFoo').do(() => - fetchData() - ) + const asyncProcess = getAsyncProcessTestInstance('loadFoo').do(fetchData) await asyncProcess.start() @@ -208,7 +204,7 @@ describe('AsyncProcess', () => { } const asyncProcess = getAsyncProcessTestInstance('loadFoo') - .do(() => fetchData()) + .do(fetchData) .onError(setErrorMessage('Something bad happened')) const doSomethingAfterAsyncProcess = jest.fn() @@ -220,6 +216,83 @@ describe('AsyncProcess', () => { }) }) + describe('Identifiers', () => { + describe('onStart', () => { + it('should use a default identifier so that fns are replaced by default', () => { + const startSpy1 = jest.fn() + const startSpy2 = jest.fn() + + const foo1 = getAsyncProcessTestInstance('loadFoo') + foo1.onStart(startSpy1) + + const foo2 = getAsyncProcessTestInstance('loadFoo') + foo2.onStart(startSpy1) + foo2.onStart(startSpy2) + + // register only `startSpy2` because added last + expect(foo1.fns.onStartFns.size).toBe(1) + }) + }) + + describe('onSuccess', () => { + it('should use a default identifier so that fns are replaced by default', () => { + const successSpy1 = jest.fn() + const successSpy2 = jest.fn() + + const foo1 = getAsyncProcessTestInstance('loadFoo') + foo1.onSuccess(successSpy1) + + const foo2 = getAsyncProcessTestInstance('loadFoo') + foo2.onSuccess(successSpy1) + foo2.onSuccess(successSpy2) + + // register only `successSpy2` because added last + expect(foo1.fns.onSuccessFns.size).toBe(1) + }) + + it('should allow using an identifier to add several fns', async () => { + const successSpy1 = jest.fn() + const successSpy2 = jest.fn() + const successSpy3 = jest.fn() + const successSpy4 = jest.fn() + + const foo1 = getAsyncProcessTestInstance('loadFoo') + foo1.onSuccess([successSpy1, successSpy2], 'success1') + + const foo2 = getAsyncProcessTestInstance('loadFoo') + foo2.onSuccess([successSpy3, successSpy4], 'success2') + + // register `[successSpy1, successSpy2]` and `[successSpy3, successSpy4]`, so 2 entries + expect(foo1.fns.onSuccessFns.size).toBe(2) + + await foo1.start() + + // check that the 4 fns have been called + expect(successSpy1).toHaveBeenCalled() + expect(successSpy2).toHaveBeenCalled() + expect(successSpy3).toHaveBeenCalled() + expect(successSpy4).toHaveBeenCalled() + }) + }) + + describe('onError', () => { + it('should use a default identifier so that fns are replaced by default', () => { + const errorSpy1 = jest.fn() + const errorSpy2 = jest.fn() + + const foo1 = getAsyncProcessTestInstance('loadFoo') + foo1.onError(errorSpy1) + + const foo2 = getAsyncProcessTestInstance('loadFoo') + foo2.onError(errorSpy1) + foo2.onError(errorSpy2) + + // register only `errorSpy2` because added last + expect(foo1.fns.onErrorFns.size).toBe(1) + }) + }) + }) + describe('Predicates', () => { it('should execute the async process if the predicate function is true', async () => { const fetchData = jest.fn() @@ -230,9 +303,9 @@ describe('AsyncProcess', () => { const onErrorFn = jest.fn() const asyncProcess = getAsyncProcessTestInstance('loadFoo') - .do(() => fetchData()) - .if(() => predicateFn1()) - .if(() => predicateFn2()) + .do(fetchData) + .if(predicateFn1) + .if(predicateFn2) .onStart(onStartFn) .onSuccess(onSuccessFn) .onError(onErrorFn) @@ -255,8 +328,8 @@ describe('AsyncProcess', () => { const onErrorFn = jest.fn() const asyncProcess = getAsyncProcessTestInstance('loadFoo') - .do(() => fetchData()) - .if(() => predicateFn1()) + .do(fetchData) + .if(predicateFn1) .onStart(onStartFn) .onSuccess(onSuccessFn) .onError(onErrorFn) @@ -280,10 +353,10 @@ describe('AsyncProcess', () => { const onErrorFn = jest.fn() const asyncProcess = getAsyncProcessTestInstance('loadFoo') - .do(() => fetchData()) - .if(() => predicateFn1()) - .if(() => predicateFn2()) - .if(() => predicateFn3()) + .do(fetchData) + .if(predicateFn1) + .if(predicateFn2) + .if(predicateFn3) .onStart(onStartFn) .onSuccess(onSuccessFn) .onError(onErrorFn) @@ -311,12 +384,14 @@ describe('AsyncProcess', () => { }) } - const onStartgFn1 = jest.fn() - const onStartgFn2 = jest.fn() + const onStartFn0 = jest.fn() + const onStartFn1 = jest.fn() + const onStartFn2 = jest.fn() + const onStartFn3 = jest.fn() + const onStartFn4 = jest.fn() + + const onSuccessFn = jest.fn() - // Add two same functions, should be unique at the end - const onStartFns = [onStartgFn1, onStartgFn1, onStartgFn2] - const onSuccessFns = [jest.fn()] const onErrorFn = jest.fn() let asyncProcessBaseIdentifiers @@ -326,10 +401,13 @@ describe('AsyncProcess', () => { ) => { const baseAsyncProcess = getAsyncProcessTestInstance( asyncProcess.identifier, - ['dep1', 'dep2'] + ['id1', 'id2'] ) - .onStart(onStartFns) - .onSuccess(onSuccessFns) + // define custom identifiers for each fns addition + .onStart(onStartFn1, 'onStartFn1') + .onStart(onStartFn2, 'onStartFn2') + .onStart([onStartFn3, onStartFn4], 'onStartFn3+onStartFn4') + .onSuccess(onSuccessFn) .onError(onErrorFn) asyncProcessBaseIdentifiers = baseAsyncProcess.identitiers @@ -338,30 +416,31 @@ describe('AsyncProcess', () => { } const asyncProcess = getAsyncProcessTestInstance('loadFoo') - .do(() => fetchData()) + .do(fetchData) + .onStart(onStartFn0, 'onStartFn0') .compose(asyncProcessExtended) await asyncProcess.start() - onStartFns.forEach(onStartFn => { - expect(onStartFn).toHaveBeenCalled() - }) + expect(onStartFn0).toHaveBeenCalled() + expect(onStartFn1).toHaveBeenCalled() + expect(onStartFn2).toHaveBeenCalled() + expect(onStartFn3).toHaveBeenCalled() + expect(onStartFn4).toHaveBeenCalled() - onSuccessFns.forEach(onSuccessFn => { - expect(onSuccessFn).toHaveBeenCalled() - }) + expect(onSuccessFn).toHaveBeenCalled() expect(onErrorFn).not.toHaveBeenCalled() - // Dedup same functions - expect(asyncProcess.fns.onStartFns.size).toEqual(2) + // Count the number of additions (by uniq identifiers) of onStartFns entries + expect(asyncProcess.fns.onStartFns.size).toEqual(4) expect(data).toEqual({ id: 1, name: 'Bob' }) - expect(asyncProcessBaseIdentifiers).toEqual(['loadFoo', 'dep1/dep2']) + expect(asyncProcessBaseIdentifiers).toEqual(['loadFoo', 'id1/id2']) }) }) @@ -370,7 +449,7 @@ describe('AsyncProcess', () => { const successSpy = jest.fn() const foo1 = getAsyncProcessTestInstance('loadFoo') - foo1.onSuccess(() => successSpy()) + foo1.onSuccess(successSpy) const foo2 = getAsyncProcessTestInstance('loadFoo') const bar1 = getAsyncProcessTestInstance('loadBar') @@ -390,7 +469,7 @@ describe('AsyncProcess', () => { const successSpy = jest.fn() const foo1 = getAsyncProcessTestInstance('loadFoo') - foo1.onSuccess(() => successSpy()) + foo1.onSuccess(successSpy) const foo1Dep1 = getAsyncProcessTestInstance('loadFoo', ['dep1']) const foo2Dep1 = getAsyncProcessTestInstance('loadFoo', ['dep1']) diff --git a/src/types/index.ts b/src/types/index.ts index 8cb6f94..c0819e4 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -16,10 +16,10 @@ export type PredicateFn = ( ) => boolean | Promise export interface IAsyncProcessFns { - asyncFns: Set - onStartFns: Set - onSuccessFns: Set - onErrorFns: Set + asyncFns: Map> + onStartFns: Map> + onSuccessFns: Map> + onErrorFns: Map> } export type AsyncProcessIdentifiers = [ From 0742364a6e25f88f1fcaeb2d90e440efaaa4f481 Mon Sep 17 00:00:00 2001 From: Alexis Mineaud Date: Thu, 19 Jan 2023 22:17:56 +0100 Subject: [PATCH 02/20] Add an option to delete functions after having started the async process. --- src/AsyncProcess/index.ts | 40 ++ src/__tests__/AsyncProcess.test.ts | 791 +++++++++++++++-------------- src/types/index.ts | 4 + 3 files changed, 458 insertions(+), 377 deletions(-) diff --git a/src/AsyncProcess/index.ts b/src/AsyncProcess/index.ts index e6a0e18..d9b4a77 100644 --- a/src/AsyncProcess/index.ts +++ b/src/AsyncProcess/index.ts @@ -6,6 +6,7 @@ import { AsyncFns, AsyncProcessIdentifiers, IAsyncProcessFns, + IAsyncProcessOptions, PredicateFn } from '../types' @@ -19,6 +20,14 @@ export class AsyncProcess { private _identifiers: AsyncProcessIdentifiers + private _options: IAsyncProcessOptions = { + /** + * When set to true, registered functions are deleted after AsyncProcess has been started. + * Useful when reusing a same instance of AsyncProcess to not have functions registered several times. + */ + deleteFunctionsWhenStarted: false + } + private _error: Maybe = null private _fns: IAsyncProcessFns = { @@ -57,6 +66,14 @@ export class AsyncProcess { ) } + /** + * Set AsyncProcess options. + */ + setOptions(options: IAsyncProcessOptions): this { + this._options = options + return this + } + /** * Save the async process function(s). */ @@ -149,12 +166,17 @@ export class AsyncProcess { try { if (this._predicateFns && !(await this.shouldStart())) { await this._execAsyncFns(this._fns.onSuccessFns) + + this.shouldResetFunctions() + return this } await this._execAsyncFns(this._fns.onStartFns) await this._execAsyncFns(this._fns.asyncFns) await this._execAsyncFns(this._fns.onSuccessFns) + + this.shouldResetFunctions() } catch (err) { this._error = err instanceof Error ? err : new Error('Unknown error') this._execAsyncErrorFns(this._error, this._fns.onErrorFns) @@ -175,6 +197,24 @@ export class AsyncProcess { return true } + /** + * Reset functions. + */ + shouldResetFunctions(): this { + if (!this._options.deleteFunctionsWhenStarted) { + return this + } + + this._fns = { + asyncFns: new Map(), + onStartFns: new Map(), + onSuccessFns: new Map(), + onErrorFns: new Map() + } + + return this + } + /** * Getters */ diff --git a/src/__tests__/AsyncProcess.test.ts b/src/__tests__/AsyncProcess.test.ts index 0dbe34e..8d64281 100644 --- a/src/__tests__/AsyncProcess.test.ts +++ b/src/__tests__/AsyncProcess.test.ts @@ -1,5 +1,6 @@ import { Maybe } from '@productive-codebases/toolbox' import { AsyncProcess } from '../AsyncProcess' +import { IAsyncProcessOptions } from '../types' interface IData { id: number @@ -8,486 +9,522 @@ interface IData { type AsyncProcessTestIdentifier = 'loadFoo' | 'loadBar' -function getAsyncProcessTestInstance( - identifier: AsyncProcessTestIdentifier, - subIdentifiers?: string[] -): AsyncProcess { - return AsyncProcess.instance(identifier, subIdentifiers) -} - describe('AsyncProcess', () => { - let data: Maybe = null + describe.each([ + { deleteFunctionsWhenStarted: false }, + { deleteFunctionsWhenStarted: true } + ])('According to options: %o', ({ deleteFunctionsWhenStarted }) => { + let data: Maybe = null + + const setData = (data_: any) => { + data = data_ + } + + function getAsyncProcessTestInstance( + identifier: AsyncProcessTestIdentifier, + subIdentifiers?: string[] + ): AsyncProcess { + return AsyncProcess.instance(identifier, subIdentifiers).setOptions({ + deleteFunctionsWhenStarted + }) + } - const setData = (data_: any) => { - data = data_ - } + beforeEach(() => { + data = null + AsyncProcess.clearInstances() + }) - beforeEach(() => { - data = null - AsyncProcess.clearInstances() - }) + describe('On success', () => { + it('should call the onStart / onSuccess fn(s)', async () => { + const fetchData = (): Promise => { + return new Promise(resolve => { + setTimeout(() => { + setData({ id: 1, name: 'Bob' }) + resolve(null) + }, 50) + }) + } + + const onStartFns = [jest.fn(), jest.fn()] + const onSuccessFns = [jest.fn()] + const onErrorFn = jest.fn() + + const asyncProcess = getAsyncProcessTestInstance('loadFoo') + .do(fetchData) + .onStart(onStartFns) + .onSuccess(onSuccessFns) + .onError(onErrorFn) - describe('On success', () => { - it('should call the onStart / onSuccess fn(s)', async () => { - const fetchData = (): Promise => { - return new Promise(resolve => { - setTimeout(() => { - setData({ id: 1, name: 'Bob' }) - resolve(null) - }, 50) - }) - } + await asyncProcess.start() - const onStartFns = [jest.fn(), jest.fn()] - const onSuccessFns = [jest.fn()] - const onErrorFn = jest.fn() + onStartFns.forEach(onStartFn => { + expect(onStartFn).toHaveBeenCalled() + }) - const asyncProcess = getAsyncProcessTestInstance('loadFoo') - .do(fetchData) - .onStart(onStartFns) - .onSuccess(onSuccessFns) - .onError(onErrorFn) + onSuccessFns.forEach(onSuccessFn => { + expect(onSuccessFn).toHaveBeenCalled() + }) - await asyncProcess.start() + expect(onErrorFn).not.toHaveBeenCalled() - onStartFns.forEach(onStartFn => { - expect(onStartFn).toHaveBeenCalled() + expect(data).toEqual({ + id: 1, + name: 'Bob' + }) }) - onSuccessFns.forEach(onSuccessFn => { - expect(onSuccessFn).toHaveBeenCalled() - }) + it('should call the functions sequentially', async () => { + const fetchData = (): Promise => { + return new Promise(resolve => { + setTimeout(() => { + resolve(null) + }, 50) + }) + } + + const successValues: number[] = [] + + const setSuccess = (value: number) => () => { + successValues.push(value) + } + + const onStartFns = [jest.fn(), jest.fn()] + const onSuccessFns = [setSuccess(2), setSuccess(1)] + const onErrorFn = jest.fn() + + const asyncProcess = getAsyncProcessTestInstance('loadFoo') + .do(fetchData) + .onStart(onStartFns) + .onSuccess(onSuccessFns) + .onError(onErrorFn) - expect(onErrorFn).not.toHaveBeenCalled() + await asyncProcess.start() - expect(data).toEqual({ - id: 1, - name: 'Bob' + expect(successValues).toEqual([2, 1]) }) - }) - it('should call the functions sequentially', async () => { - const fetchData = (): Promise => { - return new Promise(resolve => { - setTimeout(() => { - resolve(null) - }, 50) - }) - } + it('should return a success promise', async () => { + const fetchData = (): Promise => { + return new Promise(resolve => { + setTimeout(() => { + resolve(null) + }, 50) + }) + } - const successValues: number[] = [] + const asyncProcess = + getAsyncProcessTestInstance('loadFoo').do(fetchData) - const setSuccess = (value: number) => () => { - successValues.push(value) - } + const doSomethingAfterAsyncProcess = jest.fn() - const onStartFns = [jest.fn(), jest.fn()] - const onSuccessFns = [setSuccess(2), setSuccess(1)] - const onErrorFn = jest.fn() + await asyncProcess.start().then(() => doSomethingAfterAsyncProcess()) - const asyncProcess = getAsyncProcessTestInstance('loadFoo') - .do(fetchData) - .onStart(onStartFns) - .onSuccess(onSuccessFns) - .onError(onErrorFn) + expect(doSomethingAfterAsyncProcess).toHaveBeenCalled() + }) + }) - await asyncProcess.start() + describe('On error', () => { + it('should call the onStart / onError fn(s)', async () => { + const fetchData = (): Promise => { + return new Promise((_, reject) => { + setTimeout(() => { + reject(new Error('Something bad happened')) + }, 50) + }) + } + + const onStartFns = [jest.fn(), jest.fn()] + const onSuccessFns = [jest.fn()] + const onErrorFn = jest.fn() + + const asyncProcess = getAsyncProcessTestInstance('loadFoo') + .do(fetchData) + .onStart(onStartFns) + .onSuccess(onSuccessFns) + .onError(onErrorFn) - expect(successValues).toEqual([2, 1]) - }) + await asyncProcess.start() - it('should return a success promise', async () => { - const fetchData = (): Promise => { - return new Promise(resolve => { - setTimeout(() => { - resolve(null) - }, 50) + onStartFns.forEach(onStartFn => { + expect(onStartFn).toHaveBeenCalled() }) - } - const asyncProcess = getAsyncProcessTestInstance('loadFoo').do(fetchData) + onSuccessFns.forEach(onSuccessFn => { + expect(onSuccessFn).not.toHaveBeenCalled() + }) - const doSomethingAfterAsyncProcess = jest.fn() + expect(data).toEqual(null) + }) - await asyncProcess.start().then(() => doSomethingAfterAsyncProcess()) + it('should pass the Error to error functions', async () => { + const error = new Error('Something bad happened') - expect(doSomethingAfterAsyncProcess).toHaveBeenCalled() - }) - }) + const fetchData = (): Promise => { + return new Promise((_, reject) => { + setTimeout(() => { + reject(error) + }, 50) + }) + } - describe('On error', () => { - it('should call the onStart / onError fn(s)', async () => { - const fetchData = (): Promise => { - return new Promise((_, reject) => { - setTimeout(() => { - reject(new Error('Something bad happened')) - }, 50) - }) - } + const onErrorFn = jest.fn() - const onStartFns = [jest.fn(), jest.fn()] - const onSuccessFns = [jest.fn()] - const onErrorFn = jest.fn() - - const asyncProcess = getAsyncProcessTestInstance('loadFoo') - .do(fetchData) - .onStart(onStartFns) - .onSuccess(onSuccessFns) - .onError(onErrorFn) + const asyncProcess = getAsyncProcessTestInstance('loadFoo') + .do(fetchData) + .onError(onErrorFn) - await asyncProcess.start() + await asyncProcess.start() - onStartFns.forEach(onStartFn => { - expect(onStartFn).toHaveBeenCalled() + expect(onErrorFn).toHaveBeenCalledWith(error) }) - onSuccessFns.forEach(onSuccessFn => { - expect(onSuccessFn).not.toHaveBeenCalled() + it('should expose the Error', async () => { + const fetchData = (): Promise => { + return new Promise((_, reject) => { + setTimeout(() => { + reject(new Error('Something bad happened')) + }, 50) + }) + } + + const asyncProcess = + getAsyncProcessTestInstance('loadFoo').do(fetchData) + + await asyncProcess.start() + + expect(data).toEqual(null) + expect(asyncProcess.error).toBeInstanceOf(Error) + expect(asyncProcess.error?.message).toEqual('Something bad happened') }) - expect(data).toEqual(null) - }) + it('should return a success promise even if an error occurred', async () => { + const fetchData = (): Promise => { + return new Promise((_, reject) => { + setTimeout(() => { + reject() + }, 50) + }) + } - it('should pass the Error to error functions', async () => { - const error = new Error('Something bad happened') + let errorMessage: Maybe = null - const fetchData = (): Promise => { - return new Promise((_, reject) => { - setTimeout(() => { - reject(error) - }, 50) - }) - } + const setErrorMessage = (message: string) => () => { + errorMessage = message + } - const onErrorFn = jest.fn() + const asyncProcess = getAsyncProcessTestInstance('loadFoo') + .do(fetchData) + .onError(setErrorMessage('Something bad happened')) - const asyncProcess = getAsyncProcessTestInstance('loadFoo') - .do(fetchData) - .onError(onErrorFn) + const doSomethingAfterAsyncProcess = jest.fn() - await asyncProcess.start() + await asyncProcess.start().then(() => doSomethingAfterAsyncProcess()) - expect(onErrorFn).toHaveBeenCalledWith(error) + expect(doSomethingAfterAsyncProcess).toHaveBeenCalled() + expect(errorMessage).toEqual('Something bad happened') + }) }) - it('should expose the Error', async () => { - const fetchData = (): Promise => { - return new Promise((_, reject) => { - setTimeout(() => { - reject(new Error('Something bad happened')) - }, 50) - }) - } - - const asyncProcess = getAsyncProcessTestInstance('loadFoo').do(fetchData) + describe('Identifiers', () => { + describe('onStart', () => { + it('should use a default identifier so that fns are replaced by default', () => { + const startSpy1 = jest.fn() + const startSpy2 = jest.fn() - await asyncProcess.start() + const foo1 = getAsyncProcessTestInstance('loadFoo') + foo1.onStart(startSpy1) - expect(data).toEqual(null) - expect(asyncProcess.error).toBeInstanceOf(Error) - expect(asyncProcess.error?.message).toEqual('Something bad happened') - }) + const foo2 = getAsyncProcessTestInstance('loadFoo') + foo2.onStart(startSpy1) + foo2.onStart(startSpy2) - it('should return a success promise even if an error occurred', async () => { - const fetchData = (): Promise => { - return new Promise((_, reject) => { - setTimeout(() => { - reject() - }, 50) + // register only `startSpy2` because added last + expect(foo1.fns.onStartFns.size).toBe(1) }) - } + }) - let errorMessage: Maybe = null + describe('onSuccess', () => { + it('should use a default identifier so that fns are replaced by default', () => { + const successSpy1 = jest.fn() + const successSpy2 = jest.fn() - const setErrorMessage = (message: string) => () => { - errorMessage = message - } + const foo1 = getAsyncProcessTestInstance('loadFoo') + foo1.onSuccess(successSpy1) - const asyncProcess = getAsyncProcessTestInstance('loadFoo') - .do(fetchData) - .onError(setErrorMessage('Something bad happened')) + const foo2 = getAsyncProcessTestInstance('loadFoo') + foo2.onSuccess(successSpy1) + foo2.onSuccess(successSpy2) - const doSomethingAfterAsyncProcess = jest.fn() + // register only `successSpy2` because added last + expect(foo1.fns.onSuccessFns.size).toBe(1) + }) - await asyncProcess.start().then(() => doSomethingAfterAsyncProcess()) + it('should allow using an identifier to add several fns', async () => { + const successSpy1 = jest.fn() + const successSpy2 = jest.fn() + const successSpy3 = jest.fn() + const successSpy4 = jest.fn() - expect(doSomethingAfterAsyncProcess).toHaveBeenCalled() - expect(errorMessage).toEqual('Something bad happened') - }) - }) + const foo1 = getAsyncProcessTestInstance('loadFoo') + foo1.onSuccess([successSpy1, successSpy2], 'success1') - describe('Identifiers', () => { - describe('onStart', () => { - it('should use a default identifier so that fns are replaced by default', () => { - const startSpy1 = jest.fn() - const startSpy2 = jest.fn() + const foo2 = getAsyncProcessTestInstance('loadFoo') + foo2.onSuccess([successSpy3, successSpy4], 'success2') - const foo1 = getAsyncProcessTestInstance('loadFoo') - foo1.onStart(startSpy1) + // register `[successSpy1, successSpy2]` and `[successSpy3, successSpy4]`, so 2 entries + expect(foo1.fns.onSuccessFns.size).toBe(2) - const foo2 = getAsyncProcessTestInstance('loadFoo') - foo2.onStart(startSpy1) - foo2.onStart(startSpy2) + await foo1.start() - // register only `startSpy2` because added last - expect(foo1.fns.onStartFns.size).toBe(1) + // check that the 4 fns have been called + expect(successSpy1).toHaveBeenCalled() + expect(successSpy2).toHaveBeenCalled() + expect(successSpy3).toHaveBeenCalled() + expect(successSpy4).toHaveBeenCalled() + }) }) - }) - describe('onSuccess', () => { - it('should use a default identifier so that fns are replaced by default', () => { - const successSpy1 = jest.fn() - const successSpy2 = jest.fn() + describe('onError', () => { + it('should use a default identifier so that fns are replaced by default', () => { + const errorSpy1 = jest.fn() + const errorSpy2 = jest.fn() - const foo1 = getAsyncProcessTestInstance('loadFoo') - foo1.onSuccess(successSpy1) + const foo1 = getAsyncProcessTestInstance('loadFoo') + foo1.onError(errorSpy1) - const foo2 = getAsyncProcessTestInstance('loadFoo') - foo2.onSuccess(successSpy1) - foo2.onSuccess(successSpy2) + const foo2 = getAsyncProcessTestInstance('loadFoo') + foo2.onError(errorSpy1) + foo2.onError(errorSpy2) - // register only `successSpy2` because added last - expect(foo1.fns.onSuccessFns.size).toBe(1) + // register only `errorSpy2` because added last + expect(foo1.fns.onErrorFns.size).toBe(1) + }) }) + }) - it('should allow using an identifier to add several fns', async () => { - const successSpy1 = jest.fn() - const successSpy2 = jest.fn() - const successSpy3 = jest.fn() - const successSpy4 = jest.fn() - - const foo1 = getAsyncProcessTestInstance('loadFoo') - foo1.onSuccess([successSpy1, successSpy2], 'success1') - - const foo2 = getAsyncProcessTestInstance('loadFoo') - foo2.onSuccess([successSpy3, successSpy4], 'success2') - - // register `[successSpy1, successSpy2]` and `[successSpy3, successSpy4]`, so 2 entries - expect(foo1.fns.onSuccessFns.size).toBe(2) + describe('Predicates', () => { + it('should execute the async process if the predicate function is true', async () => { + const fetchData = jest.fn() + const predicateFn1 = jest.fn().mockImplementation(() => true) + const predicateFn2 = jest.fn().mockImplementation(() => true) + const onStartFn = jest.fn() + const onSuccessFn = jest.fn() + const onErrorFn = jest.fn() + + const asyncProcess = getAsyncProcessTestInstance('loadFoo') + .do(fetchData) + .if(predicateFn1) + .if(predicateFn2) + .onStart(onStartFn) + .onSuccess(onSuccessFn) + .onError(onErrorFn) - await foo1.start() + await asyncProcess.start() - // check that the 4 fns have been called - expect(successSpy1).toHaveBeenCalled() - expect(successSpy2).toHaveBeenCalled() - expect(successSpy3).toHaveBeenCalled() - expect(successSpy4).toHaveBeenCalled() + expect(predicateFn1).toHaveBeenCalled() + expect(predicateFn2).toHaveBeenCalled() + expect(fetchData).toHaveBeenCalled() + expect(onStartFn).toHaveBeenCalled() + expect(onSuccessFn).toHaveBeenCalled() + expect(onErrorFn).not.toHaveBeenCalled() }) - }) - - describe('onError', () => { - it('should use a default identifier so that fns are replaced by default', () => { - const errorSpy1 = jest.fn() - const errorSpy2 = jest.fn() - const foo1 = getAsyncProcessTestInstance('loadFoo') - foo1.onError(errorSpy1) + it('should not execute the async process if the predicate function is false', async () => { + const fetchData = jest.fn() + const predicateFn1 = jest.fn().mockImplementation(() => false) + const onStartFn = jest.fn() + const onSuccessFn = jest.fn() + const onErrorFn = jest.fn() + + const asyncProcess = getAsyncProcessTestInstance('loadFoo') + .do(fetchData) + .if(predicateFn1) + .onStart(onStartFn) + .onSuccess(onSuccessFn) + .onError(onErrorFn) - const foo2 = getAsyncProcessTestInstance('loadFoo') - foo2.onError(errorSpy1) - foo2.onError(errorSpy2) + await asyncProcess.start() - // register only `errorSpy2` because added last - expect(foo1.fns.onErrorFns.size).toBe(1) + expect(predicateFn1).toHaveBeenCalled() + expect(fetchData).not.toHaveBeenCalled() + expect(onStartFn).not.toHaveBeenCalled() + expect(onSuccessFn).toHaveBeenCalled() + expect(onErrorFn).not.toHaveBeenCalled() }) - }) - }) - describe('Predicates', () => { - it('should execute the async process if the predicate function is true', async () => { - const fetchData = jest.fn() - const predicateFn1 = jest.fn().mockImplementation(() => true) - const predicateFn2 = jest.fn().mockImplementation(() => true) - const onStartFn = jest.fn() - const onSuccessFn = jest.fn() - const onErrorFn = jest.fn() - - const asyncProcess = getAsyncProcessTestInstance('loadFoo') - .do(fetchData) - .if(predicateFn1) - .if(predicateFn2) - .onStart(onStartFn) - .onSuccess(onSuccessFn) - .onError(onErrorFn) - - await asyncProcess.start() - - expect(predicateFn1).toHaveBeenCalled() - expect(predicateFn2).toHaveBeenCalled() - expect(fetchData).toHaveBeenCalled() - expect(onStartFn).toHaveBeenCalled() - expect(onSuccessFn).toHaveBeenCalled() - expect(onErrorFn).not.toHaveBeenCalled() - }) + it('should not execute the async process if one of the predicate functions is false', async () => { + const fetchData = jest.fn() + const predicateFn1 = jest.fn().mockImplementation(() => true) + const predicateFn2 = jest.fn().mockImplementation(() => true) + const predicateFn3 = jest.fn().mockImplementation(() => false) + const onStartFn = jest.fn() + const onSuccessFn = jest.fn() + const onErrorFn = jest.fn() + + const asyncProcess = getAsyncProcessTestInstance('loadFoo') + .do(fetchData) + .if(predicateFn1) + .if(predicateFn2) + .if(predicateFn3) + .onStart(onStartFn) + .onSuccess(onSuccessFn) + .onError(onErrorFn) - it('should not execute the async process if the predicate function is false', async () => { - const fetchData = jest.fn() - const predicateFn1 = jest.fn().mockImplementation(() => false) - const onStartFn = jest.fn() - const onSuccessFn = jest.fn() - const onErrorFn = jest.fn() - - const asyncProcess = getAsyncProcessTestInstance('loadFoo') - .do(fetchData) - .if(predicateFn1) - .onStart(onStartFn) - .onSuccess(onSuccessFn) - .onError(onErrorFn) - - await asyncProcess.start() - - expect(predicateFn1).toHaveBeenCalled() - expect(fetchData).not.toHaveBeenCalled() - expect(onStartFn).not.toHaveBeenCalled() - expect(onSuccessFn).toHaveBeenCalled() - expect(onErrorFn).not.toHaveBeenCalled() - }) + await asyncProcess.start() - it('should not execute the async process if one of the predicate functions is false', async () => { - const fetchData = jest.fn() - const predicateFn1 = jest.fn().mockImplementation(() => true) - const predicateFn2 = jest.fn().mockImplementation(() => true) - const predicateFn3 = jest.fn().mockImplementation(() => false) - const onStartFn = jest.fn() - const onSuccessFn = jest.fn() - const onErrorFn = jest.fn() - - const asyncProcess = getAsyncProcessTestInstance('loadFoo') - .do(fetchData) - .if(predicateFn1) - .if(predicateFn2) - .if(predicateFn3) - .onStart(onStartFn) - .onSuccess(onSuccessFn) - .onError(onErrorFn) - - await asyncProcess.start() - - expect(predicateFn1).toHaveBeenCalled() - expect(predicateFn2).toHaveBeenCalled() - expect(predicateFn3).toHaveBeenCalled() - expect(fetchData).not.toHaveBeenCalled() - expect(onStartFn).not.toHaveBeenCalled() - expect(onSuccessFn).toHaveBeenCalled() - expect(onErrorFn).not.toHaveBeenCalled() + expect(predicateFn1).toHaveBeenCalled() + expect(predicateFn2).toHaveBeenCalled() + expect(predicateFn3).toHaveBeenCalled() + expect(fetchData).not.toHaveBeenCalled() + expect(onStartFn).not.toHaveBeenCalled() + expect(onSuccessFn).toHaveBeenCalled() + expect(onErrorFn).not.toHaveBeenCalled() + }) }) - }) - - describe('AsyncProcess composition', () => { - it('should compose AsyncProcess instances', async () => { - const fetchData = (): Promise => { - return new Promise(resolve => { - setTimeout(() => { - setData({ id: 1, name: 'Bob' }) - resolve(null) - }, 50) - }) - } - - const onStartFn0 = jest.fn() - const onStartFn1 = jest.fn() - const onStartFn2 = jest.fn() - const onStartFn3 = jest.fn() - const onStartFn4 = jest.fn() - const onSuccessFn = jest.fn() + describe('AsyncProcess composition', () => { + it('should compose AsyncProcess instances', async () => { + const fetchData = (): Promise => { + return new Promise(resolve => { + setTimeout(() => { + setData({ id: 1, name: 'Bob' }) + resolve(null) + }, 50) + }) + } + + const onStartFn0 = jest.fn() + const onStartFn1 = jest.fn() + const onStartFn2 = jest.fn() + const onStartFn3 = jest.fn() + const onStartFn4 = jest.fn() + + const onSuccessFn = jest.fn() + + const onErrorFn = jest.fn() + + let asyncProcessBaseIdentifiers + + const asyncProcessExtended = ( + asyncProcess: AsyncProcess + ) => { + const baseAsyncProcess = getAsyncProcessTestInstance( + asyncProcess.identifier, + ['id1', 'id2'] + ) + // define custom identifiers for each fns addition + .onStart(onStartFn1, 'onStartFn1') + .onStart(onStartFn2, 'onStartFn2') + .onStart([onStartFn3, onStartFn4], 'onStartFn3+onStartFn4') + .onSuccess(onSuccessFn) + .onError(onErrorFn) + + asyncProcessBaseIdentifiers = baseAsyncProcess.identitiers + + return baseAsyncProcess + } + + const asyncProcess = getAsyncProcessTestInstance('loadFoo') + .do(fetchData) + .onStart(onStartFn0, 'onStartFn0') + .compose(asyncProcessExtended) + + await asyncProcess.start() + + expect(onStartFn0).toHaveBeenCalled() + expect(onStartFn1).toHaveBeenCalled() + expect(onStartFn2).toHaveBeenCalled() + expect(onStartFn3).toHaveBeenCalled() + expect(onStartFn4).toHaveBeenCalled() - const onErrorFn = jest.fn() + expect(onSuccessFn).toHaveBeenCalled() - let asyncProcessBaseIdentifiers + expect(onErrorFn).not.toHaveBeenCalled() - const asyncProcessExtended = ( - asyncProcess: AsyncProcess - ) => { - const baseAsyncProcess = getAsyncProcessTestInstance( - asyncProcess.identifier, - ['id1', 'id2'] + // Count the number of additions (by uniq identifiers) of onStartFns entries + expect(asyncProcess.fns.onStartFns.size).toEqual( + deleteFunctionsWhenStarted ? 0 : 4 ) - // define custom identifiers for each fns addition - .onStart(onStartFn1, 'onStartFn1') - .onStart(onStartFn2, 'onStartFn2') - .onStart([onStartFn3, onStartFn4], 'onStartFn3+onStartFn4') - .onSuccess(onSuccessFn) - .onError(onErrorFn) - - asyncProcessBaseIdentifiers = baseAsyncProcess.identitiers - return baseAsyncProcess - } + expect(data).toEqual({ + id: 1, + name: 'Bob' + }) - const asyncProcess = getAsyncProcessTestInstance('loadFoo') - .do(fetchData) - .onStart(onStartFn0, 'onStartFn0') - .compose(asyncProcessExtended) + expect(asyncProcessBaseIdentifiers).toEqual(['loadFoo', 'id1/id2']) + }) + }) - await asyncProcess.start() + describe('Singleton', () => { + it('should return an unique instance for a same identifier', () => { + const successSpy = jest.fn() - expect(onStartFn0).toHaveBeenCalled() - expect(onStartFn1).toHaveBeenCalled() - expect(onStartFn2).toHaveBeenCalled() - expect(onStartFn3).toHaveBeenCalled() - expect(onStartFn4).toHaveBeenCalled() + const foo1 = getAsyncProcessTestInstance('loadFoo') + foo1.onSuccess(successSpy) + const foo2 = getAsyncProcessTestInstance('loadFoo') - expect(onSuccessFn).toHaveBeenCalled() + const bar1 = getAsyncProcessTestInstance('loadBar') + const bar2 = getAsyncProcessTestInstance('loadBar') - expect(onErrorFn).not.toHaveBeenCalled() + expect(foo1).toBe(foo2) + expect(bar1).toBe(bar2) - // Count the number of additions (by uniq identifiers) of onStartFns entries - expect(asyncProcess.fns.onStartFns.size).toEqual(4) + expect(foo1).not.toBe(bar1) + expect(foo2).not.toBe(bar2) - expect(data).toEqual({ - id: 1, - name: 'Bob' + expect(foo1.fns.onSuccessFns.size).toBe(1) + expect(foo2.fns.onSuccessFns.size).toBe(1) }) - expect(asyncProcessBaseIdentifiers).toEqual(['loadFoo', 'id1/id2']) - }) - }) + it('should return an unique instance for same identifiers', () => { + const successSpy = jest.fn() + + const foo1 = getAsyncProcessTestInstance('loadFoo') + foo1.onSuccess(successSpy) - describe('Singleton', () => { - it('should return an unique instance for a same identifier', () => { - const successSpy = jest.fn() + const foo1Dep1 = getAsyncProcessTestInstance('loadFoo', ['dep1']) + const foo2Dep1 = getAsyncProcessTestInstance('loadFoo', ['dep1']) - const foo1 = getAsyncProcessTestInstance('loadFoo') - foo1.onSuccess(successSpy) - const foo2 = getAsyncProcessTestInstance('loadFoo') + const foo1Dep2 = getAsyncProcessTestInstance('loadFoo', ['dep2']) + const foo2Dep2 = getAsyncProcessTestInstance('loadFoo', ['dep2']) - const bar1 = getAsyncProcessTestInstance('loadBar') - const bar2 = getAsyncProcessTestInstance('loadBar') + expect(foo1Dep1).toBe(foo2Dep1) + expect(foo1Dep2).toBe(foo2Dep2) - expect(foo1).toBe(foo2) - expect(bar1).toBe(bar2) + expect(foo1).not.toBe(foo1Dep1) + expect(foo1Dep1).not.toBe(foo1Dep2) - expect(foo1).not.toBe(bar1) - expect(foo2).not.toBe(bar2) + expect(foo1Dep1.identifier).toBe('loadFoo') + expect(foo1Dep1.identitiers).toEqual(['loadFoo', 'dep1']) - expect(foo1.fns.onSuccessFns.size).toBe(1) - expect(foo2.fns.onSuccessFns.size).toBe(1) + expect(foo1.fns.onSuccessFns.size).toBe(1) + expect(foo1Dep1.fns.onSuccessFns.size).toBe(0) + }) }) - it('should return an unique instance for same identifiers', () => { - const successSpy = jest.fn() + describe('Options', () => { + describe('deleteFunctionsWhenStarted', () => { + it('should reset functions when async process is started', async () => { + const successSpy1 = jest.fn() + const successSpy2 = jest.fn() + const successSpy3 = jest.fn() - const foo1 = getAsyncProcessTestInstance('loadFoo') - foo1.onSuccess(successSpy) + const foo1 = getAsyncProcessTestInstance('loadFoo') - const foo1Dep1 = getAsyncProcessTestInstance('loadFoo', ['dep1']) - const foo2Dep1 = getAsyncProcessTestInstance('loadFoo', ['dep1']) + foo1.onSuccess(successSpy1, 'success1') + foo1.onSuccess(successSpy2, 'success2') - const foo1Dep2 = getAsyncProcessTestInstance('loadFoo', ['dep2']) - const foo2Dep2 = getAsyncProcessTestInstance('loadFoo', ['dep2']) + expect(foo1.fns.onSuccessFns.size).toBe(2) - expect(foo1Dep1).toBe(foo2Dep1) - expect(foo1Dep2).toBe(foo2Dep2) + await foo1.start() - expect(foo1).not.toBe(foo1Dep1) - expect(foo1Dep1).not.toBe(foo1Dep2) + foo1.onSuccess(successSpy3, 'success3') - expect(foo1Dep1.identifier).toBe('loadFoo') - expect(foo1Dep1.identitiers).toEqual(['loadFoo', 'dep1']) - - expect(foo1.fns.onSuccessFns.size).toBe(1) - expect(foo1Dep1.fns.onSuccessFns.size).toBe(0) + expect(foo1.fns.onSuccessFns.size).toBe( + deleteFunctionsWhenStarted ? 1 : 3 + ) + }) + }) }) }) }) diff --git a/src/types/index.ts b/src/types/index.ts index c0819e4..3b6b48e 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -26,3 +26,7 @@ export type AsyncProcessIdentifiers = [ TIdentifier, Maybe ] + +export interface IAsyncProcessOptions { + deleteFunctionsWhenStarted: boolean +} From 24b159cc23bff317ec2ca40775ca048e1319a48b Mon Sep 17 00:00:00 2001 From: Alexis Mineaud Date: Fri, 20 Jan 2023 09:12:46 +0100 Subject: [PATCH 03/20] Use the term job to name the async functions to execute. --- README.md | 2 +- src/AsyncProcess/index.ts | 48 +++++++++++++++--------------- src/__tests__/AsyncProcess.test.ts | 8 ++--- src/types/index.ts | 5 ++-- 4 files changed, 32 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 748f3e1..3e870ee 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ AsyncProcess.instance('initUsersPage', ['optionalIdentifier']) .onSuccess(hideSpinner) // call the `showError` function when the process is done and if it fails .onError(showError) - // start the async process + // start jobs .start() ``` diff --git a/src/AsyncProcess/index.ts b/src/AsyncProcess/index.ts index d9b4a77..e6bab24 100644 --- a/src/AsyncProcess/index.ts +++ b/src/AsyncProcess/index.ts @@ -3,7 +3,7 @@ import { AsyncErrorFn, AsyncErrorFns, AsyncFn, - AsyncFns, + Jobs, AsyncProcessIdentifiers, IAsyncProcessFns, IAsyncProcessOptions, @@ -31,7 +31,7 @@ export class AsyncProcess { private _error: Maybe = null private _fns: IAsyncProcessFns = { - asyncFns: new Map(), + jobs: new Map(), onStartFns: new Map(), onSuccessFns: new Map(), onErrorFns: new Map() @@ -75,10 +75,10 @@ export class AsyncProcess { } /** - * Save the async process function(s). + * Save jobs function(s). */ - do(asyncFns: AsyncFns, identifier = 'do'): this { - this._fns.asyncFns.set(identifier, new Set(ensureArray(asyncFns))) + do(jobs: Jobs, identifier = 'do'): this { + this._fns.jobs.set(identifier, new Set(ensureArray(jobs))) return this } @@ -91,26 +91,26 @@ export class AsyncProcess { } /** - * Save functions to execute before starting the async process. + * Save functions to execute before starting jobs. */ - onStart(asyncFns: AsyncFns, identifier = 'onStart'): this { - this._fns.onStartFns.set(identifier, new Set(ensureArray(asyncFns))) + onStart(jobs: Jobs, identifier = 'onStart'): this { + this._fns.onStartFns.set(identifier, new Set(ensureArray(jobs))) return this } /** - * Save functions to execute after the async process if succeeded. + * Save functions to execute after jobs are succesful. */ - onSuccess(asyncFns: AsyncFns, identifier = 'onSuccess'): this { - this._fns.onSuccessFns.set(identifier, new Set(ensureArray(asyncFns))) + onSuccess(jobs: Jobs, identifier = 'onSuccess'): this { + this._fns.onSuccessFns.set(identifier, new Set(ensureArray(jobs))) return this } /** - * Save functions to execute after the async process if failed. + * Save functions to execute after jobs are succesful. */ - onError(asyncFns: AsyncErrorFns, identifier = 'onError'): this { - this._fns.onErrorFns.set(identifier, new Set(ensureArray(asyncFns))) + onError(jobs: AsyncErrorFns, identifier = 'onError'): this { + this._fns.onErrorFns.set(identifier, new Set(ensureArray(jobs))) return this } @@ -124,8 +124,8 @@ export class AsyncProcess { ): this { const asyncProcessFns = asyncProcessComposer(this).fns - asyncProcessFns.asyncFns.forEach((fns, identifier) => { - this._fns.asyncFns.set(identifier, fns) + asyncProcessFns.jobs.forEach((fns, identifier) => { + this._fns.jobs.set(identifier, fns) }) asyncProcessFns.onStartFns.forEach((fns, identifier) => { @@ -158,23 +158,23 @@ export class AsyncProcess { } /** - * Start the async process and executes registered functions. + * Start jobs and executes registered functions. */ async start(): Promise { this._error = null try { if (this._predicateFns && !(await this.shouldStart())) { - await this._execAsyncFns(this._fns.onSuccessFns) + await this._execJobs(this._fns.onSuccessFns) this.shouldResetFunctions() return this } - await this._execAsyncFns(this._fns.onStartFns) - await this._execAsyncFns(this._fns.asyncFns) - await this._execAsyncFns(this._fns.onSuccessFns) + await this._execJobs(this._fns.onStartFns) + await this._execJobs(this._fns.jobs) + await this._execJobs(this._fns.onSuccessFns) this.shouldResetFunctions() } catch (err) { @@ -206,7 +206,7 @@ export class AsyncProcess { } this._fns = { - asyncFns: new Map(), + jobs: new Map(), onStartFns: new Map(), onSuccessFns: new Map(), onErrorFns: new Map() @@ -234,8 +234,8 @@ export class AsyncProcess { /** * Execute sequentially async functions. */ - private _execAsyncFns(asyncFns: Map>): Promise { - return Array.from(asyncFns.values()) + private _execJobs(jobs: Map>): Promise { + return Array.from(jobs.values()) .reduce( (acc, fns_) => acc.concat(Array.from(fns_.values())), [] diff --git a/src/__tests__/AsyncProcess.test.ts b/src/__tests__/AsyncProcess.test.ts index 8d64281..363be39 100644 --- a/src/__tests__/AsyncProcess.test.ts +++ b/src/__tests__/AsyncProcess.test.ts @@ -13,7 +13,7 @@ describe('AsyncProcess', () => { describe.each([ { deleteFunctionsWhenStarted: false }, { deleteFunctionsWhenStarted: true } - ])('According to options: %o', ({ deleteFunctionsWhenStarted }) => { + ])('With options: %o', ({ deleteFunctionsWhenStarted }) => { let data: Maybe = null const setData = (data_: any) => { @@ -303,7 +303,7 @@ describe('AsyncProcess', () => { }) describe('Predicates', () => { - it('should execute the async process if the predicate function is true', async () => { + it('should execute jobs if the predicate function is true', async () => { const fetchData = jest.fn() const predicateFn1 = jest.fn().mockImplementation(() => true) const predicateFn2 = jest.fn().mockImplementation(() => true) @@ -329,7 +329,7 @@ describe('AsyncProcess', () => { expect(onErrorFn).not.toHaveBeenCalled() }) - it('should not execute the async process if the predicate function is false', async () => { + it('should not execute jobs if the predicate function is false', async () => { const fetchData = jest.fn() const predicateFn1 = jest.fn().mockImplementation(() => false) const onStartFn = jest.fn() @@ -352,7 +352,7 @@ describe('AsyncProcess', () => { expect(onErrorFn).not.toHaveBeenCalled() }) - it('should not execute the async process if one of the predicate functions is false', async () => { + it('should not execute jobs if one of the predicate functions is false', async () => { const fetchData = jest.fn() const predicateFn1 = jest.fn().mockImplementation(() => true) const predicateFn2 = jest.fn().mockImplementation(() => true) diff --git a/src/types/index.ts b/src/types/index.ts index 3b6b48e..5350631 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -4,7 +4,7 @@ import { AsyncProcess } from '../AsyncProcess' export type Fn = () => any export type AsyncFn = () => Promise export type FnOrAsyncFn = Fn | AsyncFn -export type AsyncFns = FnOrAsyncFn | Array +export type Jobs = FnOrAsyncFn | Array export type ErrorFn = (err: Error) => any export type AsyncErrorFn = (err: Error) => Promise @@ -16,7 +16,7 @@ export type PredicateFn = ( ) => boolean | Promise export interface IAsyncProcessFns { - asyncFns: Map> + jobs: Map> onStartFns: Map> onSuccessFns: Map> onErrorFns: Map> @@ -28,5 +28,6 @@ export type AsyncProcessIdentifiers = [ ] export interface IAsyncProcessOptions { + // delete registered functions when async process is started deleteFunctionsWhenStarted: boolean } From 8c21eaa3793134e8a72323935ad7256fa37540b8 Mon Sep 17 00:00:00 2001 From: Alexis Mineaud Date: Fri, 20 Jan 2023 09:28:10 +0100 Subject: [PATCH 04/20] Rename deleteFunctionsWhenStarted to deleteFunctionsWhenJobsStarted. --- src/AsyncProcess/index.ts | 4 ++-- src/__tests__/AsyncProcess.test.ts | 17 ++++++++++------- src/types/index.ts | 2 +- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/AsyncProcess/index.ts b/src/AsyncProcess/index.ts index e6bab24..377416c 100644 --- a/src/AsyncProcess/index.ts +++ b/src/AsyncProcess/index.ts @@ -25,7 +25,7 @@ export class AsyncProcess { * When set to true, registered functions are deleted after AsyncProcess has been started. * Useful when reusing a same instance of AsyncProcess to not have functions registered several times. */ - deleteFunctionsWhenStarted: false + deleteFunctionsWhenJobsStarted: false } private _error: Maybe = null @@ -201,7 +201,7 @@ export class AsyncProcess { * Reset functions. */ shouldResetFunctions(): this { - if (!this._options.deleteFunctionsWhenStarted) { + if (!this._options.deleteFunctionsWhenJobsStarted) { return this } diff --git a/src/__tests__/AsyncProcess.test.ts b/src/__tests__/AsyncProcess.test.ts index 363be39..1511f9d 100644 --- a/src/__tests__/AsyncProcess.test.ts +++ b/src/__tests__/AsyncProcess.test.ts @@ -11,9 +11,9 @@ type AsyncProcessTestIdentifier = 'loadFoo' | 'loadBar' describe('AsyncProcess', () => { describe.each([ - { deleteFunctionsWhenStarted: false }, - { deleteFunctionsWhenStarted: true } - ])('With options: %o', ({ deleteFunctionsWhenStarted }) => { + { deleteFunctionsWhenJobsStarted: false }, + { deleteFunctionsWhenJobsStarted: true } + ])('With options: %o', ({ deleteFunctionsWhenJobsStarted }) => { let data: Maybe = null const setData = (data_: any) => { @@ -25,7 +25,7 @@ describe('AsyncProcess', () => { subIdentifiers?: string[] ): AsyncProcess { return AsyncProcess.instance(identifier, subIdentifiers).setOptions({ - deleteFunctionsWhenStarted + deleteFunctionsWhenJobsStarted }) } @@ -240,6 +240,9 @@ describe('AsyncProcess', () => { // register only `startSpy2` because added last expect(foo1.fns.onStartFns.size).toBe(1) + expect(Array.from(foo1.fns.onStartFns.values())).toEqual([ + new Set([startSpy2]) + ]) }) }) @@ -443,7 +446,7 @@ describe('AsyncProcess', () => { // Count the number of additions (by uniq identifiers) of onStartFns entries expect(asyncProcess.fns.onStartFns.size).toEqual( - deleteFunctionsWhenStarted ? 0 : 4 + deleteFunctionsWhenJobsStarted ? 0 : 4 ) expect(data).toEqual({ @@ -503,7 +506,7 @@ describe('AsyncProcess', () => { }) describe('Options', () => { - describe('deleteFunctionsWhenStarted', () => { + describe('deleteFunctionsWhenJobsStarted', () => { it('should reset functions when async process is started', async () => { const successSpy1 = jest.fn() const successSpy2 = jest.fn() @@ -521,7 +524,7 @@ describe('AsyncProcess', () => { foo1.onSuccess(successSpy3, 'success3') expect(foo1.fns.onSuccessFns.size).toBe( - deleteFunctionsWhenStarted ? 1 : 3 + deleteFunctionsWhenJobsStarted ? 1 : 3 ) }) }) diff --git a/src/types/index.ts b/src/types/index.ts index 5350631..c1f7204 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -29,5 +29,5 @@ export type AsyncProcessIdentifiers = [ export interface IAsyncProcessOptions { // delete registered functions when async process is started - deleteFunctionsWhenStarted: boolean + deleteFunctionsWhenJobsStarted: boolean } From 0915de3a9a96f62f272f29f431a81b8ace662913 Mon Sep 17 00:00:00 2001 From: Alexis Mineaud Date: Fri, 20 Jan 2023 11:41:01 +0100 Subject: [PATCH 05/20] Add debug options and logging at different steps. --- package.json | 2 +- src/AsyncProcess/index.ts | 121 +++++++++++++----- src/__tests__/AsyncProcess.test.ts | 95 ++++++++++++-- .../__snapshots__/AsyncProcess.test.ts.snap | 37 ++++++ src/logger.ts | 11 +- src/types/index.ts | 4 + 6 files changed, 222 insertions(+), 48 deletions(-) create mode 100644 src/__tests__/__snapshots__/AsyncProcess.test.ts.snap diff --git a/package.json b/package.json index 1ddc33c..b9cea51 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "check:code": "tsc --noEmit", "lint": "eslint .", "prepublishOnly": "npm run check:code && npm run lint && npm t && npm run build", - "test": "jest" + "test": "DEBUG_COLORS=0 DEBUG_HIDE_DATE=true jest" }, "devDependencies": { "@types/jest": "^29.2.5", diff --git a/src/AsyncProcess/index.ts b/src/AsyncProcess/index.ts index 377416c..39163b7 100644 --- a/src/AsyncProcess/index.ts +++ b/src/AsyncProcess/index.ts @@ -1,12 +1,14 @@ import { ensureArray, Maybe, MetaData } from '@productive-codebases/toolbox' +import { LoggerLevel } from '@productive-codebases/toolbox/dist/types/libs/logger/types' +import { logger, LoggerNamespace } from '../logger' import { AsyncErrorFn, AsyncErrorFns, AsyncFn, - Jobs, AsyncProcessIdentifiers, IAsyncProcessFns, IAsyncProcessOptions, + Jobs, PredicateFn } from '../types' @@ -25,7 +27,11 @@ export class AsyncProcess { * When set to true, registered functions are deleted after AsyncProcess has been started. * Useful when reusing a same instance of AsyncProcess to not have functions registered several times. */ - deleteFunctionsWhenJobsStarted: false + deleteFunctionsWhenJobsStarted: false, + debug: { + logFunctionRegistrations: false, + logFunctionExecutions: false + } } private _error: Maybe = null @@ -69,15 +75,19 @@ export class AsyncProcess { /** * Set AsyncProcess options. */ - setOptions(options: IAsyncProcessOptions): this { - this._options = options + setOptions(options: Partial): this { + this._options = { ...this._options, ...options } return this } /** * Save jobs function(s). */ - do(jobs: Jobs, identifier = 'do'): this { + do(jobs: Jobs, identifier = 'defaultJobs'): this { + this._log('functionsRegistrations')('debug')( + `Register jobs function(s) with the identifier "${identifier}"` + ) + this._fns.jobs.set(identifier, new Set(ensureArray(jobs))) return this } @@ -93,7 +103,11 @@ export class AsyncProcess { /** * Save functions to execute before starting jobs. */ - onStart(jobs: Jobs, identifier = 'onStart'): this { + onStart(jobs: Jobs, identifier = 'defaultOnStart'): this { + this._log('functionsRegistrations')('debug')( + `Register onStart function(s) with the identifier "${identifier}"` + ) + this._fns.onStartFns.set(identifier, new Set(ensureArray(jobs))) return this } @@ -101,7 +115,11 @@ export class AsyncProcess { /** * Save functions to execute after jobs are succesful. */ - onSuccess(jobs: Jobs, identifier = 'onSuccess'): this { + onSuccess(jobs: Jobs, identifier = 'defaultOnSuccess'): this { + this._log('functionsRegistrations')('debug')( + `Register onSuccess function(s) with the identifier "${identifier}"` + ) + this._fns.onSuccessFns.set(identifier, new Set(ensureArray(jobs))) return this } @@ -109,7 +127,11 @@ export class AsyncProcess { /** * Save functions to execute after jobs are succesful. */ - onError(jobs: AsyncErrorFns, identifier = 'onError'): this { + onError(jobs: AsyncErrorFns, identifier = 'defaultOnError'): this { + this._log('functionsRegistrations')('debug')( + `Register onError function(s) with the identifier "${identifier}"` + ) + this._fns.onErrorFns.set(identifier, new Set(ensureArray(jobs))) return this } @@ -157,6 +179,13 @@ export class AsyncProcess { return this._identifiers } + /** + * Return all identifiers as a string. + */ + get identitiersAsString(): string { + return this._identifiers.filter(Boolean).join('/') + } + /** * Start jobs and executes registered functions. */ @@ -167,7 +196,7 @@ export class AsyncProcess { if (this._predicateFns && !(await this.shouldStart())) { await this._execJobs(this._fns.onSuccessFns) - this.shouldResetFunctions() + this.shouldDeleteFunctions() return this } @@ -176,7 +205,7 @@ export class AsyncProcess { await this._execJobs(this._fns.jobs) await this._execJobs(this._fns.onSuccessFns) - this.shouldResetFunctions() + this.shouldDeleteFunctions() } catch (err) { this._error = err instanceof Error ? err : new Error('Unknown error') this._execAsyncErrorFns(this._error, this._fns.onErrorFns) @@ -191,6 +220,10 @@ export class AsyncProcess { async shouldStart(): Promise { for (const predicateFn of this._predicateFns) { if (!(await predicateFn(this))) { + this._log('functionsExecutions')('debug')( + `Skip jobs execution because predicate is falsy` + ) + return false } } @@ -198,13 +231,17 @@ export class AsyncProcess { } /** - * Reset functions. + * Delete functions. */ - shouldResetFunctions(): this { + shouldDeleteFunctions(): this { if (!this._options.deleteFunctionsWhenJobsStarted) { return this } + this._log('functionsRegistrations')('debug')( + `Delete functions after starting jobs` + ) + this._fns = { jobs: new Map(), onStartFns: new Map(), @@ -234,36 +271,47 @@ export class AsyncProcess { /** * Execute sequentially async functions. */ - private _execJobs(jobs: Map>): Promise { - return Array.from(jobs.values()) - .reduce( - (acc, fns_) => acc.concat(Array.from(fns_.values())), - [] - ) - .reduce( - (promise, nextFn) => promise.then(() => nextFn()), - Promise.resolve(null) - ) - .then(() => this) + private async _execJobs(jobs: Map>): Promise { + for (const [identifier, fns] of jobs.entries()) { + for (const fn of fns) { + this._log('functionsExecutions')('debug')( + `Execute jobs functions with the identifier "${identifier}"` + ) + + await fn() + } + } + + return this } /** * Execute sequentially async error functions. */ - private _execAsyncErrorFns( + private async _execAsyncErrorFns( err: Error, asyncErrorFns: Map> ): Promise { - return Array.from(asyncErrorFns.values()) - .reduce( - (acc, fns_) => acc.concat(Array.from(fns_.values())), - [] - ) - .reduce( - (promise, nextFn) => promise.then(() => nextFn(err)), - Promise.resolve(null) - ) - .then(() => this) + for (const [identifier, fns] of asyncErrorFns.entries()) { + for (const fn of fns) { + this._log('functionsExecutions')('debug')( + `Execute onError functions with the identifier "${identifier}"` + ) + + await fn(err) + } + } + + return this + } + + /** + * Log with AsyncProcess identifier prefix. + */ + private _log(namespace: LoggerNamespace) { + return (level: LoggerLevel) => (message: string) => { + logger(namespace)(level)(`[${this.identitiersAsString}] ${message}`) + } } /** @@ -305,6 +353,9 @@ export class AsyncProcess { identifier: TIdentifier, subIdentifiers?: Maybe ): AsyncProcessIdentifiers { - return [identifier, subIdentifiers ? subIdentifiers.join('/') : null] + return [ + identifier, + subIdentifiers ? subIdentifiers.filter(Boolean).join('/') : null + ] } } diff --git a/src/__tests__/AsyncProcess.test.ts b/src/__tests__/AsyncProcess.test.ts index 1511f9d..7c4abe2 100644 --- a/src/__tests__/AsyncProcess.test.ts +++ b/src/__tests__/AsyncProcess.test.ts @@ -1,5 +1,6 @@ import { Maybe } from '@productive-codebases/toolbox' import { AsyncProcess } from '../AsyncProcess' +import { debug, logger } from '../logger' import { IAsyncProcessOptions } from '../types' interface IData { @@ -10,10 +11,16 @@ interface IData { type AsyncProcessTestIdentifier = 'loadFoo' | 'loadBar' describe('AsyncProcess', () => { - describe.each([ + describe.each>([ { deleteFunctionsWhenJobsStarted: false }, - { deleteFunctionsWhenJobsStarted: true } - ])('With options: %o', ({ deleteFunctionsWhenJobsStarted }) => { + { deleteFunctionsWhenJobsStarted: true }, + { + debug: { + logFunctionRegistrations: true, + logFunctionExecutions: true + } + } + ])('With options: %o', options => { let data: Maybe = null const setData = (data_: any) => { @@ -24,9 +31,9 @@ describe('AsyncProcess', () => { identifier: AsyncProcessTestIdentifier, subIdentifiers?: string[] ): AsyncProcess { - return AsyncProcess.instance(identifier, subIdentifiers).setOptions({ - deleteFunctionsWhenJobsStarted - }) + return AsyncProcess.instance(identifier, subIdentifiers).setOptions( + options + ) } beforeEach(() => { @@ -446,7 +453,7 @@ describe('AsyncProcess', () => { // Count the number of additions (by uniq identifiers) of onStartFns entries expect(asyncProcess.fns.onStartFns.size).toEqual( - deleteFunctionsWhenJobsStarted ? 0 : 4 + options.deleteFunctionsWhenJobsStarted ? 0 : 4 ) expect(data).toEqual({ @@ -524,10 +531,82 @@ describe('AsyncProcess', () => { foo1.onSuccess(successSpy3, 'success3') expect(foo1.fns.onSuccessFns.size).toBe( - deleteFunctionsWhenJobsStarted ? 1 : 3 + options.deleteFunctionsWhenJobsStarted ? 1 : 3 ) }) }) + + describe('Logging', () => { + // Mock the log function + let logs: any[] = [] + + beforeEach(() => { + debug.enable( + options.debug?.logFunctionExecutions ? 'AsyncProcess:*' : false + ) + + debug.formatters.s = (s: string) => { + return s + } + + debug.log = (...args: any[]) => { + logs.push(...args) + } + }) + + afterEach(() => { + // reset logs for each test + logs = [] + }) + + it('should log different events if success', async () => { + const fetchData = jest.fn() + const predicateFn1 = jest.fn().mockImplementation(() => true) + const onStartFn = jest.fn() + const onSuccessFn = jest.fn() + const onErrorFn = jest.fn() + + await getAsyncProcessTestInstance('loadFoo') + .setOptions({ + debug: { + logFunctionRegistrations: true, + logFunctionExecutions: true + } + }) + .do(fetchData) + .if(predicateFn1) + .onStart(onStartFn) + .onSuccess(onSuccessFn) + .onError(onErrorFn) + .start() + + expect(logs).toMatchSnapshot() + }) + + it('should log different events if error', async () => { + const fetchData = jest.fn().mockImplementation(() => Promise.reject()) + const predicateFn1 = jest.fn().mockImplementation(() => true) + const onStartFn = jest.fn() + const onSuccessFn = jest.fn() + const onErrorFn = jest.fn() + + await getAsyncProcessTestInstance('loadFoo') + .setOptions({ + debug: { + logFunctionRegistrations: true, + logFunctionExecutions: true + } + }) + .do(fetchData) + .if(predicateFn1) + .onStart(onStartFn) + .onSuccess(onSuccessFn) + .onError(onErrorFn) + .start() + + expect(logs).toMatchSnapshot() + }) + }) }) }) }) diff --git a/src/__tests__/__snapshots__/AsyncProcess.test.ts.snap b/src/__tests__/__snapshots__/AsyncProcess.test.ts.snap new file mode 100644 index 0000000..8621474 --- /dev/null +++ b/src/__tests__/__snapshots__/AsyncProcess.test.ts.snap @@ -0,0 +1,37 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AsyncProcess With options: { + debug: { logFunctionRegistrations: true, logFunctionExecutions: true } +} Options Logging should log different events if error 1`] = ` +[ + "AsyncProcess:functionsRegistrations:debug [loadFoo] Register jobs function(s) with the identifier "defaultJobs"", + "AsyncProcess:functionsRegistrations:debug [loadFoo] Register onStart function(s) with the identifier "defaultOnStart"", + "AsyncProcess:functionsRegistrations:debug [loadFoo] Register onSuccess function(s) with the identifier "defaultOnSuccess"", + "AsyncProcess:functionsRegistrations:debug [loadFoo] Register onError function(s) with the identifier "defaultOnError"", + "AsyncProcess:functionsExecutions:debug [loadFoo] Execute jobs functions with the identifier "defaultOnStart"", + "AsyncProcess:functionsExecutions:debug [loadFoo] Execute jobs functions with the identifier "defaultJobs"", + "AsyncProcess:functionsExecutions:debug [loadFoo] Execute onError functions with the identifier "defaultOnError"", +] +`; + +exports[`AsyncProcess With options: { + debug: { logFunctionRegistrations: true, logFunctionExecutions: true } +} Options Logging should log different events if success 1`] = ` +[ + "AsyncProcess:functionsRegistrations:debug [loadFoo] Register jobs function(s) with the identifier "defaultJobs"", + "AsyncProcess:functionsRegistrations:debug [loadFoo] Register onStart function(s) with the identifier "defaultOnStart"", + "AsyncProcess:functionsRegistrations:debug [loadFoo] Register onSuccess function(s) with the identifier "defaultOnSuccess"", + "AsyncProcess:functionsRegistrations:debug [loadFoo] Register onError function(s) with the identifier "defaultOnError"", + "AsyncProcess:functionsExecutions:debug [loadFoo] Execute jobs functions with the identifier "defaultOnStart"", + "AsyncProcess:functionsExecutions:debug [loadFoo] Execute jobs functions with the identifier "defaultJobs"", + "AsyncProcess:functionsExecutions:debug [loadFoo] Execute jobs functions with the identifier "defaultOnSuccess"", +] +`; + +exports[`AsyncProcess With options: { deleteFunctionsWhenJobsStarted: false } Options Logging should log different events if error 1`] = `[]`; + +exports[`AsyncProcess With options: { deleteFunctionsWhenJobsStarted: false } Options Logging should log different events if success 1`] = `[]`; + +exports[`AsyncProcess With options: { deleteFunctionsWhenJobsStarted: true } Options Logging should log different events if error 1`] = `[]`; + +exports[`AsyncProcess With options: { deleteFunctionsWhenJobsStarted: true } Options Logging should log different events if success 1`] = `[]`; diff --git a/src/logger.ts b/src/logger.ts index d2546ef..f3eb7bb 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -1,14 +1,17 @@ import { setupLogger } from '@productive-codebases/toolbox' const loggerMapping = { - asyncprocess: { - runtime: 'runtime' + AsyncProcess: { + functionsRegistrations: 'functionsRegistrations', + functionsExecutions: 'functionsExecutions' } } const { newLogger, debug } = setupLogger(loggerMapping) -export const logger = newLogger('asyncprocess')('runtime') -export { debug } +const logger = newLogger('AsyncProcess') +export { logger, debug } + +export type LoggerNamespace = Parameters[0] export type Logger = typeof logger diff --git a/src/types/index.ts b/src/types/index.ts index c1f7204..57cf4c2 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -30,4 +30,8 @@ export type AsyncProcessIdentifiers = [ export interface IAsyncProcessOptions { // delete registered functions when async process is started deleteFunctionsWhenJobsStarted: boolean + debug: { + logFunctionRegistrations: boolean + logFunctionExecutions: boolean + } } From 18e242b1d41ff1b879db0aae94a6328481e42673 Mon Sep 17 00:00:00 2001 From: Alexis Mineaud Date: Fri, 20 Jan 2023 13:08:10 +0100 Subject: [PATCH 06/20] Add identifier for predicate functions. --- src/AsyncProcess/index.ts | 48 +++++++++++-------- src/__tests__/AsyncProcess.test.ts | 25 ++++++---- .../__snapshots__/AsyncProcess.test.ts.snap | 28 +++++------ src/types/index.ts | 5 ++ 4 files changed, 63 insertions(+), 43 deletions(-) diff --git a/src/AsyncProcess/index.ts b/src/AsyncProcess/index.ts index 39163b7..a518f55 100644 --- a/src/AsyncProcess/index.ts +++ b/src/AsyncProcess/index.ts @@ -9,7 +9,7 @@ import { IAsyncProcessFns, IAsyncProcessOptions, Jobs, - PredicateFn + PredicateFns } from '../types' /** @@ -40,11 +40,10 @@ export class AsyncProcess { jobs: new Map(), onStartFns: new Map(), onSuccessFns: new Map(), - onErrorFns: new Map() + onErrorFns: new Map(), + predicateFns: new Map() } - private _predicateFns: Set> = new Set() - /** * Static */ @@ -85,7 +84,7 @@ export class AsyncProcess { */ do(jobs: Jobs, identifier = 'defaultJobs'): this { this._log('functionsRegistrations')('debug')( - `Register jobs function(s) with the identifier "${identifier}"` + `Register "${identifier}" jobs function(s)` ) this._fns.jobs.set(identifier, new Set(ensureArray(jobs))) @@ -95,8 +94,11 @@ export class AsyncProcess { /** * Save a predicate function to trigger or not the process. */ - if(predicateFn: PredicateFn): this { - this._predicateFns.add(predicateFn) + if( + predicateFns: PredicateFns, + identifier = 'defaultPredicate' + ): this { + this._fns.predicateFns.set(identifier, new Set(ensureArray(predicateFns))) return this } @@ -105,7 +107,7 @@ export class AsyncProcess { */ onStart(jobs: Jobs, identifier = 'defaultOnStart'): this { this._log('functionsRegistrations')('debug')( - `Register onStart function(s) with the identifier "${identifier}"` + `Register "${identifier}" onStart function(s)` ) this._fns.onStartFns.set(identifier, new Set(ensureArray(jobs))) @@ -117,7 +119,7 @@ export class AsyncProcess { */ onSuccess(jobs: Jobs, identifier = 'defaultOnSuccess'): this { this._log('functionsRegistrations')('debug')( - `Register onSuccess function(s) with the identifier "${identifier}"` + `Register "${identifier}" onSuccess function(s)` ) this._fns.onSuccessFns.set(identifier, new Set(ensureArray(jobs))) @@ -129,7 +131,7 @@ export class AsyncProcess { */ onError(jobs: AsyncErrorFns, identifier = 'defaultOnError'): this { this._log('functionsRegistrations')('debug')( - `Register onError function(s) with the identifier "${identifier}"` + `Register "${identifier}" onError function(s)` ) this._fns.onErrorFns.set(identifier, new Set(ensureArray(jobs))) @@ -193,7 +195,7 @@ export class AsyncProcess { this._error = null try { - if (this._predicateFns && !(await this.shouldStart())) { + if (this._fns.predicateFns.size && !(await this.shouldStart())) { await this._execJobs(this._fns.onSuccessFns) this.shouldDeleteFunctions() @@ -218,15 +220,18 @@ export class AsyncProcess { * Return a boolean according to registered predicates values. */ async shouldStart(): Promise { - for (const predicateFn of this._predicateFns) { - if (!(await predicateFn(this))) { - this._log('functionsExecutions')('debug')( - `Skip jobs execution because predicate is falsy` - ) - - return false + for (const [identifier, predicateFns] of this._fns.predicateFns.entries()) { + for (const predicateFn of predicateFns) { + if (!(await predicateFn(this))) { + this._log('functionsExecutions')('debug')( + `Skip jobs execution because of the "${identifier}" predicate` + ) + + return false + } } } + return true } @@ -246,7 +251,8 @@ export class AsyncProcess { jobs: new Map(), onStartFns: new Map(), onSuccessFns: new Map(), - onErrorFns: new Map() + onErrorFns: new Map(), + predicateFns: new Map() } return this @@ -275,7 +281,7 @@ export class AsyncProcess { for (const [identifier, fns] of jobs.entries()) { for (const fn of fns) { this._log('functionsExecutions')('debug')( - `Execute jobs functions with the identifier "${identifier}"` + `Execute "${identifier}" jobs function(s)` ) await fn() @@ -295,7 +301,7 @@ export class AsyncProcess { for (const [identifier, fns] of asyncErrorFns.entries()) { for (const fn of fns) { this._log('functionsExecutions')('debug')( - `Execute onError functions with the identifier "${identifier}"` + `Execute "${identifier}" onError function(s)` ) await fn(err) diff --git a/src/__tests__/AsyncProcess.test.ts b/src/__tests__/AsyncProcess.test.ts index 7c4abe2..16f69db 100644 --- a/src/__tests__/AsyncProcess.test.ts +++ b/src/__tests__/AsyncProcess.test.ts @@ -1,6 +1,6 @@ import { Maybe } from '@productive-codebases/toolbox' import { AsyncProcess } from '../AsyncProcess' -import { debug, logger } from '../logger' +import { debug } from '../logger' import { IAsyncProcessOptions } from '../types' interface IData { @@ -317,14 +317,17 @@ describe('AsyncProcess', () => { const fetchData = jest.fn() const predicateFn1 = jest.fn().mockImplementation(() => true) const predicateFn2 = jest.fn().mockImplementation(() => true) + const predicateFn3 = jest.fn().mockImplementation(() => true) + const predicateFn4 = jest.fn().mockImplementation(() => true) const onStartFn = jest.fn() const onSuccessFn = jest.fn() const onErrorFn = jest.fn() const asyncProcess = getAsyncProcessTestInstance('loadFoo') .do(fetchData) - .if(predicateFn1) - .if(predicateFn2) + .if(predicateFn1, 'predicate1') + .if(predicateFn2, 'predicate2') + .if([predicateFn3, predicateFn4], 'predicate3and4') .onStart(onStartFn) .onSuccess(onSuccessFn) .onError(onErrorFn) @@ -333,6 +336,8 @@ describe('AsyncProcess', () => { expect(predicateFn1).toHaveBeenCalled() expect(predicateFn2).toHaveBeenCalled() + expect(predicateFn3).toHaveBeenCalled() + expect(predicateFn4).toHaveBeenCalled() expect(fetchData).toHaveBeenCalled() expect(onStartFn).toHaveBeenCalled() expect(onSuccessFn).toHaveBeenCalled() @@ -367,15 +372,16 @@ describe('AsyncProcess', () => { const predicateFn1 = jest.fn().mockImplementation(() => true) const predicateFn2 = jest.fn().mockImplementation(() => true) const predicateFn3 = jest.fn().mockImplementation(() => false) + const predicateFn4 = jest.fn().mockImplementation(() => true) const onStartFn = jest.fn() const onSuccessFn = jest.fn() const onErrorFn = jest.fn() const asyncProcess = getAsyncProcessTestInstance('loadFoo') .do(fetchData) - .if(predicateFn1) - .if(predicateFn2) - .if(predicateFn3) + .if(predicateFn1, 'predicate1') + .if(predicateFn2, 'predicate2') + .if([predicateFn3, predicateFn4], 'predicate3and4') .onStart(onStartFn) .onSuccess(onSuccessFn) .onError(onErrorFn) @@ -385,6 +391,8 @@ describe('AsyncProcess', () => { expect(predicateFn1).toHaveBeenCalled() expect(predicateFn2).toHaveBeenCalled() expect(predicateFn3).toHaveBeenCalled() + // not called because predicateFn3 is falsy + expect(predicateFn4).not.toHaveBeenCalled() expect(fetchData).not.toHaveBeenCalled() expect(onStartFn).not.toHaveBeenCalled() expect(onSuccessFn).toHaveBeenCalled() @@ -590,7 +598,7 @@ describe('AsyncProcess', () => { const onSuccessFn = jest.fn() const onErrorFn = jest.fn() - await getAsyncProcessTestInstance('loadFoo') + const asyncProcess = getAsyncProcessTestInstance('loadFoo') .setOptions({ debug: { logFunctionRegistrations: true, @@ -602,7 +610,8 @@ describe('AsyncProcess', () => { .onStart(onStartFn) .onSuccess(onSuccessFn) .onError(onErrorFn) - .start() + + await asyncProcess.start() expect(logs).toMatchSnapshot() }) diff --git a/src/__tests__/__snapshots__/AsyncProcess.test.ts.snap b/src/__tests__/__snapshots__/AsyncProcess.test.ts.snap index 8621474..4dc458f 100644 --- a/src/__tests__/__snapshots__/AsyncProcess.test.ts.snap +++ b/src/__tests__/__snapshots__/AsyncProcess.test.ts.snap @@ -4,13 +4,13 @@ exports[`AsyncProcess With options: { debug: { logFunctionRegistrations: true, logFunctionExecutions: true } } Options Logging should log different events if error 1`] = ` [ - "AsyncProcess:functionsRegistrations:debug [loadFoo] Register jobs function(s) with the identifier "defaultJobs"", - "AsyncProcess:functionsRegistrations:debug [loadFoo] Register onStart function(s) with the identifier "defaultOnStart"", - "AsyncProcess:functionsRegistrations:debug [loadFoo] Register onSuccess function(s) with the identifier "defaultOnSuccess"", - "AsyncProcess:functionsRegistrations:debug [loadFoo] Register onError function(s) with the identifier "defaultOnError"", - "AsyncProcess:functionsExecutions:debug [loadFoo] Execute jobs functions with the identifier "defaultOnStart"", - "AsyncProcess:functionsExecutions:debug [loadFoo] Execute jobs functions with the identifier "defaultJobs"", - "AsyncProcess:functionsExecutions:debug [loadFoo] Execute onError functions with the identifier "defaultOnError"", + "AsyncProcess:functionsRegistrations:debug [loadFoo] Register "defaultJobs" jobs function(s)", + "AsyncProcess:functionsRegistrations:debug [loadFoo] Register "defaultOnStart" onStart function(s)", + "AsyncProcess:functionsRegistrations:debug [loadFoo] Register "defaultOnSuccess" onSuccess function(s)", + "AsyncProcess:functionsRegistrations:debug [loadFoo] Register "defaultOnError" onError function(s)", + "AsyncProcess:functionsExecutions:debug [loadFoo] Execute "defaultOnStart" jobs function(s)", + "AsyncProcess:functionsExecutions:debug [loadFoo] Execute "defaultJobs" jobs function(s)", + "AsyncProcess:functionsExecutions:debug [loadFoo] Execute "defaultOnError" onError function(s)", ] `; @@ -18,13 +18,13 @@ exports[`AsyncProcess With options: { debug: { logFunctionRegistrations: true, logFunctionExecutions: true } } Options Logging should log different events if success 1`] = ` [ - "AsyncProcess:functionsRegistrations:debug [loadFoo] Register jobs function(s) with the identifier "defaultJobs"", - "AsyncProcess:functionsRegistrations:debug [loadFoo] Register onStart function(s) with the identifier "defaultOnStart"", - "AsyncProcess:functionsRegistrations:debug [loadFoo] Register onSuccess function(s) with the identifier "defaultOnSuccess"", - "AsyncProcess:functionsRegistrations:debug [loadFoo] Register onError function(s) with the identifier "defaultOnError"", - "AsyncProcess:functionsExecutions:debug [loadFoo] Execute jobs functions with the identifier "defaultOnStart"", - "AsyncProcess:functionsExecutions:debug [loadFoo] Execute jobs functions with the identifier "defaultJobs"", - "AsyncProcess:functionsExecutions:debug [loadFoo] Execute jobs functions with the identifier "defaultOnSuccess"", + "AsyncProcess:functionsRegistrations:debug [loadFoo] Register "defaultJobs" jobs function(s)", + "AsyncProcess:functionsRegistrations:debug [loadFoo] Register "defaultOnStart" onStart function(s)", + "AsyncProcess:functionsRegistrations:debug [loadFoo] Register "defaultOnSuccess" onSuccess function(s)", + "AsyncProcess:functionsRegistrations:debug [loadFoo] Register "defaultOnError" onError function(s)", + "AsyncProcess:functionsExecutions:debug [loadFoo] Execute "defaultOnStart" jobs function(s)", + "AsyncProcess:functionsExecutions:debug [loadFoo] Execute "defaultJobs" jobs function(s)", + "AsyncProcess:functionsExecutions:debug [loadFoo] Execute "defaultOnSuccess" jobs function(s)", ] `; diff --git a/src/types/index.ts b/src/types/index.ts index 57cf4c2..4c5a524 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -15,11 +15,16 @@ export type PredicateFn = ( asyncProcess: AsyncProcess ) => boolean | Promise +export type PredicateFns = + | PredicateFn + | Array> + export interface IAsyncProcessFns { jobs: Map> onStartFns: Map> onSuccessFns: Map> onErrorFns: Map> + predicateFns: Map>> } export type AsyncProcessIdentifiers = [ From 1bf27fab5ff61ed81717bc667f6edb1eb4c85460 Mon Sep 17 00:00:00 2001 From: Alexis Mineaud Date: Fri, 20 Jan 2023 11:43:28 +0100 Subject: [PATCH 07/20] Update CHANGELOG. --- CHANGELOG.md | 12 ++++++++++++ README.md | 4 ++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81b55d5..9b5f8b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## v2.0.0 + +### Added + +- :warning: **[breaking-change]** Add an identifier for each function registration. When using a same identifier, **the new registered function(s) replace(s) the previous one(s)**. + +- :warning: **[breaking-change]** Add an identifier for predicates functions to mimic to same behavior as functions registrations. Several predicate functions can now be passed in a single `if()` with a defined identifier. In the same way, if an another `if()` predicate is set with the same identifier, **it will override the previous one**. + +- Add an option `deleteFunctionsWhenJobsStarted` allowing to delete the registered functions when the jobs are started. + +- Add an option `debug.logFunctionRegistrations` and `debug.logFunctionExecutions` to debug functions registrations and executions. Don't forget to set `DEBUG=AsyncProcess:*` as an environment variable or in your browser's localstorage. + ## v1.2.0 ### Added diff --git a/README.md b/README.md index 3e870ee..91d1f21 100644 --- a/README.md +++ b/README.md @@ -71,9 +71,9 @@ It's important to note that nothing is happening unless the `start` function is In order to retrieve the same `AsyncProcess` declarations accross your application, `AsyncProcess` is an unique singleton that saves all instances. -Each instance are referenced via an identifer and optional sub identifiers. +Each instance are referenced via an identifer and optional sub identifiers. There is no inheritance between AsyncProcess instances meaning that an instance with a same identifier but different sub-identifiers are two different instances. -The first identifier can be typed, typically by using an union of different string values, allowing to retrieve your instances in a safe way. Optional sub identifiers can be used for uuid or any arguments that identify your async process more precisely. +The first identifier can be typed, typically by using an union of different string values, allowing to retrieve your instances in a safe way. Optional sub identifiers are generally used by [composite functions](#composition) (`with(...)`) for internal AsyncProcess instances. ### Typed identifiers From 45c45c7511dca5be13fe23c23f9ff848e260a250 Mon Sep 17 00:00:00 2001 From: Alexis Mineaud Date: Fri, 20 Jan 2023 13:08:21 +0100 Subject: [PATCH 08/20] 2.0.0-beta.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 63e85f9..85b60c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@productive-codebases/async-process", - "version": "1.2.0", + "version": "2.0.0-beta.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@productive-codebases/async-process", - "version": "1.2.0", + "version": "2.0.0-beta.0", "license": "MIT", "devDependencies": { "@types/jest": "^29.2.5", diff --git a/package.json b/package.json index b9cea51..e0d5fad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@productive-codebases/async-process", - "version": "1.2.0", + "version": "2.0.0-beta.0", "description": "Declare, configure and start asynchronous processes with ease.", "author": "Alexis MINEAUD", "license": "MIT", From ea77ba95f7dc594b766b2e46182850f4ee0ebcc6 Mon Sep 17 00:00:00 2001 From: Alexis Mineaud Date: Fri, 20 Jan 2023 18:29:11 +0100 Subject: [PATCH 09/20] Delete functions for failed cases as well. --- src/AsyncProcess/index.ts | 16 +++----- src/__tests__/AsyncProcess.test.ts | 61 +++++++++++++++++++++++++++--- 2 files changed, 62 insertions(+), 15 deletions(-) diff --git a/src/AsyncProcess/index.ts b/src/AsyncProcess/index.ts index a518f55..2991785 100644 --- a/src/AsyncProcess/index.ts +++ b/src/AsyncProcess/index.ts @@ -197,20 +197,16 @@ export class AsyncProcess { try { if (this._fns.predicateFns.size && !(await this.shouldStart())) { await this._execJobs(this._fns.onSuccessFns) - - this.shouldDeleteFunctions() - - return this + } else { + await this._execJobs(this._fns.onStartFns) + await this._execJobs(this._fns.jobs) + await this._execJobs(this._fns.onSuccessFns) } - - await this._execJobs(this._fns.onStartFns) - await this._execJobs(this._fns.jobs) - await this._execJobs(this._fns.onSuccessFns) - - this.shouldDeleteFunctions() } catch (err) { this._error = err instanceof Error ? err : new Error('Unknown error') this._execAsyncErrorFns(this._error, this._fns.onErrorFns) + } finally { + this.shouldDeleteFunctions() } return this diff --git a/src/__tests__/AsyncProcess.test.ts b/src/__tests__/AsyncProcess.test.ts index 16f69db..c252e7c 100644 --- a/src/__tests__/AsyncProcess.test.ts +++ b/src/__tests__/AsyncProcess.test.ts @@ -522,25 +522,76 @@ describe('AsyncProcess', () => { describe('Options', () => { describe('deleteFunctionsWhenJobsStarted', () => { - it('should reset functions when async process is started', async () => { + it('should reset functions when async process is successful', async () => { + const fetchData = jest.fn() const successSpy1 = jest.fn() const successSpy2 = jest.fn() const successSpy3 = jest.fn() + const errorSpy1 = jest.fn() - const foo1 = getAsyncProcessTestInstance('loadFoo') + const foo1 = getAsyncProcessTestInstance('loadFoo').do(fetchData) - foo1.onSuccess(successSpy1, 'success1') - foo1.onSuccess(successSpy2, 'success2') + foo1 + .onSuccess(successSpy1, 'success1') + .onSuccess(successSpy2, 'success2') + .onError(errorSpy1, 'error1') expect(foo1.fns.onSuccessFns.size).toBe(2) + expect(foo1.fns.onErrorFns.size).toBe(1) await foo1.start() - foo1.onSuccess(successSpy3, 'success3') + foo1 + .onSuccess(successSpy3, 'success3') + .onError(errorSpy1, 'error1bis') expect(foo1.fns.onSuccessFns.size).toBe( + // if deleteFunctionsWhenJobsStarted, fns have been removed after the start, + // so we get only `success3` options.deleteFunctionsWhenJobsStarted ? 1 : 3 ) + + expect(foo1.fns.onErrorFns.size).toBe( + // if deleteFunctionsWhenJobsStarted, fns have been removed after the start, + // so we get only `error1bis` + options.deleteFunctionsWhenJobsStarted ? 1 : 2 + ) + }) + + it('should reset functions when async process has failed', async () => { + const fetchData = jest.fn().mockImplementation(() => Promise.reject()) + const successSpy1 = jest.fn() + const successSpy2 = jest.fn() + const successSpy3 = jest.fn() + const errorSpy1 = jest.fn() + + const foo1 = getAsyncProcessTestInstance('loadFoo').do(fetchData) + + foo1 + .onSuccess(successSpy1, 'success1') + .onSuccess(successSpy2, 'success2') + .onError(errorSpy1, 'error1') + + expect(foo1.fns.onSuccessFns.size).toBe(2) + expect(foo1.fns.onErrorFns.size).toBe(1) + + await foo1.start() + + foo1 + .onSuccess(successSpy3, 'success3') + .onError(errorSpy1, 'error1bis') + + expect(foo1.fns.onSuccessFns.size).toBe( + // if deleteFunctionsWhenJobsStarted, fns have been removed after the start, + // so we get only `success3` + options.deleteFunctionsWhenJobsStarted ? 1 : 3 + ) + + expect(foo1.fns.onErrorFns.size).toBe( + // if deleteFunctionsWhenJobsStarted, fns have been removed after the start, + // so we get only `error1bis` + options.deleteFunctionsWhenJobsStarted ? 1 : 2 + ) }) }) From 3f78d070eb0a6eda1da5321ee71392e949a05488 Mon Sep 17 00:00:00 2001 From: Alexis Mineaud Date: Fri, 20 Jan 2023 19:37:01 +0100 Subject: [PATCH 10/20] Return error as it. --- src/AsyncProcess/index.ts | 10 +++++----- src/__tests__/AsyncProcess.test.ts | 14 ++++++++++---- src/types/index.ts | 4 ++-- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/AsyncProcess/index.ts b/src/AsyncProcess/index.ts index 2991785..bdf91d1 100644 --- a/src/AsyncProcess/index.ts +++ b/src/AsyncProcess/index.ts @@ -34,7 +34,7 @@ export class AsyncProcess { } } - private _error: Maybe = null + private _error: Maybe = null private _fns: IAsyncProcessFns = { jobs: new Map(), @@ -203,8 +203,8 @@ export class AsyncProcess { await this._execJobs(this._fns.onSuccessFns) } } catch (err) { - this._error = err instanceof Error ? err : new Error('Unknown error') - this._execAsyncErrorFns(this._error, this._fns.onErrorFns) + this._error = err + this._execAsyncErrorFns(err, this._fns.onErrorFns) } finally { this.shouldDeleteFunctions() } @@ -262,7 +262,7 @@ export class AsyncProcess { return this._fns } - get error(): Maybe { + get error(): Maybe { return this._error } @@ -291,7 +291,7 @@ export class AsyncProcess { * Execute sequentially async error functions. */ private async _execAsyncErrorFns( - err: Error, + err: unknown, asyncErrorFns: Map> ): Promise { for (const [identifier, fns] of asyncErrorFns.entries()) { diff --git a/src/__tests__/AsyncProcess.test.ts b/src/__tests__/AsyncProcess.test.ts index c252e7c..f0eb44e 100644 --- a/src/__tests__/AsyncProcess.test.ts +++ b/src/__tests__/AsyncProcess.test.ts @@ -185,11 +185,15 @@ describe('AsyncProcess', () => { expect(onErrorFn).toHaveBeenCalledWith(error) }) - it('should expose the Error', async () => { + it('should expose the Error object as it', async () => { + class CustomError { + constructor(readonly error: string) {} + } + const fetchData = (): Promise => { return new Promise((_, reject) => { setTimeout(() => { - reject(new Error('Something bad happened')) + reject(new CustomError('Something bad happened')) }, 50) }) } @@ -200,8 +204,10 @@ describe('AsyncProcess', () => { await asyncProcess.start() expect(data).toEqual(null) - expect(asyncProcess.error).toBeInstanceOf(Error) - expect(asyncProcess.error?.message).toEqual('Something bad happened') + expect(asyncProcess.error).toBeInstanceOf(CustomError) + expect(asyncProcess.error).toEqual( + new CustomError('Something bad happened') + ) }) it('should return a success promise even if an error occurred', async () => { diff --git a/src/types/index.ts b/src/types/index.ts index 4c5a524..1a0541c 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -6,8 +6,8 @@ export type AsyncFn = () => Promise export type FnOrAsyncFn = Fn | AsyncFn export type Jobs = FnOrAsyncFn | Array -export type ErrorFn = (err: Error) => any -export type AsyncErrorFn = (err: Error) => Promise +export type ErrorFn = (err: unknown) => any +export type AsyncErrorFn = (err: unknown) => Promise export type FnOrAsyncErrorFn = ErrorFn | AsyncErrorFn export type AsyncErrorFns = FnOrAsyncErrorFn | Array From 7699845e0b2fd8ea55462624b86ff72272f65ad8 Mon Sep 17 00:00:00 2001 From: Alexis Mineaud Date: Fri, 20 Jan 2023 19:37:17 +0100 Subject: [PATCH 11/20] Return results of jobs. --- src/AsyncProcess/index.ts | 19 ++++++++++---- src/__tests__/AsyncProcess.test.ts | 42 +++++++++++++++++++++++++++--- 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/src/AsyncProcess/index.ts b/src/AsyncProcess/index.ts index bdf91d1..cbbb6ac 100644 --- a/src/AsyncProcess/index.ts +++ b/src/AsyncProcess/index.ts @@ -35,6 +35,7 @@ export class AsyncProcess { } private _error: Maybe = null + private _result: Maybe = null private _fns: IAsyncProcessFns = { jobs: new Map(), @@ -196,14 +197,16 @@ export class AsyncProcess { try { if (this._fns.predicateFns.size && !(await this.shouldStart())) { - await this._execJobs(this._fns.onSuccessFns) + this._result = await this._execJobs(this._fns.onSuccessFns) } else { await this._execJobs(this._fns.onStartFns) - await this._execJobs(this._fns.jobs) + this._result = await this._execJobs(this._fns.jobs) await this._execJobs(this._fns.onSuccessFns) } } catch (err) { this._error = err + this._result = null + this._execAsyncErrorFns(err, this._fns.onErrorFns) } finally { this.shouldDeleteFunctions() @@ -266,6 +269,10 @@ export class AsyncProcess { return this._error } + get result(): Maybe { + return this._result + } + /** * Private */ @@ -273,18 +280,20 @@ export class AsyncProcess { /** * Execute sequentially async functions. */ - private async _execJobs(jobs: Map>): Promise { + private async _execJobs(jobs: Map>): Promise { + const results: unknown[] = [] + for (const [identifier, fns] of jobs.entries()) { for (const fn of fns) { this._log('functionsExecutions')('debug')( `Execute "${identifier}" jobs function(s)` ) - await fn() + results.push(await fn()) } } - return this + return results.length === 1 ? results.pop() : results } /** diff --git a/src/__tests__/AsyncProcess.test.ts b/src/__tests__/AsyncProcess.test.ts index f0eb44e..ab9eedb 100644 --- a/src/__tests__/AsyncProcess.test.ts +++ b/src/__tests__/AsyncProcess.test.ts @@ -128,6 +128,38 @@ describe('AsyncProcess', () => { expect(doSomethingAfterAsyncProcess).toHaveBeenCalled() }) + + it('should expose the result', async () => { + const fetchData = jest + .fn() + .mockImplementation(() => Promise.resolve({ foo: 'bar' })) + + const asyncProcess = + getAsyncProcessTestInstance('loadFoo').do(fetchData) + + await asyncProcess.start() + + expect(asyncProcess.result).toEqual({ foo: 'bar' }) + }) + + it('should expose the result as an array is multiple jobs', async () => { + const fetchData1 = jest + .fn() + .mockImplementation(() => Promise.resolve({ foo: 'bar' })) + + const fetchData2 = jest + .fn() + .mockImplementation(() => Promise.resolve({ foo2: 'bar2' })) + + const asyncProcess = getAsyncProcessTestInstance('loadFoo').do([ + fetchData1, + fetchData2 + ]) + + await asyncProcess.start() + + expect(asyncProcess.result).toEqual([{ foo: 'bar' }, { foo2: 'bar2' }]) + }) }) describe('On error', () => { @@ -164,12 +196,14 @@ describe('AsyncProcess', () => { }) it('should pass the Error to error functions', async () => { - const error = new Error('Something bad happened') + class CustomError { + constructor(readonly error: string) {} + } const fetchData = (): Promise => { return new Promise((_, reject) => { setTimeout(() => { - reject(error) + reject(new CustomError('Something bad happened')) }, 50) }) } @@ -182,7 +216,9 @@ describe('AsyncProcess', () => { await asyncProcess.start() - expect(onErrorFn).toHaveBeenCalledWith(error) + expect(onErrorFn).toHaveBeenCalledWith( + new CustomError('Something bad happened') + ) }) it('should expose the Error object as it', async () => { From 2a314ee1030ef2a2abd0e2298e3a96f59df61cf1 Mon Sep 17 00:00:00 2001 From: Alexis Mineaud Date: Fri, 20 Jan 2023 19:41:50 +0100 Subject: [PATCH 12/20] Use jest functions everywhere. --- src/__tests__/AsyncProcess.test.ts | 103 ++++++++--------------------- 1 file changed, 26 insertions(+), 77 deletions(-) diff --git a/src/__tests__/AsyncProcess.test.ts b/src/__tests__/AsyncProcess.test.ts index ab9eedb..f066ff0 100644 --- a/src/__tests__/AsyncProcess.test.ts +++ b/src/__tests__/AsyncProcess.test.ts @@ -21,12 +21,6 @@ describe('AsyncProcess', () => { } } ])('With options: %o', options => { - let data: Maybe = null - - const setData = (data_: any) => { - data = data_ - } - function getAsyncProcessTestInstance( identifier: AsyncProcessTestIdentifier, subIdentifiers?: string[] @@ -37,21 +31,14 @@ describe('AsyncProcess', () => { } beforeEach(() => { - data = null AsyncProcess.clearInstances() }) describe('On success', () => { it('should call the onStart / onSuccess fn(s)', async () => { - const fetchData = (): Promise => { - return new Promise(resolve => { - setTimeout(() => { - setData({ id: 1, name: 'Bob' }) - resolve(null) - }, 50) - }) - } - + const fetchData = jest + .fn() + .mockImplementation(() => Promise.resolve({ id: 1, name: 'Bob' })) const onStartFns = [jest.fn(), jest.fn()] const onSuccessFns = [jest.fn()] const onErrorFn = jest.fn() @@ -73,21 +60,10 @@ describe('AsyncProcess', () => { }) expect(onErrorFn).not.toHaveBeenCalled() - - expect(data).toEqual({ - id: 1, - name: 'Bob' - }) }) it('should call the functions sequentially', async () => { - const fetchData = (): Promise => { - return new Promise(resolve => { - setTimeout(() => { - resolve(null) - }, 50) - }) - } + const fetchData = jest.fn().mockImplementation(() => Promise.resolve()) const successValues: number[] = [] @@ -111,13 +87,7 @@ describe('AsyncProcess', () => { }) it('should return a success promise', async () => { - const fetchData = (): Promise => { - return new Promise(resolve => { - setTimeout(() => { - resolve(null) - }, 50) - }) - } + const fetchData = jest.fn().mockImplementation(() => Promise.resolve()) const asyncProcess = getAsyncProcessTestInstance('loadFoo').do(fetchData) @@ -164,14 +134,11 @@ describe('AsyncProcess', () => { describe('On error', () => { it('should call the onStart / onError fn(s)', async () => { - const fetchData = (): Promise => { - return new Promise((_, reject) => { - setTimeout(() => { - reject(new Error('Something bad happened')) - }, 50) - }) - } - + const fetchData = jest + .fn() + .mockImplementation(() => + Promise.reject(new Error('Something bad happened')) + ) const onStartFns = [jest.fn(), jest.fn()] const onSuccessFns = [jest.fn()] const onErrorFn = jest.fn() @@ -192,7 +159,7 @@ describe('AsyncProcess', () => { expect(onSuccessFn).not.toHaveBeenCalled() }) - expect(data).toEqual(null) + expect(asyncProcess.result).toEqual(null) }) it('should pass the Error to error functions', async () => { @@ -200,14 +167,11 @@ describe('AsyncProcess', () => { constructor(readonly error: string) {} } - const fetchData = (): Promise => { - return new Promise((_, reject) => { - setTimeout(() => { - reject(new CustomError('Something bad happened')) - }, 50) - }) - } - + const fetchData = jest + .fn() + .mockImplementation(() => + Promise.reject(new CustomError('Something bad happened')) + ) const onErrorFn = jest.fn() const asyncProcess = getAsyncProcessTestInstance('loadFoo') @@ -226,20 +190,16 @@ describe('AsyncProcess', () => { constructor(readonly error: string) {} } - const fetchData = (): Promise => { - return new Promise((_, reject) => { - setTimeout(() => { - reject(new CustomError('Something bad happened')) - }, 50) - }) - } - + const fetchData = jest + .fn() + .mockImplementation(() => + Promise.reject(new CustomError('Something bad happened')) + ) const asyncProcess = getAsyncProcessTestInstance('loadFoo').do(fetchData) await asyncProcess.start() - expect(data).toEqual(null) expect(asyncProcess.error).toBeInstanceOf(CustomError) expect(asyncProcess.error).toEqual( new CustomError('Something bad happened') @@ -247,13 +207,7 @@ describe('AsyncProcess', () => { }) it('should return a success promise even if an error occurred', async () => { - const fetchData = (): Promise => { - return new Promise((_, reject) => { - setTimeout(() => { - reject() - }, 50) - }) - } + const fetchData = jest.fn().mockImplementation(() => Promise.reject()) let errorMessage: Maybe = null @@ -444,14 +398,9 @@ describe('AsyncProcess', () => { describe('AsyncProcess composition', () => { it('should compose AsyncProcess instances', async () => { - const fetchData = (): Promise => { - return new Promise(resolve => { - setTimeout(() => { - setData({ id: 1, name: 'Bob' }) - resolve(null) - }, 50) - }) - } + const fetchData = jest + .fn() + .mockImplementation(() => Promise.resolve({ id: 1, name: 'Bob' })) const onStartFn0 = jest.fn() const onStartFn1 = jest.fn() @@ -506,7 +455,7 @@ describe('AsyncProcess', () => { options.deleteFunctionsWhenJobsStarted ? 0 : 4 ) - expect(data).toEqual({ + expect(asyncProcess.result).toEqual({ id: 1, name: 'Bob' }) From 36965f23ab3ceeabe949a02fcdc17917ec7708ad Mon Sep 17 00:00:00 2001 From: Alexis Mineaud Date: Fri, 20 Jan 2023 20:03:24 +0100 Subject: [PATCH 13/20] Add a way to type results. --- src/AsyncProcess/index.ts | 39 ++++++++++++++++++------------ src/__tests__/AsyncProcess.test.ts | 17 ++++++------- src/types/index.ts | 18 +++++++------- 3 files changed, 39 insertions(+), 35 deletions(-) diff --git a/src/AsyncProcess/index.ts b/src/AsyncProcess/index.ts index cbbb6ac..053a616 100644 --- a/src/AsyncProcess/index.ts +++ b/src/AsyncProcess/index.ts @@ -1,11 +1,16 @@ -import { ensureArray, Maybe, MetaData } from '@productive-codebases/toolbox' +import { + deepMerge, + ensureArray, + Maybe, + MetaData +} from '@productive-codebases/toolbox' import { LoggerLevel } from '@productive-codebases/toolbox/dist/types/libs/logger/types' import { logger, LoggerNamespace } from '../logger' import { - AsyncErrorFn, AsyncErrorFns, - AsyncFn, AsyncProcessIdentifiers, + FnOrAsyncErrorFn, + FnOrAsyncFn, IAsyncProcessFns, IAsyncProcessOptions, Jobs, @@ -17,7 +22,7 @@ import { * and executes functions according to the successes / errors. */ -export class AsyncProcess { +export class AsyncProcess { public metadata = new MetaData() private _identifiers: AsyncProcessIdentifiers @@ -35,9 +40,9 @@ export class AsyncProcess { } private _error: Maybe = null - private _result: Maybe = null + private _result: Maybe = null - private _fns: IAsyncProcessFns = { + private _fns: IAsyncProcessFns = { jobs: new Map(), onStartFns: new Map(), onSuccessFns: new Map(), @@ -83,7 +88,7 @@ export class AsyncProcess { /** * Save jobs function(s). */ - do(jobs: Jobs, identifier = 'defaultJobs'): this { + do(jobs: Jobs, identifier = 'defaultJobs'): this { this._log('functionsRegistrations')('debug')( `Register "${identifier}" jobs function(s)` ) @@ -106,7 +111,7 @@ export class AsyncProcess { /** * Save functions to execute before starting jobs. */ - onStart(jobs: Jobs, identifier = 'defaultOnStart'): this { + onStart(jobs: Jobs, identifier = 'defaultOnStart'): this { this._log('functionsRegistrations')('debug')( `Register "${identifier}" onStart function(s)` ) @@ -118,7 +123,7 @@ export class AsyncProcess { /** * Save functions to execute after jobs are succesful. */ - onSuccess(jobs: Jobs, identifier = 'defaultOnSuccess'): this { + onSuccess(jobs: Jobs, identifier = 'defaultOnSuccess'): this { this._log('functionsRegistrations')('debug')( `Register "${identifier}" onSuccess function(s)` ) @@ -261,7 +266,7 @@ export class AsyncProcess { * Getters */ - get fns(): IAsyncProcessFns { + get fns(): IAsyncProcessFns { return this._fns } @@ -269,7 +274,7 @@ export class AsyncProcess { return this._error } - get result(): Maybe { + get result(): Maybe { return this._result } @@ -280,8 +285,10 @@ export class AsyncProcess { /** * Execute sequentially async functions. */ - private async _execJobs(jobs: Map>): Promise { - const results: unknown[] = [] + private async _execJobs( + jobs: Map>> + ): Promise> { + let results: Maybe = null for (const [identifier, fns] of jobs.entries()) { for (const fn of fns) { @@ -289,11 +296,11 @@ export class AsyncProcess { `Execute "${identifier}" jobs function(s)` ) - results.push(await fn()) + results = deepMerge([results, await fn()]) } } - return results.length === 1 ? results.pop() : results + return results } /** @@ -301,7 +308,7 @@ export class AsyncProcess { */ private async _execAsyncErrorFns( err: unknown, - asyncErrorFns: Map> + asyncErrorFns: Map> ): Promise { for (const [identifier, fns] of asyncErrorFns.entries()) { for (const fn of fns) { diff --git a/src/__tests__/AsyncProcess.test.ts b/src/__tests__/AsyncProcess.test.ts index f066ff0..0d15354 100644 --- a/src/__tests__/AsyncProcess.test.ts +++ b/src/__tests__/AsyncProcess.test.ts @@ -3,11 +3,6 @@ import { AsyncProcess } from '../AsyncProcess' import { debug } from '../logger' import { IAsyncProcessOptions } from '../types' -interface IData { - id: number - name: string -} - type AsyncProcessTestIdentifier = 'loadFoo' | 'loadBar' describe('AsyncProcess', () => { @@ -21,10 +16,10 @@ describe('AsyncProcess', () => { } } ])('With options: %o', options => { - function getAsyncProcessTestInstance( + function getAsyncProcessTestInstance( identifier: AsyncProcessTestIdentifier, subIdentifiers?: string[] - ): AsyncProcess { + ): AsyncProcess { return AsyncProcess.instance(identifier, subIdentifiers).setOptions( options ) @@ -104,8 +99,10 @@ describe('AsyncProcess', () => { .fn() .mockImplementation(() => Promise.resolve({ foo: 'bar' })) - const asyncProcess = - getAsyncProcessTestInstance('loadFoo').do(fetchData) + // typings are only indicative, no validation is sone by AsyncProcess + const asyncProcess = getAsyncProcessTestInstance<{ foo: string }>( + 'loadFoo' + ).do(fetchData) await asyncProcess.start() @@ -128,7 +125,7 @@ describe('AsyncProcess', () => { await asyncProcess.start() - expect(asyncProcess.result).toEqual([{ foo: 'bar' }, { foo2: 'bar2' }]) + expect(asyncProcess.result).toEqual({ foo: 'bar', foo2: 'bar2' }) }) }) diff --git a/src/types/index.ts b/src/types/index.ts index 1a0541c..b4df06c 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,10 +1,10 @@ import { Maybe } from '@productive-codebases/toolbox' import { AsyncProcess } from '../AsyncProcess' -export type Fn = () => any -export type AsyncFn = () => Promise -export type FnOrAsyncFn = Fn | AsyncFn -export type Jobs = FnOrAsyncFn | Array +export type Fn = () => R +export type AsyncFn = () => Promise +export type FnOrAsyncFn = Fn | AsyncFn +export type Jobs = FnOrAsyncFn | Array> export type ErrorFn = (err: unknown) => any export type AsyncErrorFn = (err: unknown) => Promise @@ -19,11 +19,11 @@ export type PredicateFns = | PredicateFn | Array> -export interface IAsyncProcessFns { - jobs: Map> - onStartFns: Map> - onSuccessFns: Map> - onErrorFns: Map> +export interface IAsyncProcessFns { + jobs: Map>> + onStartFns: Map>> + onSuccessFns: Map>> + onErrorFns: Map> predicateFns: Map>> } From 1c18b048f1f26ba8cc6516102a44181b82e22cad Mon Sep 17 00:00:00 2001 From: Alexis Mineaud Date: Fri, 20 Jan 2023 20:14:54 +0100 Subject: [PATCH 14/20] Allow to type errors thrown by jobs executions. --- src/AsyncProcess/index.ts | 32 +++++++++++++++++------------- src/__tests__/AsyncProcess.test.ts | 10 ++++++---- src/types/index.ts | 12 +++++------ 3 files changed, 30 insertions(+), 24 deletions(-) diff --git a/src/AsyncProcess/index.ts b/src/AsyncProcess/index.ts index 053a616..03faa1f 100644 --- a/src/AsyncProcess/index.ts +++ b/src/AsyncProcess/index.ts @@ -22,7 +22,7 @@ import { * and executes functions according to the successes / errors. */ -export class AsyncProcess { +export class AsyncProcess { public metadata = new MetaData() private _identifiers: AsyncProcessIdentifiers @@ -39,10 +39,10 @@ export class AsyncProcess { } } - private _error: Maybe = null + private _error: Maybe = null private _result: Maybe = null - private _fns: IAsyncProcessFns = { + private _fns: IAsyncProcessFns = { jobs: new Map(), onStartFns: new Map(), onSuccessFns: new Map(), @@ -135,7 +135,7 @@ export class AsyncProcess { /** * Save functions to execute after jobs are succesful. */ - onError(jobs: AsyncErrorFns, identifier = 'defaultOnError'): this { + onError(jobs: AsyncErrorFns, identifier = 'defaultOnError'): this { this._log('functionsRegistrations')('debug')( `Register "${identifier}" onError function(s)` ) @@ -209,10 +209,10 @@ export class AsyncProcess { await this._execJobs(this._fns.onSuccessFns) } } catch (err) { - this._error = err + this._error = err as E this._result = null - this._execAsyncErrorFns(err, this._fns.onErrorFns) + this._execAsyncErrorFns(this._error, this._fns.onErrorFns) } finally { this.shouldDeleteFunctions() } @@ -266,11 +266,11 @@ export class AsyncProcess { * Getters */ - get fns(): IAsyncProcessFns { + get fns(): IAsyncProcessFns { return this._fns } - get error(): Maybe { + get error(): Maybe { return this._error } @@ -307,8 +307,8 @@ export class AsyncProcess { * Execute sequentially async error functions. */ private async _execAsyncErrorFns( - err: unknown, - asyncErrorFns: Map> + err: E, + asyncErrorFns: Map>> ): Promise { for (const [identifier, fns] of asyncErrorFns.entries()) { for (const fn of fns) { @@ -336,10 +336,10 @@ export class AsyncProcess { * Static */ - static instance( + static instance( identifier: TIdentifier, subIdentifiers?: Maybe - ): AsyncProcess { + ): AsyncProcess { const identifiersAsString = AsyncProcess.computeIdentifiers( identifier, subIdentifiers ?? null @@ -351,10 +351,14 @@ export class AsyncProcess { return instance } - const asyncProcess = new AsyncProcess(identifier, subIdentifiers) + const asyncProcess = new AsyncProcess( + identifier, + subIdentifiers + ) + AsyncProcess._instances.set(identifiersAsString, asyncProcess) - return asyncProcess as AsyncProcess + return asyncProcess } /** diff --git a/src/__tests__/AsyncProcess.test.ts b/src/__tests__/AsyncProcess.test.ts index 0d15354..2fde71b 100644 --- a/src/__tests__/AsyncProcess.test.ts +++ b/src/__tests__/AsyncProcess.test.ts @@ -16,10 +16,10 @@ describe('AsyncProcess', () => { } } ])('With options: %o', options => { - function getAsyncProcessTestInstance( + function getAsyncProcessTestInstance( identifier: AsyncProcessTestIdentifier, subIdentifiers?: string[] - ): AsyncProcess { + ): AsyncProcess { return AsyncProcess.instance(identifier, subIdentifiers).setOptions( options ) @@ -192,8 +192,10 @@ describe('AsyncProcess', () => { .mockImplementation(() => Promise.reject(new CustomError('Something bad happened')) ) - const asyncProcess = - getAsyncProcessTestInstance('loadFoo').do(fetchData) + + const asyncProcess = getAsyncProcessTestInstance( + 'loadFoo' + ).do(fetchData) await asyncProcess.start() diff --git a/src/types/index.ts b/src/types/index.ts index b4df06c..fe2dfda 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -6,10 +6,10 @@ export type AsyncFn = () => Promise export type FnOrAsyncFn = Fn | AsyncFn export type Jobs = FnOrAsyncFn | Array> -export type ErrorFn = (err: unknown) => any -export type AsyncErrorFn = (err: unknown) => Promise -export type FnOrAsyncErrorFn = ErrorFn | AsyncErrorFn -export type AsyncErrorFns = FnOrAsyncErrorFn | Array +export type ErrorFn = (err: E) => any +export type AsyncErrorFn = (err: E) => Promise +export type FnOrAsyncErrorFn = ErrorFn | AsyncErrorFn +export type AsyncErrorFns = FnOrAsyncErrorFn | Array> export type PredicateFn = ( asyncProcess: AsyncProcess @@ -19,11 +19,11 @@ export type PredicateFns = | PredicateFn | Array> -export interface IAsyncProcessFns { +export interface IAsyncProcessFns { jobs: Map>> onStartFns: Map>> onSuccessFns: Map>> - onErrorFns: Map> + onErrorFns: Map>> predicateFns: Map>> } From bf85b0327aff414b48c34a4cdfd71f5f470310b8 Mon Sep 17 00:00:00 2001 From: Alexis Mineaud Date: Fri, 20 Jan 2023 20:19:47 +0100 Subject: [PATCH 15/20] Update CHANGELOG. --- CHANGELOG.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b5f8b7..63b33ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,9 +8,15 @@ - :warning: **[breaking-change]** Add an identifier for predicates functions to mimic to same behavior as functions registrations. Several predicate functions can now be passed in a single `if()` with a defined identifier. In the same way, if an another `if()` predicate is set with the same identifier, **it will override the previous one**. -- Add an option `deleteFunctionsWhenJobsStarted` allowing to delete the registered functions when the jobs are started. +- Add `deleteFunctionsWhenJobsStarted` option allowing to delete the registered functions when the jobs are started. -- Add an option `debug.logFunctionRegistrations` and `debug.logFunctionExecutions` to debug functions registrations and executions. Don't forget to set `DEBUG=AsyncProcess:*` as an environment variable or in your browser's localstorage. +- Add `debug.logFunctionRegistrations` and `debug.logFunctionExecutions` options allowing functions registrations and executions logs. Don't forget to set `DEBUG=AsyncProcess:*` as an environment variable or in your browser's localstorage. + +- Keep errors thrown by jobs as it. + + Example: If the job is a fetch query and if the response is a 404, the `err` object will be a Response object. + +- AsyncProcess instances now accepts two more generics, a results (`R`) and an error (`E`), allowing to type results and error. ## v1.2.0 From 9f704bc5d933b7d810f808b1d6f5ceb6fcb607fd Mon Sep 17 00:00:00 2001 From: Alexis Mineaud Date: Fri, 20 Jan 2023 20:20:18 +0100 Subject: [PATCH 16/20] 2.0.0-beta.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 85b60c3..9ff3a00 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@productive-codebases/async-process", - "version": "2.0.0-beta.0", + "version": "2.0.0-beta.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@productive-codebases/async-process", - "version": "2.0.0-beta.0", + "version": "2.0.0-beta.1", "license": "MIT", "devDependencies": { "@types/jest": "^29.2.5", diff --git a/package.json b/package.json index e0d5fad..912b3f8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@productive-codebases/async-process", - "version": "2.0.0-beta.0", + "version": "2.0.0-beta.1", "description": "Declare, configure and start asynchronous processes with ease.", "author": "Alexis MINEAUD", "license": "MIT", From 12e585d7f2542c730809a95b7f0d8aeb4a52413a Mon Sep 17 00:00:00 2001 From: Alexis Mineaud Date: Fri, 20 Jan 2023 20:23:25 +0100 Subject: [PATCH 17/20] Fix CHANGELOG typos. --- CHANGELOG.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63b33ca..78a9cf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,19 +4,19 @@ ### Added -- :warning: **[breaking-change]** Add an identifier for each function registration. When using a same identifier, **the new registered function(s) replace(s) the previous one(s)**. +- :warning: **[breaking-change]** Add an identifier for each function registration. When using the same identifier, **the new registered function(s) replace(s) the previous one(s)**. -- :warning: **[breaking-change]** Add an identifier for predicates functions to mimic to same behavior as functions registrations. Several predicate functions can now be passed in a single `if()` with a defined identifier. In the same way, if an another `if()` predicate is set with the same identifier, **it will override the previous one**. +- :warning: **[breaking-change]** Add an identifier for predicates functions to mimic to the same behavior as functions registrations. Several predicate functions can now be passed in a single `if()` with a defined identifier. In the same way, if another `if()` predicate is set with the same identifier, **it will override the previous one**. -- Add `deleteFunctionsWhenJobsStarted` option allowing to delete the registered functions when the jobs are started. +- Add `deleteFunctionsWhenJobsStarted` option allowing the deletion of registered functions when the jobs are started. -- Add `debug.logFunctionRegistrations` and `debug.logFunctionExecutions` options allowing functions registrations and executions logs. Don't forget to set `DEBUG=AsyncProcess:*` as an environment variable or in your browser's localstorage. +- Add `debug.logFunctionRegistrations` and `debug.logFunctionExecutions` options allowing functions registrations and executions logs. Don't forget to set `DEBUG=AsyncProcess:*` as an environment variable or in your browser's local storage. -- Keep errors thrown by jobs as it. +- Keep errors thrown by jobs as is. Example: If the job is a fetch query and if the response is a 404, the `err` object will be a Response object. -- AsyncProcess instances now accepts two more generics, a results (`R`) and an error (`E`), allowing to type results and error. +- AsyncProcess instances now accept two more generics, results (`R`) and an error (`E`), allowing to enforce typings of jobs results and jobs exceptions. ## v1.2.0 From 5c661dd9950812e6fb5951bbd173987aa9638b7d Mon Sep 17 00:00:00 2001 From: Alexis Mineaud Date: Thu, 2 Feb 2023 23:34:42 +0100 Subject: [PATCH 18/20] Implement dependencies in predicate. --- .../predicates/__tests__/olderThan.test.ts | 66 +++++++++++++++++-- src/AsyncProcess/predicates/olderThan.ts | 39 +++++++++-- 2 files changed, 95 insertions(+), 10 deletions(-) diff --git a/src/AsyncProcess/predicates/__tests__/olderThan.test.ts b/src/AsyncProcess/predicates/__tests__/olderThan.test.ts index 69911c9..e091b74 100644 --- a/src/AsyncProcess/predicates/__tests__/olderThan.test.ts +++ b/src/AsyncProcess/predicates/__tests__/olderThan.test.ts @@ -25,7 +25,7 @@ describe('Predicates', () => { AsyncProcess.clearInstances() }) - it('should avoid starting during a delay', async () => { + it('should not trigger the job during a delay', async () => { const doSomething = jest.fn() const asyncProcess = getAsyncProcessTestInstance('loadFoo') @@ -35,11 +35,11 @@ describe('Predicates', () => { await asyncProcess.start() await asyncProcess.start() - // 1 instead of 2 + // called only once because the second time was during the delay expect(doSomething).toHaveBeenCalledTimes(1) }) - it('should start after the delay', async () => { + it('should trigger the job after the delay', async () => { const doSomething = jest.fn() const asyncProcess = getAsyncProcessTestInstance('loadFoo') @@ -50,11 +50,69 @@ describe('Predicates', () => { await wait(1.5) await asyncProcess.start() + // called twice because the delay has expired + expect(doSomething).toHaveBeenCalledTimes(2) + }) + + it('should not trigger the job if the dependencies have not changed', async () => { + const doSomething = jest.fn() + + const asyncProcess1 = getAsyncProcessTestInstance('loadFoo') + .do(doSomething) + .if(olderThan(1, [1, 'foo', true])) + + await asyncProcess1.start() + + const asyncProcess2 = getAsyncProcessTestInstance('loadFoo') + .do(doSomething) + .if(olderThan(1, [1, 'foo', true])) + + await asyncProcess2.start() + + // called only once because the dependencies have not changed + expect(doSomething).toHaveBeenCalledTimes(1) + }) + + it('should trigger the job if the dependencies have changed', async () => { + const doSomething = jest.fn() + + const asyncProcess1 = getAsyncProcessTestInstance('loadFoo') + .do(doSomething) + .if(olderThan(1, [1, 'foo', true])) + + await asyncProcess1.start() + + const asyncProcess2 = getAsyncProcessTestInstance('loadFoo') + .do(doSomething) + .if(olderThan(1, [1, 'foo', false])) + + await asyncProcess2.start() + + // called twice because the deps have changed + expect(doSomething).toHaveBeenCalledTimes(2) + }) + + it('should trigger the job if the dependencies have not changed but if this is not the same AP instance', async () => { + const doSomething = jest.fn() + + const asyncProcess1 = getAsyncProcessTestInstance('loadFoo') + .do(doSomething) + .if(olderThan(1, [1, 'foo', true])) + + await asyncProcess1.start() + + const asyncProcess2 = getAsyncProcessTestInstance('loadBar') + .do(doSomething) + .if(olderThan(1, [1, 'foo', true])) + + await asyncProcess2.start() + + // called twice because this is 2 different AP instances expect(doSomething).toHaveBeenCalledTimes(2) }) describe('cancelOlderThanDelay', () => { - it('shouldnt cancel the delay if the sub dependencies dont match', async () => { + it('shouldnt cancel the delay if the sub identifiers dont match', async () => { const doSomething = jest.fn() const asyncProcess = getAsyncProcessTestInstance('loadFoo', ['uuid1']) diff --git a/src/AsyncProcess/predicates/olderThan.ts b/src/AsyncProcess/predicates/olderThan.ts index b01d244..b8f66d5 100644 --- a/src/AsyncProcess/predicates/olderThan.ts +++ b/src/AsyncProcess/predicates/olderThan.ts @@ -1,5 +1,6 @@ import { AsyncProcess } from '..' import { Maybe, MetaData } from '@productive-codebases/toolbox' +import { PredicateFn } from '../../types' /** * Return a predicate function to trigger async functions @@ -17,16 +18,35 @@ import { Maybe, MetaData } from '@productive-codebases/toolbox' * * AsyncProcess.instance(...).metadata.get('expireProcess')() */ -interface IOlderDataMetadata { +interface IOlderThanDataMetadata { olderThan: { lastDate: number cancelDelay: () => void + dependencies: OlderThanDependency[] } } -export function olderThan(seconds: number) { - return (asyncProcess: AsyncProcess): boolean => { - const metadata = asyncProcess.metadata as MetaData +type OlderThanDependency = boolean | string | number + +export function olderThan( + seconds: number, + dependencies: OlderThanDependency[] = [] +): PredicateFn { + function hasDependenciesChanged(previousDependancies: OlderThanDependency[]) { + for (let index = 0; index <= dependencies.length; index++) { + if (previousDependancies[index] !== dependencies[index]) { + return true + } + } + + return false + } + + /** + * Predicate function. + */ + return asyncProcess => { + const metadata = asyncProcess.metadata as MetaData const predicateMetadata = metadata.get('olderThan') if (!predicateMetadata) { @@ -36,7 +56,8 @@ export function olderThan(seconds: number) { olderThan: { lastDate: newLastDate, // when calling expireProcess(), delete lastData to set a new lastDate the next time - cancelDelay: () => metadata.delete('olderThan') + cancelDelay: () => metadata.delete('olderThan'), + dependencies } }) @@ -44,6 +65,11 @@ export function olderThan(seconds: number) { return true } + // if dependencies has changed, load data + if (hasDependenciesChanged(predicateMetadata.dependencies)) { + return true + } + const currentDate = new Date().getTime() const delta = currentDate / 1000 - predicateMetadata.lastDate / 1000 const isDataExpired = delta > seconds @@ -57,6 +83,7 @@ export function olderThan(seconds: number) { }) } + // if data is expired, load data return isDataExpired } } @@ -73,7 +100,7 @@ export function cancelOlderThanDelay( subIdentifiers?: Maybe ): void { const predicateMetadata = AsyncProcess.instance(identifier, subIdentifiers) - .metadata as MetaData + .metadata as MetaData const cancelDelayFn = predicateMetadata.get('olderThan')?.cancelDelay From 9a8a058eda944dae2efbfc42501814d4c8a3203b Mon Sep 17 00:00:00 2001 From: Alexis Mineaud Date: Thu, 2 Feb 2023 23:48:56 +0100 Subject: [PATCH 19/20] Remove useless log options used only in tests. --- src/AsyncProcess/index.ts | 6 +---- src/__tests__/AsyncProcess.test.ts | 27 +++++++++---------- .../__snapshots__/AsyncProcess.test.ts.snap | 4 +-- src/types/index.ts | 4 --- 4 files changed, 15 insertions(+), 26 deletions(-) diff --git a/src/AsyncProcess/index.ts b/src/AsyncProcess/index.ts index 03faa1f..39c4d2c 100644 --- a/src/AsyncProcess/index.ts +++ b/src/AsyncProcess/index.ts @@ -32,11 +32,7 @@ export class AsyncProcess { * When set to true, registered functions are deleted after AsyncProcess has been started. * Useful when reusing a same instance of AsyncProcess to not have functions registered several times. */ - deleteFunctionsWhenJobsStarted: false, - debug: { - logFunctionRegistrations: false, - logFunctionExecutions: false - } + deleteFunctionsWhenJobsStarted: false } private _error: Maybe = null diff --git a/src/__tests__/AsyncProcess.test.ts b/src/__tests__/AsyncProcess.test.ts index 2fde71b..1b9edef 100644 --- a/src/__tests__/AsyncProcess.test.ts +++ b/src/__tests__/AsyncProcess.test.ts @@ -6,11 +6,20 @@ import { IAsyncProcessOptions } from '../types' type AsyncProcessTestIdentifier = 'loadFoo' | 'loadBar' describe('AsyncProcess', () => { - describe.each>([ + describe.each< + Partial< + IAsyncProcessOptions & { + _debug: { + logFunctionRegistrations: boolean + logFunctionExecutions: boolean + } + } + > + >([ { deleteFunctionsWhenJobsStarted: false }, { deleteFunctionsWhenJobsStarted: true }, { - debug: { + _debug: { logFunctionRegistrations: true, logFunctionExecutions: true } @@ -591,7 +600,7 @@ describe('AsyncProcess', () => { beforeEach(() => { debug.enable( - options.debug?.logFunctionExecutions ? 'AsyncProcess:*' : false + options._debug?.logFunctionExecutions ? 'AsyncProcess:*' : false ) debug.formatters.s = (s: string) => { @@ -616,12 +625,6 @@ describe('AsyncProcess', () => { const onErrorFn = jest.fn() await getAsyncProcessTestInstance('loadFoo') - .setOptions({ - debug: { - logFunctionRegistrations: true, - logFunctionExecutions: true - } - }) .do(fetchData) .if(predicateFn1) .onStart(onStartFn) @@ -640,12 +643,6 @@ describe('AsyncProcess', () => { const onErrorFn = jest.fn() const asyncProcess = getAsyncProcessTestInstance('loadFoo') - .setOptions({ - debug: { - logFunctionRegistrations: true, - logFunctionExecutions: true - } - }) .do(fetchData) .if(predicateFn1) .onStart(onStartFn) diff --git a/src/__tests__/__snapshots__/AsyncProcess.test.ts.snap b/src/__tests__/__snapshots__/AsyncProcess.test.ts.snap index 4dc458f..930be63 100644 --- a/src/__tests__/__snapshots__/AsyncProcess.test.ts.snap +++ b/src/__tests__/__snapshots__/AsyncProcess.test.ts.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`AsyncProcess With options: { - debug: { logFunctionRegistrations: true, logFunctionExecutions: true } + _debug: { logFunctionRegistrations: true, logFunctionExecutions: true } } Options Logging should log different events if error 1`] = ` [ "AsyncProcess:functionsRegistrations:debug [loadFoo] Register "defaultJobs" jobs function(s)", @@ -15,7 +15,7 @@ exports[`AsyncProcess With options: { `; exports[`AsyncProcess With options: { - debug: { logFunctionRegistrations: true, logFunctionExecutions: true } + _debug: { logFunctionRegistrations: true, logFunctionExecutions: true } } Options Logging should log different events if success 1`] = ` [ "AsyncProcess:functionsRegistrations:debug [loadFoo] Register "defaultJobs" jobs function(s)", diff --git a/src/types/index.ts b/src/types/index.ts index fe2dfda..9d60a7c 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -35,8 +35,4 @@ export type AsyncProcessIdentifiers = [ export interface IAsyncProcessOptions { // delete registered functions when async process is started deleteFunctionsWhenJobsStarted: boolean - debug: { - logFunctionRegistrations: boolean - logFunctionExecutions: boolean - } } From 3925553e2d4d569177b56260df325a7834df2624 Mon Sep 17 00:00:00 2001 From: Alexis Mineaud Date: Fri, 3 Feb 2023 08:56:49 +0100 Subject: [PATCH 20/20] Delete all metadata (deps included) when delay has been cancelled. --- src/AsyncProcess/predicates/olderThan.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/AsyncProcess/predicates/olderThan.ts b/src/AsyncProcess/predicates/olderThan.ts index b8f66d5..2cf39aa 100644 --- a/src/AsyncProcess/predicates/olderThan.ts +++ b/src/AsyncProcess/predicates/olderThan.ts @@ -55,8 +55,7 @@ export function olderThan( metadata.set({ olderThan: { lastDate: newLastDate, - // when calling expireProcess(), delete lastData to set a new lastDate the next time - cancelDelay: () => metadata.delete('olderThan'), + cancelDelay: () => metadata.clear(), dependencies } })