From 1aa42bc8613107c997526f0fd8dcd73c3d8bfce7 Mon Sep 17 00:00:00 2001 From: make-github-pseudonymous-again <5165674+make-github-pseudonymous-again@users.noreply.github.com> Date: Fri, 26 Jul 2024 01:14:17 +0200 Subject: [PATCH] :woman_technologist: dx: First draft for `defer` utility. --- imports/_test/fixtures.ts | 2 + imports/lib/async/defer.tests.ts | 233 +++++++++++++++++++++++++++++++ imports/lib/async/defer.ts | 79 +++++++++++ 3 files changed, 314 insertions(+) create mode 100644 imports/lib/async/defer.tests.ts create mode 100644 imports/lib/async/defer.ts diff --git a/imports/_test/fixtures.ts b/imports/_test/fixtures.ts index 761de719d..b1ab77a1b 100644 --- a/imports/_test/fixtures.ts +++ b/imports/_test/fixtures.ts @@ -260,3 +260,5 @@ export const findOneOrThrow = async ( export const makeTemplate = (template) => (extra?) => create(template, extra, extra !== undefined); + +export const isNode = () => Meteor.isServer; diff --git a/imports/lib/async/defer.tests.ts b/imports/lib/async/defer.tests.ts new file mode 100644 index 000000000..03642b6d7 --- /dev/null +++ b/imports/lib/async/defer.tests.ts @@ -0,0 +1,233 @@ +import {assert} from 'chai'; + +import {isNode, isomorphic} from '../../_test/fixtures'; + +import {cancelAll, defer, flushAll} from './defer'; +import sleep from './sleep'; + +isomorphic(__filename, () => { + it('should queue to macrotask queue', async () => { + const x: number[] = []; + + defer(() => x.push(1)); + + assert.deepEqual(x, []); + + await Promise.resolve().then(() => { + assert.deepEqual(x, []); + }); + + await sleep(0); + + assert.deepEqual(x, [1]); + }); + + it('should allow cancellation', async () => { + const x: number[] = []; + + const deferred = defer(() => x.push(1)); + + assert.deepEqual(x, []); + + await Promise.resolve().then(() => { + assert.deepEqual(x, []); + }); + + deferred.cancel(); + + await sleep(0); + + assert.deepEqual(x, []); + }); + + it('should allow flushing before microtask queue', async () => { + const x: number[] = []; + + const deferred = defer(() => x.push(1)); + + assert.deepEqual(x, []); + + deferred.flush(); + + await Promise.resolve().then(() => { + assert.deepEqual(x, [1]); + }); + + await sleep(0); + + assert.deepEqual(x, [1]); + }); + + it('should flush after main loop', async () => { + const x: number[] = []; + + const deferred = defer(() => x.push(1)); + + deferred.flush(); + + assert.deepEqual(x, []); + + await Promise.resolve().then(() => { + assert.deepEqual(x, [1]); + }); + + await sleep(0); + + assert.deepEqual(x, [1]); + }); + + it('should catch errors', async () => { + const x: number[] = []; + + defer(() => { + x.push(1); + throw new Error('test'); + }); + + await sleep(0); + + assert.deepEqual(x, [1]); + }); + + it('should catch errors when flushing', async () => { + const x: number[] = []; + + const deferred = defer(() => { + x.push(1); + throw new Error('test'); + }); + + deferred.flush(); + + await sleep(0); + + assert.deepEqual(x, [1]); + }); + + it('should allow cancellation of all deferred computations', async () => { + const x: number[] = []; + + defer(() => x.push(1)); + defer(() => x.push(2)); + + assert.deepEqual(x, []); + + await Promise.resolve().then(() => { + assert.deepEqual(x, []); + }); + + cancelAll(); + + await sleep(0); + + assert.deepEqual(x, []); + }); + + it('should allow flushing all deferred computations before microtask queue', async () => { + const x: number[] = []; + + defer(() => x.push(1)); + defer(() => x.push(2)); + + assert.deepEqual(x, []); + + flushAll(); + + await Promise.resolve().then(() => { + assert.deepEqual(x, [1, 2]); + }); + + await sleep(0); + + assert.deepEqual(x, [1, 2]); + }); + + it('should flush all after main loop', async () => { + const x: number[] = []; + + defer(() => x.push(1)); + defer(() => x.push(2)); + + flushAll(); + + assert.deepEqual(x, []); + + await Promise.resolve().then(() => { + assert.deepEqual(x, [1, 2]); + }); + + await sleep(0); + + assert.deepEqual(x, [1, 2]); + }); + + it('should execute in order', async () => { + const x: number[] = []; + + defer(() => { + x.push(1); + }); + defer(() => { + x.push(2); + }); + + assert.deepEqual(x, []); + + await Promise.resolve().then(() => { + assert.deepEqual(x, []); + }); + + await sleep(0); + + assert.deepEqual(x, [1, 2]); + }); + + it('should respect timeout', async () => { + const x: number[] = []; + + const delay = isNode() ? 5 : 1; + + defer(() => { + x.push(1); + }, delay); + defer(() => { + x.push(2); + }); + + assert.deepEqual(x, []); + + await Promise.all([ + sleep(delay).then(() => { + assert.deepEqual(x, [2, 1]); + }), + sleep(0).then(() => { + assert.deepEqual(x, [2]); + }), + Promise.resolve().then(() => { + assert.deepEqual(x, []); + }), + ]); + }); + + it('should allow passing arguments', async () => { + const x: number[] = []; + defer( + (a, b) => { + x.push(a, b); + }, + 0, + 1, + 2, + ); + + assert.deepEqual(x, []); + + await Promise.resolve().then(() => { + assert.deepEqual(x, []); + }); + + await sleep(0); + + assert.deepEqual(x, [1, 2]); + }); +}); diff --git a/imports/lib/async/defer.ts b/imports/lib/async/defer.ts new file mode 100644 index 000000000..c775012ab --- /dev/null +++ b/imports/lib/async/defer.ts @@ -0,0 +1,79 @@ +import type Timeout from '../types/Timeout'; + +import createPromise from './createPromise'; + +type Resolve = (value?: any) => void; +type Reject = (reason?: any) => void; + +type Callback = (...args: A) => void; + +const _pending = new Set(); + +export class Deferred { + #timeout: Timeout; + #resolve: Resolve; + #reject: Reject; + + constructor(timeout: Timeout, resolve: Resolve, reject: Reject) { + this.#timeout = timeout; + this.#resolve = resolve; + this.#reject = reject; + } + + cancel() { + if (!_pending.has(this)) return; + _pending.delete(this); + clearTimeout(this.#timeout); + this.#reject(); + } + + flush() { + if (!_pending.has(this)) return; + _pending.delete(this); + clearTimeout(this.#timeout); + this.#resolve(); + } +} + +export const defer = ( + callback: Callback, + timeout?: number, + ...args: A +): Deferred => { + const {promise, resolve, reject} = createPromise(); + promise + .then( + () => { + _pending.delete(deferred); + callback(...args); + }, + + () => { + // NOTE This handles cancellation. + }, + ) + .catch((error: unknown) => { + console.error({error}); + }); + const deferred = new Deferred(setTimeout(resolve, timeout), resolve, reject); + _pending.add(deferred); + return deferred; +}; + +const _cancelAll = (pending: Iterable) => { + for (const deferred of pending) deferred.cancel(); +}; + +export const cancelAll = () => { + _cancelAll(_pending); + _pending.clear(); +}; + +const _flushAll = (pending: Iterable) => { + for (const deferred of pending) deferred.flush(); +}; + +export const flushAll = () => { + _flushAll(_pending); + _pending.clear(); +};