mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
84 lines
2.7 KiB
TypeScript
84 lines
2.7 KiB
TypeScript
|
/**
|
||
|
* 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<Disposable["onDispose"]>;
|
||
|
|
||
|
export class CancelledError extends Error {}
|
||
|
|
||
|
export class FlowRunner extends Disposable {
|
||
|
public resultPromise: Promise<void>;
|
||
|
|
||
|
constructor(func: (flow: AsyncFlow) => Promise<void>) {
|
||
|
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<IDisposable, DisposeListener>();
|
||
|
private _isCancelled = false;
|
||
|
|
||
|
public autoDispose<T extends IDisposable>(obj: T): T {
|
||
|
const lis = this.onDispose(obj.dispose, obj);
|
||
|
this._handles.set(obj, lis);
|
||
|
return obj;
|
||
|
}
|
||
|
|
||
|
public release<T extends IDisposable>(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;
|
||
|
}
|
||
|
}
|