@xndrjs/tasks
v0.2.0
Published
Lazy async tasks utilities.
Readme
@xndrjs/tasks
Lazy asynchronous tasks: effects run only when awaited or chained like a Promise, with optional retry and predictable composability, mostly for infrastructure code.
Installation
npm install @xndrjs/tasksConcepts
task(effect)— wrapseffect: () => Promise<T>. Eachawaitor.then/.catch/.finallyinvokes the effect again unless you memoize externally or use.inflightDedup..retry(shouldRetry, options?)— runs the underlying effect up tomaxAttemptstimes (including the first try).shouldRetry(error, attempt)is async-friendly (e.g. backoff inside the predicate). DefaultmaxAttemptsis 3;maxAttemptsmust be an integer ≥ 1. At most one.retry()per chain..inflightDedup(key, registry?)—keymust be asymbol. Concurrent consumers using the samekeyin the sameInflightRegistryshare one in-flight run of the current effect (if you used.retry, the shared run is the whole retry sequence). At most one.inflightDedup()per chain. When that run settles, the slot is cleared. Omitregistryto use the package default (process-wide); passcreateInflightRegistry()to scope slots (tests, isolation).
Allowed shapes
task(effect)onlytask(effect).retry(...)(no dedup)task(effect).inflightDedup(...)(no retry)task(effect).retry(...).inflightDedup(...)— retry before dedup
You cannot call .retry after .inflightDedup, chain two .retry, or chain two .inflightDedup (enforced by TypeScript).
Choosing a dedup key
- Private logical operation (no accidental cross-package collisions): one module-level binding, e.g.
const loadUsersOp = Symbol("loadUsers"). Each call toSymbol("loadUsers")without reusing the binding would create a different symbol; keep a singleconstand import it where needed. - Intentional global agreement (same slot for everyone using that name):
Symbol.for("MY_APP:loadUsers")— the runtime registry is shared; only use when overlaps are desired.
Registry
createInflightRegistry()— returns a freshMap-compatible store. Use when tests or subsystems must not share in-flight state with the rest of the process.
Example
import { sleep, task } from "@xndrjs/tasks";
const loadUsers = Symbol("loadUsers");
const usersTask = task(async () => fetch("/api/users"))
.retry(
async (error, attempt) => {
if (!shouldRetry(error)) return false;
// `attempt` starts at 0 after the first failure.
// Wait: 200ms, 400ms, 800ms, 1600ms...
const delayMs = 200 * 2 ** attempt;
await sleep(delayMs);
return true; // retry after sleep
},
{
maxAttempts: 5,
}
)
.inflightDedup(loadUsers)
.then((response) => response.json());
const users = await usersTask;API
Exported symbols: task, sleep, Task, TaskAfterRetry, TaskFinal, TaskPromise, InflightRegistry, createInflightRegistry, RetryPredicate, RetryOptions (maxAttempts?: number).
Caveats
- Tasks are lazy and re-executed on each await/then; use
.inflightDedupfor in-flight coalescing, or memoize externally for other caching needs. retrydoes not classify errors for you: domain-specific retry rules should live inshouldRetry.
License
MIT
