Last active
July 22, 2020 22:03
-
-
Save furf/5f805572ea6b2449760add1089e5a8d9 to your computer and use it in GitHub Desktop.
Experiments for canceling timeouts when system sleeps.
This file contains hidden or 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
type Timestamp = number; | |
type Callback = (error: Error | null, timestamp: Timestamp) => void; | |
/** | |
* The default number of milliseconds beyond the expected time of execution to | |
* wait before assuming failure. | |
*/ | |
const THRESHOLD = 10000; | |
/** | |
* A custom error for passing to failed timers. | |
*/ | |
class TimeoutError extends Error { | |
name = 'TimeoutError'; | |
} | |
/** | |
* setTimeoutWithThreshold sets a timer which executes a function or specified | |
* piece of code once the timer expires. Should the code not execute within a | |
* specified threshold beyond the expected delay, the callback will receive an | |
* error to signal failure to execute in a timely manner. | |
* @param callback A function to be executed after the timer expires. | |
* @param delay The time, in milliseconds (thousandths of a second), the timer | |
* should wait before the specified function or code is executed. | |
* @param threshold The number of milliseconds beyond the expected time of | |
* execution to wait before assuming failure to execute in a timely | |
* manner. | |
* @returns The returned timeoutID is a positive integer value which identifies | |
* the timer created by the call to setTimeout(); this value can be | |
* passed to clearTimeout() to cancel the timeout. | |
*/ | |
function setTimeoutWithThreshold( | |
callback: Callback, | |
delay: number, | |
threshold = THRESHOLD, | |
) { | |
const then = Date.now(); | |
return setTimeout(() => { | |
const now = Date.now(); | |
const expectedAt = then + delay; | |
const expiredAt = expectedAt + threshold; | |
const isExpired = now > expiredAt; | |
if (isExpired) { | |
callback(new TimeoutError('Expired timeout.'), expectedAt); | |
} else { | |
callback(null, now); | |
} | |
}, delay); | |
} | |
/** | |
* setIntervalWithThreshold repeatedly calls a function or executes a code snippet, | |
* with a fixed time delay between each call. Should the code not execute within | |
* a specified threshold beyond the expected delay, the callback will receive an | |
* error to signal failure to execute in a timely manner. | |
* @param callback A function to be executed after the timer expires. | |
* @param delay The time, in milliseconds (thousandths of a second), the timer | |
* should wait before the specified function or code is executed. | |
* @param threshold The number of milliseconds beyond the expected time of | |
* execution to wait before assuming failure to execute in a timely | |
* manner. | |
* @returns The returned intervalID is a numeric, non-zero value which identifies | |
* the timer created by the call to setInterval(); this value can be | |
* passed to clearInterval() to cancel the timeout. | |
*/ | |
function setIntervalWithThreshold( | |
callback: Callback, | |
delay: number, | |
threshold = THRESHOLD, | |
) { | |
let then = Date.now(); | |
const intervalId = setInterval(() => { | |
const now = Date.now(); | |
const expectedAt = then + delay; | |
const expiredAt = expectedAt + threshold; | |
const isExpired = now > expiredAt; | |
if (isExpired) { | |
clearInterval(intervalId); | |
callback(new TimeoutError('Expired interval.'), expectedAt); | |
} else { | |
then = now; | |
callback(null, now); | |
} | |
}, delay); | |
return intervalId; | |
} |
This file contains hidden or 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
interface WaitPromise<T> extends Promise<T> { | |
then<TResult1 = T, TResult2 = never>( | |
onfulfilled?: | |
| ((value: T) => TResult1 | PromiseLike<TResult1>) | |
| undefined | |
| null, | |
onrejected?: | |
| ((reason: any) => TResult2 | PromiseLike<TResult2>) | |
| undefined | |
| null, | |
): WaitPromise<TResult1 | TResult2>; | |
catch<TResult = never>( | |
onrejected?: | |
| ((reason: any) => TResult | PromiseLike<TResult>) | |
| undefined | |
| null, | |
): WaitPromise<T | TResult>; | |
finally(onfinally?: (() => void) | undefined | null): WaitPromise<T>; | |
} | |
type Timeout = ReturnType<typeof setTimeout>; | |
const THRESHOLD = 10000; | |
/** | |
* Linking | |
*/ | |
const promiseToTimeout = new Map<WaitPromise<any>, Timeout>(); | |
const timeoutToPromises = new Map<Timeout, WaitPromise<any>[]>(); | |
function link(promise: WaitPromise<any>, timeout: Timeout): void { | |
// Create promise-to-timeout reference. | |
promiseToTimeout.set(promise, timeout); | |
// Create timeout-to-promises reference. | |
const promises = timeoutToPromises.get(timeout) || []; | |
timeoutToPromises.set(timeout, [...promises, promise]); | |
} | |
function unlink(timeout: Timeout): void { | |
const promises = timeoutToPromises.get(timeout); | |
if (typeof promises === 'undefined') return; | |
// Delete all promise-to-timeout references. | |
promises.forEach(promise => { | |
promiseToTimeout.delete(promise); | |
}); | |
// Delete timeout-to-promises reference. | |
timeoutToPromises.delete(timeout); | |
} | |
/** | |
* WaitPromise | |
*/ | |
class WaitPromise<T> extends Promise<T> { | |
then(onFulfilled: () => void, onRejected: () => void) { | |
const promise = super.then(onFulfilled, onRejected); | |
const timeout = promiseToTimeout.get(this); | |
if (timeout) { | |
link(promise, timeout); | |
} | |
return promise; | |
} | |
catch(onRejected: () => void) { | |
const promise = super.catch(onRejected); | |
const timeout = promiseToTimeout.get(this); | |
if (timeout) { | |
link(promise, timeout); | |
} | |
return promise; | |
} | |
finally(onFinally: () => void) { | |
const promise = super.finally(onFinally); | |
const timeout = promiseToTimeout.get(this); | |
if (timeout) { | |
link(promise, timeout); | |
} | |
return promise; | |
} | |
} | |
/** | |
* wait | |
*/ | |
export function waitFor(delay: number, threshold = THRESHOLD) { | |
const expected = Date.now() + delay; | |
let timeout: Timeout; | |
const promise = new WaitPromise((resolve, reject) => { | |
timeout = setTimeout(() => { | |
const expired = Date.now() - threshold > expected; | |
if (expired) { | |
reject(); | |
} else { | |
resolve(); | |
} | |
unlink(timeout); | |
}, delay); | |
}); | |
// Map the returned promise to the timeout for cancellation. | |
link(promise, timeout!); | |
return promise; | |
} | |
export function waitUntil(time: number, threshold = THRESHOLD) { | |
const delay = time - Date.now(); | |
return waitFor(delay, threshold); | |
} | |
export function clearWait(promise) { | |
const timeout = promiseToTimeout.get(promise); | |
if (typeof timeout === 'undefined') return; | |
clearTimeout(timeout); | |
unlink(timeout); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment