-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
👩💻 dx: First draft for
defer
utility.
- Loading branch information
1 parent
3d14f2e
commit 18763d1
Showing
2 changed files
with
314 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,235 @@ | ||
import {assert} from 'chai'; | ||
|
||
import {client} from '../../_test/fixtures'; | ||
|
||
import {cancelAll, defer, flushAll} from './defer'; | ||
import sleep from './sleep'; | ||
|
||
client(__filename, () => { | ||
it('should queue to macrotask queue', async () => { | ||
let i = 0; | ||
|
||
defer(() => ++i); | ||
|
||
assert.strictEqual(i, 0); | ||
|
||
await Promise.resolve().then(() => { | ||
assert.strictEqual(i, 0); | ||
}); | ||
|
||
await sleep(0); | ||
|
||
assert.strictEqual(i, 1); | ||
}); | ||
|
||
it('should allow cancellation', async () => { | ||
let i = 0; | ||
|
||
const deferred = defer(() => ++i); | ||
|
||
assert.strictEqual(i, 0); | ||
|
||
await Promise.resolve().then(() => { | ||
assert.strictEqual(i, 0); | ||
}); | ||
|
||
deferred.cancel(); | ||
|
||
await sleep(0); | ||
|
||
assert.strictEqual(i, 0); | ||
}); | ||
|
||
it('should allow flushing before microtask queue', async () => { | ||
let i = 0; | ||
|
||
const deferred = defer(() => ++i); | ||
|
||
assert.strictEqual(i, 0); | ||
|
||
deferred.flush(); | ||
|
||
await Promise.resolve().then(() => { | ||
assert.strictEqual(i, 1); | ||
}); | ||
|
||
await sleep(0); | ||
|
||
assert.strictEqual(i, 1); | ||
}); | ||
|
||
it('should flush after main loop', async () => { | ||
let i = 0; | ||
|
||
const deferred = defer(() => ++i); | ||
|
||
deferred.flush(); | ||
|
||
assert.strictEqual(i, 0); | ||
|
||
await Promise.resolve().then(() => { | ||
assert.strictEqual(i, 1); | ||
}); | ||
|
||
await sleep(0); | ||
|
||
assert.strictEqual(i, 1); | ||
}); | ||
|
||
it('should catch errors', async () => { | ||
let i = 0; | ||
|
||
defer(() => { | ||
++i; | ||
throw new Error('test'); | ||
}); | ||
|
||
await sleep(0); | ||
|
||
assert.strictEqual(i, 1); | ||
}); | ||
|
||
it('should catch errors when flushing', async () => { | ||
let i = 0; | ||
|
||
const deferred = defer(() => { | ||
++i; | ||
throw new Error('test'); | ||
}); | ||
|
||
deferred.flush(); | ||
|
||
await sleep(0); | ||
|
||
assert.strictEqual(i, 1); | ||
}); | ||
|
||
it('should allow cancellation of all deferred computations', async () => { | ||
let i = 0; | ||
|
||
defer(() => ++i); | ||
defer(() => ++i); | ||
|
||
assert.strictEqual(i, 0); | ||
|
||
await Promise.resolve().then(() => { | ||
assert.strictEqual(i, 0); | ||
}); | ||
|
||
cancelAll(); | ||
|
||
await sleep(0); | ||
|
||
assert.strictEqual(i, 0); | ||
}); | ||
|
||
it('should allow flushing all deferred computations before microtask queue', async () => { | ||
let i = 0; | ||
|
||
defer(() => ++i); | ||
defer(() => ++i); | ||
|
||
assert.strictEqual(i, 0); | ||
|
||
flushAll(); | ||
|
||
await Promise.resolve().then(() => { | ||
assert.strictEqual(i, 2); | ||
}); | ||
|
||
await sleep(0); | ||
|
||
assert.strictEqual(i, 2); | ||
}); | ||
|
||
it('should flush all after main loop', async () => { | ||
let i = 0; | ||
|
||
defer(() => ++i); | ||
defer(() => ++i); | ||
|
||
flushAll(); | ||
|
||
assert.strictEqual(i, 0); | ||
|
||
await Promise.resolve().then(() => { | ||
assert.strictEqual(i, 2); | ||
}); | ||
|
||
await sleep(0); | ||
|
||
assert.strictEqual(i, 2); | ||
}); | ||
|
||
it('should execute in order', async () => { | ||
let x = 'a'; | ||
|
||
defer(() => { | ||
x = 'b'; | ||
}); | ||
defer(() => { | ||
x = 'c'; | ||
}); | ||
|
||
assert.strictEqual(x, 'a'); | ||
|
||
await Promise.resolve().then(() => { | ||
assert.strictEqual(x, 'a'); | ||
}); | ||
|
||
await sleep(0); | ||
|
||
assert.strictEqual(x, 'c'); | ||
}); | ||
|
||
it('should respect timeout', async () => { | ||
let x = 'a'; | ||
|
||
defer(() => { | ||
x = 'b'; | ||
}, 1); | ||
defer(() => { | ||
x = 'c'; | ||
}); | ||
|
||
assert.strictEqual(x, 'a'); | ||
|
||
await Promise.resolve().then(() => { | ||
assert.strictEqual(x, 'a'); | ||
}); | ||
|
||
await sleep(0); | ||
|
||
assert.strictEqual(x, 'c'); | ||
|
||
await sleep(0); | ||
|
||
assert.strictEqual(x, 'c'); | ||
|
||
await sleep(1); | ||
|
||
assert.strictEqual(x, 'b'); | ||
}); | ||
|
||
it('should allow passing arguments', async () => { | ||
let z = 0; | ||
defer( | ||
(x, y) => { | ||
z = x + y; | ||
}, | ||
0, | ||
1, | ||
2, | ||
); | ||
|
||
assert.strictEqual(z, 0); | ||
|
||
await Promise.resolve().then(() => { | ||
assert.strictEqual(z, 0); | ||
}); | ||
|
||
await sleep(0); | ||
|
||
assert.strictEqual(z, 3); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
#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 = <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(); | ||
}; |