/** * This module is a helper for asynchronous work. It allows resources acquired asynchronously to * be conveniently and reliably released. * * Usage: * (1) Implement a function `myFunc(flow: AsyncFlow)`. The `flow` argument provides some helpers: * * // Create a disposable, making it owned by the flow. It will be disposed when the flow * // ends, whether successfully, on error, or by being cancelled. * const foo = Foo.create(flow, ...); * * // As with Disposables in general, schedule a callback to be called when the flow ends. * flow.onDispose(...); * * // Release foo from the flow's ownership, and give its ownership to another object. This way * // `other` will be responsible for disposing foo, and not flow. * other.autoDispose(flow.release(foo)) * * // Abort the flow (by throwing CancelledError) if cancellation is requested. This should * // be called after async work, in case the flow shouldn't be continued. * checkIfCancelled(); * * (2) Call `runner = FlowRunner.create(owner, myFunc)`. The flow will start. Once myFunc's * promise resolves (including on failure), the objects owned by the flow will be disposed. * * The runner exposes the promise for when the flow ends as `runner.resultPromise`. * * If the runner itself is disposed, the flow will be cancelled, and disposed once it notices * the cancellation. * * To replace one FlowRunner with another, put it in a grainjs Holder. */ import {Disposable, IDisposable} from 'grainjs'; type DisposeListener = ReturnType; export class CancelledError extends Error {} export class FlowRunner extends Disposable { public resultPromise: Promise; constructor(func: (flow: AsyncFlow) => Promise) { super(); const flow = AsyncFlow.create(null); async function runFlow() { try { return await func(flow); } finally { flow.dispose(); } } this.resultPromise = runFlow(); this.onDispose(flow.cancel, flow); } } export class AsyncFlow extends Disposable { private _handles = new Map(); private _isCancelled = false; public autoDispose(obj: T): T { const lis = this.onDispose(obj.dispose, obj); this._handles.set(obj, lis); return obj; } public release(obj: T): T { const h = this._handles.get(obj); if (h) { h.dispose(); } this._handles.delete(obj); return obj; } public checkIfCancelled() { if (this._isCancelled) { throw new CancelledError('cancelled'); } } public cancel() { this._isCancelled = true; } }