Cancellable async tasks with LIFO cleanup hooks, race, all, and task pool.
Module cancellable | Source packages/front/fw/src/io/sync/cancellable.js | Deps abort | Worker-safe yes
Resolve
const cancellable = runtime.resolve('cancellable');
// Returns: { create, race, all, pool }
API
cancellable.create(asyncFn) → TaskInstance
Creates a cancellable task.
const task = cancellable.create(async (ctx) => {
const conn = await openConnection();
ctx.onCancel(() => conn.close()); // LIFO cleanup
const data = await fetchData(url, { signal: ctx.signal });
ctx.throwIfAborted(); // explicit cooperation point
return process(data);
});
Context ctx:
| Property | Type | Description |
|---|---|---|
ctx.signal |
AbortSignal |
Cancellation signal |
ctx.onCancel(fn) |
(fn) => void |
Registers a cleanup hook |
ctx.throwIfAborted() |
() => void |
Throws AbortError if the task is cancelled |
Task instance:
| Method/Prop | Description |
|---|---|
task.run() |
async — executes the task. Throws if called twice |
task.cancel(reason?) |
→ Promise — cancels and runs the hooks. Idempotent |
task.cancelled |
getter boolean |
task.done |
getter boolean (resolved, rejected, or cancelled) |
task.signal |
getter AbortSignal |
Hook semantics:
onCancel: LIFO (last registered = first called)- Hook that throws: error logged via
console.error, other hooks continue cancel()beforerun():run()rejects immediately with AbortError, hooks not executedonCancelafter cancel already triggered:fnruns at the next microtask
cancellable.race(...tasks) → Promise
Runs all tasks. The first to complete (resolve or reject) wins. The others receive cancel('cancellable: race lost').
cancellable.all(tasks) → Promise
Runs all tasks. Resolves with [r1, r2, …] if all succeed. If one fails, cancels all others and rejects with the first error.
cancellable.pool() → PoolInstance
| Method/Prop | Description |
|---|---|
pool.add(task) |
Registers and starts the task |
pool.cancelAll(reason?) |
→ Promise — cancels all, waits for cleanups |
pool.size |
getter number — tasks still active |
pool.cleared |
getter boolean — true after cancelAll |
Auto-cleanup: a task that finishes on its own is automatically removed from the pool.
Cooperative cancellation
Important: this module cannot force-interrupt an async function. The task must cooperate with
ctx.signal.
Cooperation patterns:
- Pass the signal to network APIs:
fetch(url, { signal: ctx.signal }) - Check explicitly:
ctx.throwIfAborted()at critical logical points - Wait with abort:
await abort.race(myOperation, ctx.signal)
Difference from raw abort
abort |
cancellable |
|
|---|---|---|
| Signal | ✓ | via ctx.signal |
| Cleanup hooks | ✗ | ✓ LIFO |
| race/all composition | ✗ | ✓ |
| Pool | ✗ | ✓ |
abort exposes signals; cancellable adds cleanup orchestration.
Examples
Task with cleanup
const cancellable = runtime.resolve('cancellable');
const task = cancellable.create(async (ctx) => {
const ws = new WebSocket(url);
ctx.onCancel(() => ws.close());
const result = await new Promise((resolve, reject) => {
ws.onmessage = e => resolve(e.data);
ws.onerror = reject;
});
return result;
});
task.run().then(console.log).catch(console.error);
setTimeout(() => task.cancel(), 5000);
race — first result wins
const primary = cancellable.create(() => fetchPrimary(signal));
const fallback = cancellable.create(() => new Promise(r => setTimeout(() => r(cached), 100)));
const result = await cancellable.race(primary, fallback);
Request pool
const pool = cancellable.pool();
for (const url of urls) {
pool.add(cancellable.create(async (ctx) => {
return await fetch(url, { signal: ctx.signal }).then(r => r.json());
}));
}
// Cancel everything on error or navigation
onUnload(() => pool.cancelAll('page unload'));
Worker Usage
const worker = fw.createWorker(
async function ({ libs }) {
const task = libs.cancellable.create(async (ctx) => {
// heavy computation in worker
ctx.throwIfAborted();
return result;
});
await task.run();
},
{ dependencies: ['cancellable'] }
);
// Note: pool is intra-context only. For cross-worker, use processRPC + serialized abort signal.
Notes
cancel()returns aPromisethat resolves when all hooks have finished (useful forpool.cancelAll).- Async hooks (returning a Promise) are awaited by
pool.cancelAll()but not by standalonetask.cancel(). pool.add(task)starts the task — do not calltask.run()separately.