generated from sapphiredev/sapphire-template
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: add `sleep` and `sleepSync` * feat: support ref and abort controller * chore: add dom to lib * fix: error DOMException isn't available in older version * test: proper error check * build: add dom lib ref * build: dont add DOMException in index.d.ts * fix: add proper reason * fix: apply suggestions * test: use closeto * test: 45 will pass all the tests * test: change sync test to 50+ * fix: exactly 50ms * docs: add note for sleepSync * fix: add 51 too
- Loading branch information
1 parent
36e21d1
commit 8cd1293
Showing
6 changed files
with
162 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
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,68 @@ | ||
export interface SleepOptions { | ||
/** | ||
* When provided the corresponding `AbortController` can be used to cancel an asynchronous action. | ||
*/ | ||
signal?: AbortSignal | undefined; | ||
|
||
/** | ||
* Set to `false` to indicate that the scheduled `Timeout` | ||
* should not require the Node.js event loop to remain active. | ||
* @default true | ||
*/ | ||
ref?: boolean | undefined; | ||
} | ||
|
||
export class AbortError extends Error { | ||
public readonly code: string; | ||
public constructor( | ||
message?: string, | ||
options?: { | ||
cause?: unknown; | ||
} | ||
) { | ||
super(message, options); | ||
this.name = 'AbortError'; | ||
this.code = 'ERR_ABORT'; | ||
} | ||
} | ||
|
||
/** | ||
* Sleeps for the specified number of milliseconds. | ||
* @param ms The number of milliseconds to sleep. | ||
* @param value A value with which the promise is fulfilled. | ||
* @see {@link sleepSync} for a synchronous version. | ||
*/ | ||
export function sleep<T = undefined>(ms: number, value?: T, options?: SleepOptions): Promise<T> { | ||
return new Promise((resolve, reject) => { | ||
const timer: NodeJS.Timeout | number = setTimeout(() => resolve(value!), ms); | ||
const signal = options?.signal; | ||
if (signal) { | ||
signal.addEventListener('abort', () => { | ||
clearTimeout(timer); | ||
reject( | ||
new AbortError('The operation was aborted', { | ||
cause: signal.reason | ||
}) | ||
); | ||
}); | ||
} | ||
if (options?.ref === false && typeof timer === 'object') { | ||
timer.unref(); | ||
} | ||
}); | ||
} | ||
|
||
/** | ||
* Sleeps for the specified number of milliseconds synchronously. | ||
* We should probably note that unlike {@link sleep} (which uses CPU tick times), | ||
* sleepSync uses wall clock times, so the precision is near-absolute by comparison. | ||
* That, and that synchronous means that nothing else in the thread will run for the length of the timer. | ||
* @param ms The number of milliseconds to sleep. | ||
* @param value A value to return. | ||
* @see {@link sleep} for an asynchronous version. | ||
*/ | ||
export function sleepSync<T = undefined>(ms: number, value?: T): T { | ||
const end = Date.now() + ms; | ||
while (Date.now() < end) continue; | ||
return value!; | ||
} |
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
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,68 @@ | ||
import { AbortError, sleep, sleepSync } from '../src'; | ||
import { expectError } from './util/macros/comparators'; | ||
|
||
const DOMException: typeof globalThis.DOMException = | ||
globalThis.DOMException ?? | ||
(() => { | ||
// DOMException was only made a global in Node v17.0.0, | ||
// but our CI runs on Node v16.6.0 too | ||
try { | ||
atob('~'); | ||
} catch (err) { | ||
return Object.getPrototypeOf(err).constructor; | ||
} | ||
})(); | ||
|
||
describe('sleep', () => { | ||
test('GIVEN a number of ms THEN resolve the promise after that time', async () => { | ||
const start = Date.now(); | ||
await sleep(50); | ||
expect(Date.now() - start).greaterThanOrEqual(45); | ||
}); | ||
|
||
test('GIVEN a number of ms and a value THEN resolve the promise after that time with the value', async () => { | ||
const start = Date.now(); | ||
const value = await sleep(50, 'test'); | ||
expect(Date.now() - start).greaterThanOrEqual(45); | ||
expect<string>(value).toBe('test'); | ||
}); | ||
|
||
test('GIVEN a abort signal THEN reject the promise', async () => { | ||
const controller = new AbortController(); | ||
const promise = sleep(1000, 'test', { signal: controller.signal }); | ||
controller.abort(); | ||
await expectError( | ||
() => promise, | ||
new AbortError('The operation was aborted', { | ||
cause: new DOMException('This operation was aborted', 'AbortError') | ||
}) | ||
); | ||
}); | ||
|
||
test('GIVEN a abort signal with reason THEN reject the promise with the reason as cause', async () => { | ||
const controller = new AbortController(); | ||
const promise = sleep(1000, 'test', { signal: controller.signal }); | ||
controller.abort('test'); | ||
await expectError( | ||
() => promise, | ||
new AbortError('The operation was aborted', { | ||
cause: 'test' | ||
}) | ||
); | ||
}); | ||
}); | ||
|
||
describe('sleepSync', () => { | ||
test('GIVEN a number of ms THEN return after that time', () => { | ||
const start = Date.now(); | ||
sleepSync(50); | ||
expect(Date.now() - start).oneOf([50, 51]); | ||
}); | ||
|
||
test('GIVEN a number of ms and a value THEN return after that time with the value', () => { | ||
const start = Date.now(); | ||
const value = sleepSync(50, 'test'); | ||
expect(Date.now() - start).oneOf([50, 51]); | ||
expect<string>(value).toBe('test'); | ||
}); | ||
}); |
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
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,23 @@ | ||
type _Error = Error & { code?: string }; | ||
|
||
export function IdenticalError(expected: _Error, actual: _Error) { | ||
expect(actual).toBeDefined(); | ||
expect(actual).toBeInstanceOf(expected.constructor); | ||
expect(actual.message).toBe(expected.message); | ||
if (expected.code) expect(actual.code).toBe(expected.code); | ||
if (expected.cause) { | ||
if (expected.cause instanceof Error) IdenticalError(expected.cause, actual.cause as _Error); | ||
else expect(actual.cause).toBe(expected.cause); | ||
} | ||
} | ||
|
||
export async function expectError<T = any>(cb: () => T, expected: Error & { code?: string }) { | ||
try { | ||
await cb(); | ||
} catch (error) { | ||
IdenticalError(expected, error as _Error); | ||
return; | ||
} | ||
|
||
throw new Error('Expected to throw, but failed to do so'); | ||
} |