UNPKG

@naturalcycles/js-lib

Version:

Standard library for universal (browser + Node.js) javascript

86 lines (85 loc) 3.05 kB
class AsyncManagerImpl { logger = console; pendingOps = new Set(); onErrorHooks = []; runInBackground(promise) { const wrappedPromise = promise .catch(err => this.fireOnErrorHooks(err)) .finally(() => this.pendingOps.delete(wrappedPromise)); this.pendingOps.add(wrappedPromise); } onError(fn) { this.onErrorHooks.push(fn); } /** * Resolves when all pending operations settle. * They may resolve or reject, allDone will never throw. * Errors (rejections) are reported to onErrorHooks (instead). * * If timeout is specified - it resolves if timeout has reached. */ async allDone(timeout) { const { size } = this.pendingOps; if (!size) return; const { logger } = this; const started = Date.now(); if (timeout) { const result = await Promise.race([ Promise.allSettled(this.pendingOps), new Promise(resolve => setTimeout(resolve, timeout, 'timeout')), ]); if (result === 'timeout') { logger.warn(`AsyncManager.allDone timed out after ${timeout} ms with ${this.pendingOps.size} pending op(s)`); return; } } else { await Promise.allSettled(this.pendingOps); } logger.log(`AsyncManager.allDone for ${size} op(s) in ${Date.now() - started} ms`); } reset() { this.pendingOps.clear(); this.onErrorHooks = []; } fireOnErrorHooks(err) { if (this.onErrorHooks.length) { this.onErrorHooks.forEach(hook => hook(err)); } else { this.logger.error('AsyncManager unhandled rejection:', err); } } } /** * Singleton which keeps track of async operations - "voided promise-returning functions" * that should run in parallel to the main request. * * It is an alternative to do `void doSomeAnalytics()`, which should run in parallel * and not block the request (not slow down nor fail the request on analytics api failure). * * At the same time, `void doSomeAnalytics()` gets completely detached and untracked, * nothing awaits it, its rejection becomes unhandledRejection (and may kill Node.js process). * * With AsyncManager, you instead register all those "voided" calls like this: * * AsyncManager.runInBackground(doSomeAnalytics()) * * Then, in a few places you may be interested to ensure that all async operations have been finished. * The places can be: * - Graceful shutdown of a backend service * - Before the end of runScript * - At the end of each unit test, to make sure async ops don't leak * * You ensure no pending async operations like this: * * await AsyncManager.allDone() * * which never throws, but instead awaits all operations to be settled. * * @experimental */ export const AsyncManager = new AsyncManagerImpl(); // Shorthand alias export const runInBackground = AsyncManager.runInBackground.bind(AsyncManager);