From 1b9f7ccc788aa81852633b9f9f5308596e13a44e Mon Sep 17 00:00:00 2001 From: Ethan Ferrari Date: Wed, 22 Jun 2022 15:33:33 -0500 Subject: [PATCH 1/2] update jaasync and add timeout option to scan --- package.json | 4 +- src/scanner.ts | 2 +- src/strongbus.ts | 23 +- src/strongbus_spec.ts | 487 +++++++++++++++++++++++++----------------- yarn.lock | 10 +- 5 files changed, 320 insertions(+), 206 deletions(-) diff --git a/package.json b/package.json index f3cfe6c..c76fe08 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "license": "MIT", "dependencies": { "core-decorators": "^0.20.0", - "jaasync": "^0.10.1" + "jaasync": "^0.11.0" }, "devDependencies": { "@types/jasmine": "^3.3.0", @@ -45,7 +45,7 @@ "jasmine": "^3.3.0", "jasmine-spec-reporter": "^7.0.0", "tslint": "^6.1.3", - "typedoc": "^0.23.0", + "typedoc": "0.23.0-beta.5", "typescript": "^4.7.4" } } diff --git a/src/scanner.ts b/src/scanner.ts index 1709add..7234408 100644 --- a/src/scanner.ts +++ b/src/scanner.ts @@ -107,7 +107,7 @@ export class Scanner implements CancelablePromise { return this._promise.finally(onfinally); } - public cancel(reason?: string): boolean { + public cancel(reason?: string|Error): boolean { if(this.settle()) { this._promise.reject(reason); return true; diff --git a/src/strongbus.ts b/src/strongbus.ts index e5d68b1..56f6f4a 100644 --- a/src/strongbus.ts +++ b/src/strongbus.ts @@ -1,6 +1,6 @@ import {autobind} from 'core-decorators'; -import {CancelablePromise, cancelable} from 'jaasync'; +import {CancelablePromise, cancelable, timeout} from 'jaasync'; import {Scanner} from './scanner'; import {StrongbusLogger} from './strongbusLogger'; @@ -267,6 +267,8 @@ export class Bus implements * @param params.evaluator - an evaluation function that should check for a certain state * and may resolve or reject the scan based on the state. * @param params.trigger - event or events that should trigger evaluator + * @param {boolean} [params.pool=true] - attempt to pool scanners that can be resolved by the same evaluator and trigger; default is `true` + * @param {integer} [params.timeout] - cancel the scan after `params.timeout` milliseconds. Values `<= 0` are ignored. If configured, will disable pooling regardles of `params.pool`'s value * @param {boolean} [params.eager=true] - should `params.evaluator` be called immediately; default is `true`. * This eliminates the following anti-pattern: * ``` @@ -274,7 +276,6 @@ export class Bus implements * await this.scan({evaluator: evaluateSomeCondition, trigger: ...}); * } * ``` - * @param {boolean} [params.pool=true] - attempt to pool scanners that can be resolved by the same evaluator and trigger; default is `true` */ public scan>( params: { @@ -282,17 +283,31 @@ export class Bus implements trigger: Events.Listenable>; eager?: boolean; pool?: boolean; + timeout?: number; }): CancelablePromise ? U : any> { type TReturnType = TEvaluator extends Scanner.Evaluator ? U : any; - if(params.pool === false) { + if(params.timeout && params.timeout > 0) { + const scanner = new Scanner(params); + scanner.scan(this, params.trigger); + // tslint:disable-next-line:prefer-object-spread + return Object.assign( + timeout(scanner, {ms: params.timeout, cancelUnderlyingPromiseOnTimeout: true}), + { + [INTERNAL_PROMISE]: scanner, + cancel: (err: any) => scanner.cancel(err) + } + ); + } else if(params.pool === false) { const scanner = new Scanner(params); scanner.scan(this, params.trigger); + // tslint:disable-next-line:prefer-object-spread return Object.assign( - cancelable(() => scanner), + scanner, {[INTERNAL_PROMISE]: scanner} ); + } /* diff --git a/src/strongbus_spec.ts b/src/strongbus_spec.ts index 259ee06..ecd42e6 100644 --- a/src/strongbus_spec.ts +++ b/src/strongbus_spec.ts @@ -1,5 +1,5 @@ -import {parallel, sleep} from 'jaasync'; +import {parallel, sleep, TimeoutExpiredError} from 'jaasync'; import * as Strongbus from './'; import {Scanner} from './scanner'; @@ -1506,158 +1506,98 @@ describe('Strongbus.Bus', () => { expect(onResolve).toHaveBeenCalledWith(true); }); + }); - describe('and params.eager=false', () => { - it('does not resolve the promise until it receives an event triggering evaluation', async () => { - const hasFoo: boolean = true; - const evaluator = (resolve: Scanner.Resolver, reject: Scanner.Rejecter) => { - if(hasFoo) { - resolve(true); - } - }; - const p = bus.scan({ - evaluator, - trigger: 'foo', - eager: false - }); + describe('and params.eager=false', () => { + it('does not resolve the promise until it receives an event triggering evaluation', async () => { + const hasFoo: boolean = true; + const evaluator = (resolve: Scanner.Resolver, reject: Scanner.Rejecter) => { + if(hasFoo) { + resolve(true); + } + }; + const p = bus.scan({ + evaluator, + trigger: 'foo', + eager: false + }); - p.then(onResolve); - await sleep(1); + p.then(onResolve); + await sleep(1); - expect(onResolve).not.toHaveBeenCalled(); + expect(onResolve).not.toHaveBeenCalled(); - bus.emit('foo', 'FOO!'); - await sleep(1); + bus.emit('foo', 'FOO!'); + await sleep(1); - expect(onResolve).toHaveBeenCalledWith(true); - }); + expect(onResolve).toHaveBeenCalledWith(true); }); + }); - describe('Scanner pooling', () => { - describe('given multiple scan invocations in which params.evaluator and params.eager are the same', () => { - describe('given params.trigger is the same single event', () => { - it('returns the same promise object', () => { - const evaluator = (resolve: Scanner.Resolver, reject: Scanner.Rejecter) => { - // doing nothing in the evaluator - return; - }; - - const p1 = bus.scan({ - evaluator, - trigger: 'foo' - }); - const p2 = bus.scan({ - evaluator, - trigger: 'foo' - }); - - expect((p1 as any)[INTERNAL_PROMISE] === (p2 as any)[INTERNAL_PROMISE]).toBeTrue(); - }); - }); + describe('Scanner pooling', () => { + describe('given multiple scan invocations in which params.evaluator and params.eager are the same', () => { + describe('given params.trigger is the same single event', () => { + it('returns the same promise object', () => { + const evaluator = (resolve: Scanner.Resolver, reject: Scanner.Rejecter) => { + // doing nothing in the evaluator + return; + }; - describe('given params.trigger is the same vector of events in the same order', () => { - it('returns the same promise object', () => { - const evaluator = (resolve: Scanner.Resolver, reject: Scanner.Rejecter) => { - // doing nothing in the evaluator - return; - }; - - const p1 = bus.scan({ - evaluator, - trigger: ['foo', 'bar'] - }); - const p2 = bus.scan({ - evaluator, - trigger: ['foo', 'bar'] - }); - - expect((p1 as any)[INTERNAL_PROMISE] === (p2 as any)[INTERNAL_PROMISE]).toBeTrue(); + const p1 = bus.scan({ + evaluator, + trigger: 'foo' }); - }); - - describe('given params.trigger is the same vector of events in a different order', () => { - it('returns the same promise object', () => { - const evaluator = (resolve: Scanner.Resolver, reject: Scanner.Rejecter) => { - // doing nothing in the evaluator - return; - }; - - const p1 = bus.scan({ - evaluator, - trigger: ['foo', 'bar'] - }); - const p2 = bus.scan({ - evaluator, - trigger: ['bar', 'foo'] - }); - - expect((p1 as any)[INTERNAL_PROMISE] === (p2 as any)[INTERNAL_PROMISE]).toBeTrue(); + const p2 = bus.scan({ + evaluator, + trigger: 'foo' }); + + expect((p1 as any)[INTERNAL_PROMISE] === (p2 as any)[INTERNAL_PROMISE]).toBeTrue(); }); + }); - describe('given params.trigger is a subset of an existing vector of events', () => { - it('returns the same promise object', () => { - const evaluator = (resolve: Scanner.Resolver, reject: Scanner.Rejecter) => { - // doing nothing in the evaluator - return; - }; - - const p1 = bus.scan({ - evaluator, - trigger: ['foo', 'bar'] - }); - const p2 = bus.scan({ - evaluator, - trigger: ['bar'] - }); - const p3 = bus.scan({ - evaluator, - trigger: 'foo' - }); - - expect((p1 as any)[INTERNAL_PROMISE] === (p2 as any)[INTERNAL_PROMISE]).toBeTrue(); - expect((p2 as any)[INTERNAL_PROMISE] === (p3 as any)[INTERNAL_PROMISE]).toBeTrue(); + describe('given params.trigger is the same vector of events in the same order', () => { + it('returns the same promise object', () => { + const evaluator = (resolve: Scanner.Resolver, reject: Scanner.Rejecter) => { + // doing nothing in the evaluator + return; + }; + + const p1 = bus.scan({ + evaluator, + trigger: ['foo', 'bar'] }); + const p2 = bus.scan({ + evaluator, + trigger: ['foo', 'bar'] + }); + + expect((p1 as any)[INTERNAL_PROMISE] === (p2 as any)[INTERNAL_PROMISE]).toBeTrue(); }); + }); + + describe('given params.trigger is the same vector of events in a different order', () => { + it('returns the same promise object', () => { + const evaluator = (resolve: Scanner.Resolver, reject: Scanner.Rejecter) => { + // doing nothing in the evaluator + return; + }; - describe('given params.trigger is a subset of an existing wildcard scan', () => { - it('returns the same promise object', () => { - const evaluator = (resolve: Scanner.Resolver, reject: Scanner.Rejecter) => { - // doing nothing in the evaluator - return; - }; - - const p1 = bus.scan({ - evaluator, - trigger: '*' - }); - const p2 = bus.scan({ - evaluator, - trigger: ['bar'] - }); - const p3 = bus.scan({ - evaluator, - trigger: 'foo' - }); - const p4 = bus.scan({ - evaluator, - trigger: ['foo', 'bar'] - }); - const p5 = bus.scan({ - evaluator, - trigger: '*' - }); - - expect((p1 as any)[INTERNAL_PROMISE] === (p2 as any)[INTERNAL_PROMISE]).toBeTrue(); - expect((p2 as any)[INTERNAL_PROMISE] === (p3 as any)[INTERNAL_PROMISE]).toBeTrue(); - expect((p3 as any)[INTERNAL_PROMISE] === (p4 as any)[INTERNAL_PROMISE]).toBeTrue(); - expect((p4 as any)[INTERNAL_PROMISE] === (p5 as any)[INTERNAL_PROMISE]).toBeTrue(); + const p1 = bus.scan({ + evaluator, + trigger: ['foo', 'bar'] }); + const p2 = bus.scan({ + evaluator, + trigger: ['bar', 'foo'] + }); + + expect((p1 as any)[INTERNAL_PROMISE] === (p2 as any)[INTERNAL_PROMISE]).toBeTrue(); }); }); - describe('given params.trigger is NOT subset of an existing trigger', () => { - it('returns different promise objects', () => { + describe('given params.trigger is a subset of an existing vector of events', () => { + it('returns the same promise object', () => { const evaluator = (resolve: Scanner.Resolver, reject: Scanner.Rejecter) => { // doing nothing in the evaluator return; @@ -1669,74 +1609,106 @@ describe('Strongbus.Bus', () => { }); const p2 = bus.scan({ evaluator, - trigger: ['bar', 'baz'] + trigger: ['bar'] }); const p3 = bus.scan({ evaluator, - trigger: '*' + trigger: 'foo' }); - expect((p1 as any)[INTERNAL_PROMISE] === (p2 as any)[INTERNAL_PROMISE]).toBeFalse(); - expect((p1 as any)[INTERNAL_PROMISE] === (p3 as any)[INTERNAL_PROMISE]).toBeFalse(); - expect((p2 as any)[INTERNAL_PROMISE] === (p3 as any)[INTERNAL_PROMISE]).toBeFalse(); + expect((p1 as any)[INTERNAL_PROMISE] === (p2 as any)[INTERNAL_PROMISE]).toBeTrue(); + expect((p2 as any)[INTERNAL_PROMISE] === (p3 as any)[INTERNAL_PROMISE]).toBeTrue(); }); }); - describe('given params.trigger is the wildcard', () => { - describe('and a pooled scanner exists for the wildcard already', () => { - it('returns the same promise object', () => { - const evaluator = (resolve: Scanner.Resolver, reject: Scanner.Rejecter) => { - // doing nothing in the evaluator - return; - }; - - const p1 = bus.scan({ - evaluator, - trigger: '*' - }); - - const p2 = bus.scan({ - evaluator, - trigger: '*' - }); - - expect((p1 as any)[INTERNAL_PROMISE] === (p2 as any)[INTERNAL_PROMISE]).toBeTrue(); + describe('given params.trigger is a subset of an existing wildcard scan', () => { + it('returns the same promise object', () => { + const evaluator = (resolve: Scanner.Resolver, reject: Scanner.Rejecter) => { + // doing nothing in the evaluator + return; + }; + + const p1 = bus.scan({ + evaluator, + trigger: '*' }); + const p2 = bus.scan({ + evaluator, + trigger: ['bar'] + }); + const p3 = bus.scan({ + evaluator, + trigger: 'foo' + }); + const p4 = bus.scan({ + evaluator, + trigger: ['foo', 'bar'] + }); + const p5 = bus.scan({ + evaluator, + trigger: '*' + }); + + expect((p1 as any)[INTERNAL_PROMISE] === (p2 as any)[INTERNAL_PROMISE]).toBeTrue(); + expect((p2 as any)[INTERNAL_PROMISE] === (p3 as any)[INTERNAL_PROMISE]).toBeTrue(); + expect((p3 as any)[INTERNAL_PROMISE] === (p4 as any)[INTERNAL_PROMISE]).toBeTrue(); + expect((p4 as any)[INTERNAL_PROMISE] === (p5 as any)[INTERNAL_PROMISE]).toBeTrue(); }); }); + }); - describe('given a pooled scanner is settled', () => { - describe('and another scan is invoked with the same parameters that created the pooled scanner', () => { - it('a new pooled scanner is created', async () => { - let criteria: boolean = false; - const evaluator = (resolve: Scanner.Resolver) => { - if(criteria) { - resolve(criteria); - } - }; + describe('given params.trigger is NOT subset of an existing trigger', () => { + it('returns different promise objects', () => { + const evaluator = (resolve: Scanner.Resolver, reject: Scanner.Rejecter) => { + // doing nothing in the evaluator + return; + }; - const p1 = bus.scan({ - evaluator, - trigger: 'foo' - }); + const p1 = bus.scan({ + evaluator, + trigger: ['foo', 'bar'] + }); + const p2 = bus.scan({ + evaluator, + trigger: ['bar', 'baz'] + }); + const p3 = bus.scan({ + evaluator, + trigger: '*' + }); - criteria = true; - bus.emit('foo', null); + expect((p1 as any)[INTERNAL_PROMISE] === (p2 as any)[INTERNAL_PROMISE]).toBeFalse(); + expect((p1 as any)[INTERNAL_PROMISE] === (p3 as any)[INTERNAL_PROMISE]).toBeFalse(); + expect((p2 as any)[INTERNAL_PROMISE] === (p3 as any)[INTERNAL_PROMISE]).toBeFalse(); + }); + }); - await p1; + describe('given params.trigger is the wildcard', () => { + describe('and a pooled scanner exists for the wildcard already', () => { + it('returns the same promise object', () => { + const evaluator = (resolve: Scanner.Resolver, reject: Scanner.Rejecter) => { + // doing nothing in the evaluator + return; + }; - const p2 = bus.scan({ - evaluator, - trigger: 'foo' - }); + const p1 = bus.scan({ + evaluator, + trigger: '*' + }); - expect((p1 as any)[INTERNAL_PROMISE] === (p2 as any)[INTERNAL_PROMISE]).toBeFalse(); + const p2 = bus.scan({ + evaluator, + trigger: '*' }); + + expect((p1 as any)[INTERNAL_PROMISE] === (p2 as any)[INTERNAL_PROMISE]).toBeTrue(); }); }); + }); - describe('given an scan invocation is canceled', () => { - it('only rejects the canceled invocation, not all scans in the pool', async () => { + describe('given a pooled scanner is settled', () => { + describe('and another scan is invoked with the same parameters that created the pooled scanner', () => { + it('a new pooled scanner is created', async () => { let criteria: boolean = false; const evaluator = (resolve: Scanner.Resolver) => { if(criteria) { @@ -1744,31 +1716,59 @@ describe('Strongbus.Bus', () => { } }; - const resolveSpy = jasmine.createSpy('resolve'); - const rejectSpy = jasmine.createSpy('reject'); - const p1 = bus.scan({ evaluator, - trigger: '*' + trigger: 'foo' }); - p1.then(resolveSpy); + + criteria = true; + bus.emit('foo', null); + + await p1; const p2 = bus.scan({ evaluator, - trigger: '*' + trigger: 'foo' }); - p2.catch(rejectSpy); - p2.cancel('test'); - criteria = true; - bus.emit('foo', null); + expect((p1 as any)[INTERNAL_PROMISE] === (p2 as any)[INTERNAL_PROMISE]).toBeFalse(); + }); + }); + }); - await parallel([p1, p2]); + describe('given an scan invocation is canceled', () => { + it('only rejects the canceled invocation, not all scans in the pool', async () => { + let criteria: boolean = false; + const evaluator = (resolve: Scanner.Resolver) => { + if(criteria) { + resolve(criteria); + } + }; - expect(resolveSpy).toHaveBeenCalledWith(true); - expect(rejectSpy).toHaveBeenCalledWith('test'); + const resolveSpy = jasmine.createSpy('resolve'); + const rejectSpy = jasmine.createSpy('reject'); + + const p1 = bus.scan({ + evaluator, + trigger: '*' + }); + p1.then(resolveSpy); + const p2 = bus.scan({ + evaluator, + trigger: '*' }); + p2.catch(rejectSpy); + + p2.cancel('test'); + criteria = true; + bus.emit('foo', null); + + await parallel([p1, p2]); + + expect(resolveSpy).toHaveBeenCalledWith(true); + expect(rejectSpy).toHaveBeenCalledWith('test'); + }); }); @@ -1794,5 +1794,104 @@ describe('Strongbus.Bus', () => { }); }); }); + + fdescribe('given params.timeout is configured', () => { + describe('and its value is 0', () => { + describe('given pooling is configured', () => { + it('pooling is used (timeout is ignored)', () => { + const evaluator = (resolve: Scanner.Resolver, reject: Scanner.Rejecter) => { + // doing nothing in the evaluator + return; + }; + + const p1 = bus.scan({ + evaluator, + trigger: 'foo', + timeout: 0 + }); + const p2 = bus.scan({ + evaluator, + trigger: 'foo' + }); + + expect((p1 as any)[INTERNAL_PROMISE] === (p2 as any)[INTERNAL_PROMISE]).toBeTrue(); + }); + }); + }); + + describe('and its value is < 0', () => { + describe('given pooling is configured', () => { + it('pooling is used (timeout is ignored)', () => { + const evaluator = (resolve: Scanner.Resolver, reject: Scanner.Rejecter) => { + // doing nothing in the evaluator + return; + }; + + const p1 = bus.scan({ + evaluator, + trigger: 'foo', + timeout: -1 + }); + const p2 = bus.scan({ + evaluator, + trigger: 'foo' + }); + + expect((p1 as any)[INTERNAL_PROMISE] === (p2 as any)[INTERNAL_PROMISE]).toBeTrue(); + }); + }); + }); + + describe('and its value is > 0', () => { + it('the scan is canceled after `params.timeout` ms', (done) => { + const evaluator = (resolve: Scanner.Resolver, reject: Scanner.Rejecter) => { + // doing nothing in the evaluator + return; + }; + + const p1 = bus.scan({ + evaluator, + trigger: 'foo', + timeout: 100 + }); + expect(bus.hasListenersFor('foo')).toBeTrue(); + + p1.then(() => done.fail()) + .catch((e) => { + expect(e).toBeInstanceOf(TimeoutExpiredError); + expect(bus.hasListenersFor('foo')).toBeFalse(); + done(); + }); + }); + + describe('given pooling is configured', () => { + it('it does not pool the scanners', () => { + const evaluator = (resolve: Scanner.Resolver, reject: Scanner.Rejecter) => { + // doing nothing in the evaluator + return; + }; + + const p1 = bus.scan({ + evaluator, + trigger: 'foo', + timeout: 100 + }); + const p2 = bus.scan({ + evaluator, + trigger: 'foo', + timeout: 100 + }); + + expect((p1 as any)[INTERNAL_PROMISE] === (p2 as any)[INTERNAL_PROMISE]).toBeFalse(); + + p1.catch(e => null); + p2.catch(e => null); + + p1.cancel(); + p2.cancel(); + }); + }); + }); + }); }); }); \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 2b3b4d1..ff65172 100644 --- a/yarn.lock +++ b/yarn.lock @@ -182,10 +182,10 @@ is-core-module@^2.8.1: dependencies: has "^1.0.3" -jaasync@^0.10.1: - version "0.10.1" - resolved "https://registry.yarnpkg.com/jaasync/-/jaasync-0.10.1.tgz#7a516a72625c41a34d1a397a97c4e25152c4f377" - integrity sha512-4TKOvo8iZ+i7yNzUPhYolhpEZW+ZtQdba12+YIj5HAPDyGro6HeLbNZnHlUoyv7m/PUPJsJuR+bjl7wrqDaWyg== +jaasync@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/jaasync/-/jaasync-0.11.0.tgz#d1d1a287d573d114f191d1d52a2bafb27133905b" + integrity sha512-J1zp19w0hwpFA8hCU7hD/8xYadCxs2/63549W/bndBW/UzTvLtk8l7RZvzLELt8/731DPFGyqz9zRJXA0QvV1A== dependencies: core-decorators "^0.20.0" @@ -351,7 +351,7 @@ tsutils@^2.29.0: dependencies: tslib "^1.8.1" -typedoc@^0.23.0: +typedoc@0.23.0-beta.5: version "0.23.0-beta.5" resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.23.0-beta.5.tgz#dc7d716063c19212b48ba117817c3047f2106535" integrity sha512-6z6dJMIBbErjtLKJ4iTx9BN9/BwScARZq3YH4bmDOT0IN200FbT2A7JSGujw7rP6aMLlUm605VpZNIB4GYbDBg== From df74fdcc1e7344a9af37bb66c234eb2541246a14 Mon Sep 17 00:00:00 2001 From: Ethan Ferrari Date: Wed, 22 Jun 2022 15:49:10 -0500 Subject: [PATCH 2/2] remove fdescribe --- src/strongbus_spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/strongbus_spec.ts b/src/strongbus_spec.ts index ecd42e6..06f63bb 100644 --- a/src/strongbus_spec.ts +++ b/src/strongbus_spec.ts @@ -1795,7 +1795,7 @@ describe('Strongbus.Bus', () => { }); }); - fdescribe('given params.timeout is configured', () => { + describe('given params.timeout is configured', () => { describe('and its value is 0', () => { describe('given pooling is configured', () => { it('pooling is used (timeout is ignored)', () => {