import type { BackpressureOptions } from "./backpressure.js"; import { TaskContext } from "./context.js"; import type { TaskStore } from "./store/base.js"; import { type TaskDef } from "./task.js"; export type Handler = (ctx: TaskContext, payload: any) => unknown | Promise; /** Handler typed against a TaskDef: payload is P, the return is R. */ export type TypedHandler = (ctx: TaskContext, payload: P) => R | Promise; /** Where an error the worker recovered from came from. */ export type ErrorPhase = "claim" | "execute"; /** * Backpressure is accepted here too, not only on CairnQ: a handler spawning * children through TaskContext.submit is a producer, and in a worker process * there is usually no CairnQ handle to have configured the store. */ export interface WorkerOptions extends Partial { concurrency?: number; leaseMs?: number; heartbeatIntervalMs?: number; pollIntervalMs?: number; claimBatch?: number; /** Base delay before re-running a failed attempt; doubles per attempt. 0 disables. */ retryBackoffMs?: number; /** Ceiling for the doubling. */ retryBackoffMaxMs?: number; /** * Wall-clock ceiling for one attempt. The heartbeat renews the lease for as * long as a handler runs, so a hung handler would otherwise hold its task * `running` (and its concurrency slot) forever — cancel can't help, * cooperative checks need a live handler. On expiry the worker abandons the * attempt (ctx.signal aborts, further ctx writes throw LostLease) and records * a retryable `handler_timeout` failure. Unset disables the ceiling. */ maxRunMs?: number; /** * Resident payload bytes allowed across running handlers, independent of * their count. * * `concurrency` bounds tasks, not memory, so a worker sized for small payloads * holds concurrency * largest-payload bytes the moment a batch of big ones * arrives — for payloads that carry media inline, that is the difference * between megabytes and gigabytes resident. Once the budget is spent the * worker stops claiming until running handlers give it back. * * The bound is on tasks already executing. A claim commits to a whole batch * before any size is known, so one batch can overshoot by up to `claimBatch` * payloads; lower `claimBatch` to tighten that. A single payload larger than * the entire budget still runs — alone, rather than deadlocking the worker. * * Costs one JSON serialization per task to measure, so it is only computed * when set. Unset disables the budget. */ maxInFlightBytes?: number; /** * Called for errors the worker survived — a claim that threw, a store write * that failed while finalizing a task. Without it these are silent: the run * loop carries on either way, so this is the only place an operator learns a * worker is limping. Must not throw. */ onError?: (err: unknown, info: { phase: ErrorPhase; taskId?: string; }) => void; } /** Exponential backoff for the next attempt of a task that just failed. */ export declare function retryDelayMs(attempt: number, baseMs: number, maxMs: number): number; export declare class Worker { private readonly store; private readonly queues; private readonly opts; private readonly handlers; private readonly workerId; /** Payload bytes charged to running handlers — see maxInFlightBytes. */ private inFlightBytes; private stopped; private stopWake; private readonly stopped$; private ownsStore; constructor(store: TaskStore, queues: string[], opts?: WorkerOptions); static sqlite(path: string, opts?: WorkerOptions & { queues?: string[]; busyTimeoutMs?: number; }): Worker; /** Multi-host backend. `dsn` is a libpq connection string; requires the * optional `pg` package. */ static postgres(dsn: string, opts?: WorkerOptions & { queues?: string[]; max?: number; }): Worker; get id(): string; task(handler: Handler): this; task(name: string, handler: Handler): this; task(def: TaskDef, handler: TypedHandler): this; stop(): void; /** Close the underlying store connection. Call after run() returns. */ close(): Promise; private closeIfOwned; private report; run(opts?: { concurrency?: number; }): Promise; private loop; /** Blocking-style entry point for a standalone worker process: run until * SIGINT/SIGTERM, then close the store. Use this at a script's top level; * use run() / background() when you manage the event loop yourself. */ serve(opts?: { concurrency?: number; }): Promise; /** Run the worker in the same process for the duration of fn (deployment mode A). */ background(fn: () => Promise, opts?: { concurrency?: number; }): Promise; /** * Run one task to completion. Never rejects: a task-level failure is reported * through onError and the loop moves on. (It used to reject into a promise * nobody awaited — an unhandled rejection that took the process down.) */ private execute; /** * Run one attempt, bounded by maxRunMs when set. On timeout the attempt is * abandoned: the context is flagged lease-lost first (ctx.signal aborts, and * a handler that keeps running can never write again — see * TaskContext.owned), then the still-pending promise is left to settle on * its own, its outcome discarded. The caller records the handler_timeout * failure; lease recovery is NOT involved, so redelivery is immediate. */ private attempt; private startHeartbeat; private safeFail; /** * The empty-poll sleep. A store with a push channel (Postgres LISTEN/NOTIFY) * cuts it short when a task on this worker's queues becomes claimable; * stop() interrupts it either way, and sleepOrStop bounds it at `ms` so the * poll fallback — which also drives lease recovery — never stretches. */ private idle; private sleepOrStop; /** Take SIGINT/SIGTERM for the duration of serve(). Returns the undo. */ private installSignals; }