Skip to content

Commit

Permalink
👩‍💻 dx: First draft for defer utility.
Browse files Browse the repository at this point in the history
  • Loading branch information
make-github-pseudonymous-again committed Jul 26, 2024
1 parent 3d14f2e commit 1aa42bc
Show file tree
Hide file tree
Showing 3 changed files with 314 additions and 0 deletions.
2 changes: 2 additions & 0 deletions imports/_test/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,3 +260,5 @@ export const findOneOrThrow = async <T extends Document, U = T>(

export const makeTemplate = (template) => (extra?) =>
create(template, extra, extra !== undefined);

export const isNode = () => Meteor.isServer;
233 changes: 233 additions & 0 deletions imports/lib/async/defer.tests.ts
Original file line number Diff line number Diff line change
@@ -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]);
});
});
79 changes: 79 additions & 0 deletions imports/lib/async/defer.ts
Original file line number Diff line number Diff line change
@@ -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<A extends any[]> = (...args: A) => void;

const _pending = new Set<Deferred>();

export class Deferred {
#timeout: Timeout;
#resolve: Resolve;

Check warning on line 14 in imports/lib/async/defer.ts

View check run for this annotation

Codecov / codecov/patch

imports/lib/async/defer.ts#L14

Added line #L14 was not covered by tests
#reject: Reject;

constructor(timeout: Timeout, resolve: Resolve, reject: Reject) {
this.#timeout = timeout;
this.#resolve = resolve;
this.#reject = reject;
}

Check warning on line 21 in imports/lib/async/defer.ts

View check run for this annotation

Codecov / codecov/patch

imports/lib/async/defer.ts#L21

Added line #L21 was not covered by tests

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 = <A extends any[]>(
callback: Callback<A>,
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<Deferred>) => {
for (const deferred of pending) deferred.cancel();
};

export const cancelAll = () => {
_cancelAll(_pending);
_pending.clear();
};

const _flushAll = (pending: Iterable<Deferred>) => {
for (const deferred of pending) deferred.flush();
};

export const flushAll = () => {
_flushAll(_pending);
_pending.clear();
};

0 comments on commit 1aa42bc

Please sign in to comment.