Skip to content

Instantly share code, notes, and snippets.

@furf
Last active July 22, 2020 22:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save furf/5f805572ea6b2449760add1089e5a8d9 to your computer and use it in GitHub Desktop.
Save furf/5f805572ea6b2449760add1089e5a8d9 to your computer and use it in GitHub Desktop.
Experiments for canceling timeouts when system sleeps.
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;
}
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