import { Snode } from '../onions'; type SimpleFunction = (arg: T) => void; type Return = Promise | T; async function toPromise(value: Return): Promise { return value instanceof Promise ? value : Promise.resolve(value); } export class TaskTimedOutError extends Error { constructor() { super('Task timed out'); // Set the prototype explicitly. Object.setPrototypeOf(this, TaskTimedOutError.prototype); } } // one action resolves all const snodeGlobalLocks: Record> = {}; export async function allowOnlyOneAtATime( name: string, process: () => Promise, timeoutMs?: number ) { // if currently not in progress if (snodeGlobalLocks[name] === undefined) { // set lock snodeGlobalLocks[name] = new Promise(async (resolve, reject) => { // set up timeout feature let timeoutTimer = null; if (timeoutMs) { timeoutTimer = setTimeout(() => { window.log.warn(`allowOnlyOneAtATime - TIMEDOUT after ${timeoutMs}s`); // tslint:disable-next-line: no-dynamic-delete delete snodeGlobalLocks[name]; // clear lock reject(); }, timeoutMs); } // do actual work let innerRetVal; try { innerRetVal = await process(); } catch (e) { if (typeof e === 'string') { window.log.error(`allowOnlyOneAtATime - error ${e}`); } else { window.log.error(`allowOnlyOneAtATime - error ${e.code} ${e.message}`); } // clear timeout timer if (timeoutMs) { if (timeoutTimer !== null) { clearTimeout(timeoutTimer); timeoutTimer = null; } } // tslint:disable-next-line: no-dynamic-delete delete snodeGlobalLocks[name]; // clear lock throw e; } // clear timeout timer if (timeoutMs) { if (timeoutTimer !== null) { clearTimeout(timeoutTimer); timeoutTimer = null; } } // tslint:disable-next-line: no-dynamic-delete delete snodeGlobalLocks[name]; // clear lock // release the kraken resolve(innerRetVal); }); } return snodeGlobalLocks[name]; } /** * Create a promise which waits until `done` is called or until `timeout` period is reached. * If `timeout` is reached then this will throw an Error. * * @param task The task to wait for. * @param timeout The timeout period. */ export async function waitForTask( task: (done: SimpleFunction) => Return, timeoutMs: number = 2000 ): Promise { const timeoutPromise = new Promise((_, rej) => { const wait = setTimeout(() => { clearTimeout(wait); rej(new TaskTimedOutError()); }, timeoutMs); }); const taskPromise = new Promise(async (res, rej) => { try { await toPromise(task(res)); } catch (e) { rej(e); } }); return Promise.race([timeoutPromise, taskPromise]) as Promise; } export interface PollOptions { timeoutMs: number; interval: number; } /** * Creates a promise which calls the `task` every `interval` until `done` is called or until `timeout` period is reached. * If `timeout` is reached then this will throw an Error. * * @param task The task which runs every `interval` ms. * @param options The polling options. */ export async function poll( task: (done: SimpleFunction) => Return, options: Partial = {} ): Promise { const defaults: PollOptions = { timeoutMs: 2000, interval: 100, }; const { timeoutMs, interval } = { ...defaults, ...options, }; const endTime = Date.now() + timeoutMs; let stop = false; const finish = () => { stop = true; }; const _poll = async (resolve: any, reject: any) => { if (stop) { resolve(); } else if (Date.now() >= endTime) { finish(); reject(new Error('Periodic check timeout')); } else { try { await toPromise(task(finish)); } catch (e) { finish(); reject(e); return; } setTimeout(() => { void _poll(resolve, reject); }, interval); } }; return new Promise((resolve, reject) => { void _poll(resolve, reject); }); } /** * Creates a promise which waits until `check` returns `true` or rejects if `timeout` preiod is reached. * If `timeout` is reached then this will throw an Error. * * @param check The boolean check. * @param timeout The time before an error is thrown. */ export async function waitUntil(check: () => Return, timeoutMs: number = 2000) { // This is causing unhandled promise rejection somewhere in MessageQueue tests return poll( async done => { const result = await toPromise(check()); if (result) { done(); } }, { timeoutMs, interval: timeoutMs / 20, } ); } export async function timeout(promise: Promise, timeoutMs: number = 2000): Promise { const timeoutPromise = new Promise((_, rej) => { const wait = setTimeout(() => { clearTimeout(wait); rej(new TaskTimedOutError()); }, timeoutMs); }); return Promise.race([timeoutPromise, promise]); } export async function delay(timeoutMs: number = 2000): Promise { return new Promise(resolve => { setTimeout(() => { resolve(true); }, timeoutMs); }); } // tslint:disable: no-string-based-set-timeout export const sleepFor = async (ms: number) => new Promise(resolve => { setTimeout(resolve, ms); }); // Taken from https://stackoverflow.com/questions/51160260/clean-way-to-wait-for-first-true-returned-by-promise // The promise returned by this function will resolve true when the first promise // in ps resolves true *or* it will resolve false when all of ps resolve false export const firstTrue = async (ps: Array>) => { const newPs = ps.map( async p => new Promise( // eslint-disable more/no-then // tslint:disable: no-void-expression (resolve, reject) => p.then(v => v && resolve(v), reject) ) ); // eslint-disable-next-line more/no-then newPs.push(Promise.all(ps).then(() => false)); return Promise.race(newPs) as Promise; };