/** * Lifecycle-bound registry for fire-and-forget best-effort DB writes (T10490). * * Task mutators (`addTask`, `completeTask`) kick off best-effort graph/LOOM * population that must NOT block or fail the mutation. Historically these were * orphaned `import().then().catch()` promises with no handle. Under the vitest * `forks` pool that creates two failure modes: * * 1. **Cross-test DB races (intermittent shard failures).** A detached op * from test/file A can still be in flight when test/file B has already * created a fresh fixture and reset the shared SQLite singleton * (`store/sqlite.ts` `_db`/`_nativeDb`). The late write/read then lands on * B's connection, corrupting B's reads — e.g. a freshly-written * `pipeline_stage` reading back as null, which silently flips a * forward-only transition guard. * 2. **`EnvironmentTeardownError: Closing rpc while onUserConsoleLog was * pending`.** A detached op logging as the worker tears down races the * RPC close. (The structured-logger sweep, T10490 §3.3, addresses the * logging half; this registry addresses the lifetime half.) * * Registering each op here lets the test harness (and any caller that needs a * barrier) drain them via {@link awaitBackgroundOps} before resetting DB state. * * **Production behaviour is unchanged**: ops still run detached and nobody * awaits them on the hot path. The registry only adds an opt-in flush point. * * @task T10490 * @see packages/core/src/store/__tests__/test-db-helper.ts (flush wiring) */ import type { DatabaseSync } from 'node:sqlite'; import type { BackgroundJobWriteFence, OperationExecutionContext, OperationExecutionIdentity, OperationExecutionOptions, OperationExecutionStopCode, OperationExecutionTransfer, OperationExecutionTransferHandle, OperationResourceUsage, OperationWaitResult } from '@cleocode/contracts/jobs'; /** * Stage lazy effects until the supplied transaction has actually committed. * @typeParam T - Committed transaction result. * @param native - Existing writer handle whose nested savepoints share this boundary. * @param commit - Existing transaction implementation, including commit or rollback. * @param execution - Existing captured budget; never replaced with a fresh context. * @param schedule - Existing writer queue admission for committed effects. * @returns The original committed result; deferred failures never undo it. * @remarks Call around the actual transaction, not an individual row write. Nested * successful savepoints transfer effects to their parent; rollback discards them. * This adds no deadline and never executes eager promises again. * @example * ```ts * await withBackgroundOpCommitBoundary(native, () => executeTransaction()); * ``` */ export declare function withBackgroundOpCommitBoundary(native: DatabaseSync, commit: () => Promise, execution?: OperationExecutionContext, schedule?: (work: () => Promise) => Promise): Promise; /** * Track an eager legacy promise or defer a lazy effect until outer transaction commit. * @param op - Existing promise, or lazy operation that must not run before commit. * @param execution - Optional original budget, otherwise inherited from the transaction. * @returns An inspectable settled result, including rollback, cancellation and failures. * @remarks Lazy work retains captured async context and the original execution budget. * The registry counts staged work as pending. Eager promises have already started * and cannot gain rollback safety. Results are process-local, not durable job receipts. * @example * ```ts * const outcome = trackBackgroundOp(() => writeProjection()); * const result = await outcome; * ``` */ export declare function trackBackgroundOp(op: Promise | (() => Promise), execution?: OperationExecutionContext): Promise>; /** * What the drain barrier actually observed about the producers it awaited. * * Settlement alone never established producer success, which is why the * shutdown receipt used to disclose every drain as `unassessed`. The registry * stores each producer's own settled result, so the barrier can assess them * and report a number instead of a caveat (T12310). */ export interface BackgroundDrainReport { /** Tracked producers assessed since the previous drain consumed the ledger. * Untracked work is not included. */ readonly observed: number; /** Producers that completed their own work. */ readonly fulfilled: number; /** Producers that rejected for a reason other than an expected lifecycle stop. */ readonly failed: number; /** Producers stopped by teardown cancellation, scope closure or deadline, * not by their own failure. */ readonly cancelled: number; /** Staged producers never started because their transaction rolled back. */ readonly discarded: number; } /** * Whether a producer rejection is an expected teardown outcome rather than a failure. * * Teardown cancels registered execution contexts before it drains them, so a * best-effort producer that was still in flight rejects with a lifecycle code. * That is the shutdown path working as designed: the work was abandoned, not * attempted and failed, and the caller has nothing to act on. * * @param reason - The rejection reason observed from a tracked producer. * @returns `true` for cancellation, scope closure and deadline expiry. * @remarks An expected stop still means the work did not happen; it is silent * because it is not actionable, never because it succeeded. * @example * ```ts * if (!isExpectedTeardownRejection(error)) reportFailure(error); * ``` */ export declare function isExpectedTeardownRejection(reason: unknown): boolean; /** * Await every currently in-flight background op, then report what they did. * Safe to call repeatedly and when nothing is pending. Loops until the set * drains so an op that schedules further tracked work during the flush is also * awaited. * @returns Counts for every tracked producer assessed since the previous drain. * @remarks This legacy barrier can wait indefinitely on any one promise. It is * not the foreground maintenance budget or proof that untracked work stopped. * The counts describe only the tracked subset and are CONSUMED by this call, so * two drains never report the same producer twice; a zero `observed` means no * tracked producer settled since the last drain, not that none exists. * @example * ```ts * const report = await awaitBackgroundOps(); * if (report.failed > 0) disclose(report.failed); * ``` */ export declare function awaitBackgroundOps(): Promise; /** * Wait, within a budget, for the tracked best-effort work in flight to settle, * without consuming the producer ledger that the shutdown receipt reports. * * For a CLI path that hands a failure to code that may `process.exit` before * the coordinated teardown runs (T13164): `process.exit` kills a pending write * outright, so the caller settles the registry first. Settling is not closing: * the process keeps its handles and may continue. * * @param budgetMs - Upper bound on the wait. The timer is deliberately NOT * unref'd: a producer that never settles must not let the event loop drain * mid-wait, or the process would exit on its own before the caller sets its * exit code (a failed command exiting 0). It is cleared as soon as the drain * finishes, so it holds the process only while work is pending. * @returns Number of tracked producers still pending when the wait ended (`0` * when everything settled within the budget). * @remarks Untracked work is outside the registry and is not waited for. A * producer that outlives the budget keeps running and is lost if the process * then exits, exactly as before. * @example * ```ts * if (!response.success) await settleBackgroundOps(STEP_DEADLINE_MS); * ``` * @task T13164 */ export declare function settleBackgroundOps(budgetMs: number): Promise; /** * Number of background ops currently in flight. Diagnostic/test use — assert it * is `0` after a flush to account for the tracked subset at a test boundary. * @returns Number of registered, unsettled promises. * @remarks Unregistered work is not included in this count. * @example * ```ts * const pending = pendingBackgroundOpCount(); * ``` */ export declare function pendingBackgroundOpCount(): number; /** * Stable refusal at a cooperating operation boundary. * @remarks A refusal does not roll back work already committed by another stage. * @example * ```ts * if (error instanceof OperationExecutionError) record(error.code); * ``` */ export declare class OperationExecutionError extends Error { /** Machine-readable reason that a new stage was refused. */ readonly code: OperationExecutionStopCode; /** * Construct a boundary refusal. * @param code - Stable cancellation/deadline/limit reason. * @param message - Specific diagnostic for the caller. */ constructor(code: OperationExecutionStopCode, message: string); } /** * Capture one immutable scope and cooperative foreground lifetime. * @param identity - Explicit routing and caller identity, captured before awaiting. * @param options - Shared deadline, caller cancellation and admission limits. * @returns One context to pass unchanged through nested stages; close it in finally. * @remarks The timer requests cancellation; boundary checks independently inspect * wall time. Neither can preempt synchronous work. Resource limits count declared * bytes/items, not actual process memory. This creates no executor or background job. * @example * ```ts * const context = createOperationExecutionContext(identity, { budgetMs: 2000 }); * try { context.assertActive(); await inspect(context); } * finally { context.close(); } * ``` */ export declare function createOperationExecutionContext(identity: OperationExecutionIdentity, options?: OperationExecutionOptions): OperationExecutionContext; /** * Bind a newly claimed job fence without replacing the caller's lifetime or budget. * @param context - Original captured operation whose accounting remains authoritative. * @param writeFence - Persisted attempt reference to check at domain write boundaries. * @param allowCancelledOutcome - Permit an already cancelled scope solely for bounded outcome bookkeeping. * @returns Immutable scoped view sharing the original deadline, cancellation and accounting. * @throws Error if the scope is inactive, already fenced, or the reference is malformed. * @remarks This does not renew a lease or authorize mutation. Binding changes only * the immutable authority reference; closing either view invalidates both. The * outcome option requires an aborted signal and remaining deadline/lease time, * preserves the original failing domain guards, and does not prove persisted ownership. * @example * ```ts * const guarded = bindOperationWriteFence(context, fence); * await existingDomainOperation(guarded); * ``` */ export declare function bindOperationWriteFence(context: OperationExecutionContext, writeFence: BackgroundJobWriteFence, allowCancelledOutcome?: boolean): OperationExecutionContext; /** * Observe supplied work until it settles or the shared scope is cancelled. * @typeParam T - Supplied promise's result type. * @param context - Existing operation context; this does not start a new budget. * @param work - Already accepted work whose outcome is being observed. * @returns Actual settlement or an explicit unresolved observation, never fake cancellation. * @remarks The underlying promise may continue. Its rejection remains handled, * but preventing later writes requires guarded executor/chokepoint boundaries. * A synchronous overrun is reported after returning control, not called preemption. * @example * ```ts * const observed = await observeOperation(context, verification); * if (!observed.settled) reportPending(operationId); * ``` */ export declare function observeOperation(context: OperationExecutionContext, work: Promise): Promise>; /** * Reserve a worker stage in the original resource budget and link cancellation. * @param context - Origin-owned operation context. * @param usage - Complete declared stage admission charged before the transfer exists. * @returns Structured-cloneable scope and listener cleanup owned by the originating caller. * @remarks The shared flag is cooperative evidence, not synchronous preemption or * permission to write. Executors still validate domain fences at the commit boundary. * Release only after observing actual completion or explicitly reporting unresolved work. * @example * ```ts * const usage = { bytes: payloadBytes, items: 1 }; * const link = transferOperationContext(context, usage); * try { await existingWorker.enqueue(link.transfer); } finally { link.release(); } * ``` */ export declare function transferOperationContext(context: OperationExecutionContext, usage: OperationResourceUsage): OperationExecutionTransferHandle; /** * Receive an admitted worker scope without granting another resource budget. * @param transfer - Serialized identity/deadline plus the origin-owned cancellation flag. * @returns Cooperating worker context with no additional byte/item admission allowance. * @throws TypeError - Malformed shared cancellation storage or identity/deadline. * @remarks The origin must reserve the complete stage before sending it. Positive * receiver resource charges refuse rather than reset the original aggregate limits. * A busy worker observes cancellation when it next checks a boundary; a committed * result must still be reported as committed if cancellation arrives afterwards. * @example * ```ts * const context = receiveOperationContext(envelope.execution); * try { context.assertActive(); await existingHandler(context); } finally { context.close(); } * ``` */ export declare function receiveOperationContext(transfer: OperationExecutionTransfer): OperationExecutionContext; //# sourceMappingURL=background-ops.d.ts.map