Sign In

@ultimat3/core

Package Overview
Dependencies
Maintainers
1
Versions
17
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@ultimat3/core - npm Package Compare versions

Comparing version
11.1.0
to
11.2.0
+60
src/backoff.ts
// Single responsibility: how long to wait before the next attempt. ONE curve-and-jitter function
// for the whole framework, because four packages shipped four of them — `@ultimat3/jobs`
// (equal jitter), `@ultimat3/ai` (full jitter, `Math.random` inline and so untestable),
// `@ultimat3/realtime` (full jitter, 0-based attempt) and `@ultimat3/db` (no backoff at all).
export type BackoffCurve = 'exponential' | 'linear' | 'fixed';
/**
* `full` decorrelates a herd and is the only mode that does; `equal` keeps a latency floor for a
* client that must not be starved; `none` is for tests, for a printable schedule, and for nothing
* in production — a burst of failures retrying in lockstep is the thundering herd itself.
*/
export type JitterMode = 'full' | 'equal' | 'none';
/** Injected everywhere. A delay only provable by observing a range is a delay no test pins. */
export type Random = () => number;
export interface BackoffOptions {
/** 1-BASED: the wait after the first failure is `attempt: 1`. Below 1 is clamped to 1. */
readonly attempt: number;
/** The first delay, in ms. */
readonly base: number;
/** Ceiling for any single delay, in ms. Applied BEFORE jitter, never after. */
readonly max: number;
/** Default 2, and read by `exponential` only. */
readonly factor?: number | undefined;
readonly curve?: BackoffCurve | undefined;
/** Default `none`, so a caller that says nothing gets a schedule it can predict. */
readonly jitter?: JitterMode | undefined;
readonly random?: Random | undefined;
}
/**
* Milliseconds to wait before `attempt`, rounded to a whole ms and never negative.
*
* The clamp lands before the jitter deliberately: jittering first and capping after turns `full`
* into a distribution whose upper half is a single value at `max`, which is the correlation the
* jitter exists to remove.
*/
export function backoffDelay(options: BackoffOptions): number {
const step = Math.max(1, Math.trunc(options.attempt));
const base = Math.max(0, options.base);
const factor = options.factor ?? 2;
let raw: number;
if (options.curve === 'fixed') raw = base;
else if (options.curve === 'linear') raw = base * step;
else raw = base * factor ** (step - 1);
const capped = Math.max(0, Math.min(raw, options.max));
// A NaN reaches here from an unvalidated config value, and `setTimeout(NaN)` fires IMMEDIATELY —
// a retry loop with no wait at all, which is the failure mode backoff exists to prevent. 0 is
// wrong too, but it is wrong loudly: the next attempt still happens once, not in a tight spin.
if (!Number.isFinite(capped)) return 0;
const roll = options.random ?? Math.random;
if (options.jitter === 'full') return Math.round(capped * roll());
if (options.jitter === 'equal') return Math.round(capped / 2 + (capped / 2) * roll());
return Math.round(capped);
}
/**
* One typed-client call's flight control: supersession, dedup, retry, a deadline and a
* concurrency ceiling — composed from this package's own primitives, so no second fence, flight
* map, gate or backoff curve exists anywhere. Transport-agnostic: the caller supplies the
* dispatch, this file decides how many times it happens and whether the answer still counts.
*
* Tier 0 because `@ultimat3/action` and `@ultimat3/query` both project a typed client and are
* both tier 3, so neither may import the other. It shipped as a byte-identical 288-line copy in
* each; both re-export this one, so their public surface is unchanged and there is one file.
*
* Nothing here is imported by either package's `client.ts` at VALUE level. A caller that wants a
* plain typed fetch never mentions `createClientFlight`, so this module and every module it
* imports are shaken out of that caller's bundle — the 36 kB island problem is the reason it is
* built this way, and `packages/{action,query}/src/client.ts` must keep naming `ClientFlight` as
* an `import type`.
*/
import type { Random } from './backoff';
import { classifyThrown } from './error-retry';
import { UltimateError } from './errors';
import type { FlightGate, FlightGateLimits } from './flight-gate';
import { createFlightGate } from './flight-gate';
import { createFence } from './generation-fence';
import type { RetryPolicy } from './retry';
import { retry } from './retry';
import type { Scheduler } from './single-flight';
import { createSingleFlight } from './single-flight';
/** Overrides for the shipped policy. `attempts: 1` — the default — means one dispatch, no retry. */
export type ClientRetry = Partial<RetryPolicy>;
/**
* `attempts: 1` is NO retry, and that is the default deliberately: every existing caller of
* `rpc()` and `queryClient()` was written against exactly one dispatch, and a client that retries
* by default triples the load on a service on the day it can least afford it. Opt in per client
* or per call with `retry: { attempts: 3 }`; the curve, the ceiling and the jitter are then
* `backoffDelay`'s, never a second table.
*/
export const DEFAULT_CLIENT_RETRY: RetryPolicy = {
attempts: 1,
base: 100,
max: 2_000,
jitter: 'full',
curve: 'exponential',
};
/**
* What a client may send again: a classification somebody DECLARED, plus a dispatch that produced
* no response at all.
*
* The default is the OPPOSITE of `retryDecision`'s, one file over, and that is the point.
* `retry.ts` sends a throw nobody classified again until the attempts run out — which for a
* client means a caller's own `AbortError` and a `TypeError` out of a mapper both get retried.
* `@ultimat3/ai` and `@ultimat3/db` each refused that executor outright over it; this predicate
* keeps the executor and inverts the default instead, and is a parameter (`transient`) so an app
* can do neither.
*/
export function isTransientFailure(error: unknown): boolean {
const declared = classifyThrown(error);
if (declared !== undefined) return declared !== 'terminal';
return isNetworkRejection(error);
}
/**
* A dispatch that never produced a response. `fetch` rejects with a plain `TypeError` when the
* network is down, DNS fails or the connection drops mid-body — the one unclassified throw a
* client must send again. An abort is excluded because it is somebody's DECISION, not a failure:
* matched on `name` rather than `instanceof DOMException`, which is not constructible in every
* runtime this bundles into.
*/
function isNetworkRejection(error: unknown): boolean {
if (!(error instanceof Error)) return false;
if (error.name === 'AbortError' || error.name === 'TimeoutError') return false;
return error.name === 'TypeError';
}
export interface FlightKeyOptions {
readonly signal?: AbortSignal | undefined;
/** `true` refuses to join a dispatch that left before this call did. */
readonly fresh?: boolean | undefined;
}
export interface FlightPlan<T> {
/** The dedup key, or `undefined` for a call that may not share one. A mutation is always `undefined`. */
readonly key: string | undefined;
/** Whether `bump()` and the deadline may ABORT this work. A read yes; a write never. */
readonly abortable: boolean;
/** One attempt. `signal` is the flight's own — a caller's signal never reaches here. */
run(signal: AbortSignal | undefined, attempt: number): Promise<T>;
/** Overrides the flight's policy for this one call. */
readonly retry?: ClientRetry | undefined;
}
export interface ClientFlightOptions {
/**
* Who is asking, folded into every dedup key. Dedup is OFF without it, and that is enforced by
* `keyFor` answering `undefined`: a key that is only the URL lets one caller join another's
* still-open read across a sign-in, a tenant switch or an impersonation.
*/
readonly principal?: (() => string) | undefined;
readonly retry?: ClientRetry | undefined;
/**
* One wall-clock budget with three readers: the retry loop's `timeBudgetMs`, the abort that ends
* an abortable dispatch, and the eviction that frees a wedged dedup key. A NON-abortable plan —
* a write — is never aborted by it; it only stops being retried.
*/
readonly deadlineMs?: number | undefined;
/** A ceiling on calls in flight. Past the queue the answer is `X_FLIGHT_GATE_OVERLOADED`. */
readonly limit?: FlightGateLimits | undefined;
/** What the fence counts and what the gate names when it refuses. */
readonly subject?: string | undefined;
readonly transient?: ((error: unknown) => boolean) | undefined;
readonly sleep?: ((ms: number) => Promise<void>) | undefined;
readonly schedule?: Scheduler | undefined;
readonly random?: Random | undefined;
readonly now?: (() => number) | undefined;
}
export interface ClientFlight {
/** The generation work started NOW is issued at. */
generation(): number;
/** Everything issued before this call is superseded, and every abortable call is aborted. */
bump(): number;
/** The dedup key for one URL, or `undefined` when this call may not share a dispatch. */
keyFor(url: string, options?: FlightKeyOptions): string | undefined;
run<T>(plan: FlightPlan<T>): Promise<T>;
/** Dispatches holding a dedup key right now. A number that does not fall back to 0 is a leak. */
readonly inflight: number;
readonly active: number;
readonly queued: number;
}
/** Ends the retry loop by RESOLVING. Compared by identity and never observable to a caller. */
const STOPPED: unique symbol = Symbol('client-flight.stopped');
const defaultSchedule: Scheduler = (fn, ms) => {
const timer = setTimeout(fn, ms);
return (): void => {
clearTimeout(timer);
};
};
export function createClientFlight(options: ClientFlightOptions = {}): ClientFlight {
const subject = options.subject ?? 'a typed client call';
const fence = createFence(subject);
const schedule = options.schedule ?? defaultSchedule;
const sleep =
options.sleep ??
((ms: number): Promise<void> =>
new Promise<void>((done) => {
schedule(done, ms);
}));
const transient = options.transient ?? isTransientFailure;
const deadlineMs = options.deadlineMs;
const flights = createSingleFlight({ deadlineMs, schedule });
const gate: FlightGate | undefined =
options.limit === undefined ? undefined : createFlightGate(options.limit, { subject });
const live = new Set<AbortController>();
const attempt = async <T>(plan: FlightPlan<T>, signal: AbortSignal | undefined): Promise<T> => {
const policy: RetryPolicy = {
...DEFAULT_CLIENT_RETRY,
...options.retry,
...plan.retry,
...(deadlineMs === undefined ? {} : { timeBudgetMs: deadlineMs }),
};
// A non-transient failure ends the loop by RESOLVING to a sentinel rather than by throwing:
// core's `retryDecision` retries anything nobody classified, so a throw would send an abort
// and a foreign `TypeError` again. The original value is rethrown below, unwrapped — wrapping
// it would replace a code, a cause and a runnable `fix:` with the fact that something retried.
let stopped: { readonly error: unknown } | undefined;
const answer = await retry<T | typeof STOPPED>(
async (count) => {
try {
return await plan.run(signal, count);
} catch (error) {
if (transient(error)) throw error;
stopped = { error };
return STOPPED;
}
},
policy,
{
sleep,
...(options.random === undefined ? {} : { random: options.random }),
...(options.now === undefined ? {} : { now: options.now }),
},
);
if (stopped !== undefined) throw stopped.error;
return answer as T;
};
const dispatch = async <T>(plan: FlightPlan<T>): Promise<T> => {
const controller = plan.abortable ? new AbortController() : undefined;
let expired: number | undefined;
const cancel =
controller === undefined || deadlineMs === undefined
? undefined
: schedule(() => {
expired = deadlineMs;
controller.abort();
}, deadlineMs);
if (controller !== undefined) live.add(controller);
try {
return await attempt(plan, controller?.signal);
} catch (error) {
// Raised here rather than at the caller, so every joiner of a deduped read is told the same
// thing: the leader's abort reaches them as a bare `AbortError` otherwise.
if (expired !== undefined) throw deadlineExpired(subject, expired);
throw error;
} finally {
cancel?.();
if (controller !== undefined) live.delete(controller);
}
};
const settle = async <T>(shared: Promise<T>, issued: number): Promise<T> => {
try {
const value = await shared;
fence.guard(issued);
return value;
} catch (error) {
// The guard runs on the failure path too: a bump supersedes a call's refusal exactly as
// much as its answer, and a caller that cannot tell the two apart retries a request its own
// context has already replaced.
fence.guard(issued);
throw error;
}
};
return {
generation: (): number => fence.generation(),
bump: (): number => {
const next = fence.bump();
// Abortable plans only: `live` never holds a write's controller, because closing a
// mutation's socket does not un-commit it — it only destroys this caller's one chance of
// learning whether it landed.
const aborting = [...live];
live.clear();
for (const controller of aborting) controller.abort();
return next;
},
keyFor: (url: string, keyOptions?: FlightKeyOptions): string | undefined => {
if (options.principal === undefined) return undefined;
// A caller's own signal disqualifies the call from sharing, in ONE line rather than by
// refcounting joiners: the leader owns the request, so one caller's abort would cancel every
// other caller's read. The cost is that an explicitly cancellable call does its own dispatch.
if (keyOptions?.signal !== undefined) return undefined;
if (keyOptions?.fresh === true) return undefined;
// JSON, never a joined string — a principal is app data and may carry the separator, the
// reason `@ultimat3/entity`'s `scopeKey` gives for the same shape.
return JSON.stringify([options.principal(), url]);
},
run<T>(plan: FlightPlan<T>): Promise<T> {
const issued = fence.generation();
const work = (): Promise<T> =>
gate === undefined ? dispatch(plan) : gate.run(() => dispatch(plan));
// The single flight sits OUTSIDE the gate: a joiner takes no slot, so dedup relieves the
// ceiling instead of queueing behind it.
return settle(plan.key === undefined ? work() : flights.run(plan.key, work), issued);
},
get inflight(): number {
return flights.size;
},
get active(): number {
return gate?.active ?? 0;
},
get queued(): number {
return gate?.queued ?? 0;
},
};
}
/**
* `X_TIMEOUT`, never a code of the calling package's: the deadline is the framework's own
* vocabulary for "nothing was wrong, the budget ran out", and it is already classified `retryable`
* in `error-retry.ts`, so a caller's own retry loop reads the right answer off `error.retry` with
* no table to consult.
*/
function deadlineExpired(subject: string, deadlineMs: number): UltimateError {
return new UltimateError({
code: 'X_TIMEOUT',
cause: `${subject} was aborted after its client deadline of ${deadlineMs}ms`,
fix: 'raise deadlineMs at the createClientFlight({ deadlineMs }) call site, or find what is answering that slowly with x doctor --json',
meta: { subject, deadlineMs },
});
}
/**
* What a typed client puts on the wire and reads back off it: the W3C trace header, the
* problem+json body, and the immutable answer one dispatch hands to every caller sharing it.
*
* Tier 0 because `@ultimat3/action` and `@ultimat3/query` need this identical file and are both
* tier 3, so neither may import the other — the shape `canonical-json.ts` is already here for.
* It shipped as a byte-identical copy in each, policed by a `client-twin.test.ts` in both; a test
* that makes drift LOUD is not the same as a file that cannot drift.
*/
import type { ErrorRetry } from './error-retry';
import { declaredErrorRetry } from './error-retry';
import { isJsonObject } from './json-object';
import { isRetryableStatus } from './retryable-status';
import { currentSpanContext, traceparent } from './telemetry';
/**
* One dispatch's answer, and deliberately not a `Response`: a deduped read hands its answer to
* every joiner, so what they share is the immutable TEXT and each parses its own object. A
* `Response` carries a single-use stream, and a parsed body is a mutable object two callers would
* then be holding one of.
*/
export interface WireAnswer {
readonly status: number;
readonly text: string;
}
/** A `traceparent` is `00-<32 hex>-<16 hex>-<2 hex>`, and nothing else may be sent as one. */
const TRACE_ID = /^[0-9a-f]{32}$/;
const SPAN_ID = /^[0-9a-f]{16}$/;
/**
* The current trace, as the W3C header — or nothing at all. `currentSpanContext()` answers with
* an empty `spanId` when a request context exists but no span is active, and `00-<trace>--01` is
* a header every collector drops, so an incomplete context sends none. In a browser there is no
* ambient context and this is always empty, which is also what keeps a cross-origin call from
* acquiring a CORS preflight it did not have.
*/
export function traceHeaders(): Record<string, string> {
const context = currentSpanContext();
if (context === undefined) return {};
if (!TRACE_ID.test(context.traceId) || !SPAN_ID.test(context.spanId)) return {};
return { traceparent: traceparent(context) };
}
/**
* A framework code, spelled the one way codes are spelled. `typeof code === 'string'` alone
* accepted `""` and `"error"` — a gateway's JSON body became an `UltimateError` whose code
* nothing in the framework or the app declares, rendering `: ` under a humanised title.
*/
export const FRAMEWORK_CODE = /^X_[A-Z0-9]+(?:_[A-Z0-9]+)*$/;
/**
* `application/problem+json`, or nothing when a proxy answered instead of the app. Total by
* construction: a gateway's HTML, an empty body and a truncated stream are all "no problem here",
* never a `SyntaxError` thrown out of the failure path.
*/
export function problemOf(text: string): Record<string, unknown> {
let body: unknown;
try {
body = JSON.parse(text) as unknown;
} catch {
return {};
}
return isJsonObject(body) ? body : {};
}
/**
* The classification a failure off the wire carries, or `undefined` to leave the code's own
* standing. The STATUS decides only when nobody has declared one for the code: a 503 is the
* canonical "send it again", but `X_NOT_IMPLEMENTED` behind a 501 and a config fault behind a 500
* are permanent answers somebody already gave, and a status that overrode them would have a client
* hammer a service that will refuse it identically forever.
*
* `UltimateError` fills `retry` from `retryFor(code)` otherwise, which fails closed to `terminal` —
* so before this every 502 out of a typed client read as "never try again", on the one field the
* framework promises a client never has to infer.
*/
export function retryForStatus(code: string, status: number): ErrorRetry | undefined {
if (declaredErrorRetry(code) !== undefined) return undefined;
return isRetryableStatus(status) ? 'retryable' : undefined;
}
// Single responsibility: how many of one kind of work may run at once, and how many callers may
// wait. THREE bounded pools shipped before this one — `@ultimat3/auth`'s kdf gate, `@ultimat3/ai`'s
// hive pool, `@ultimat3/http`'s `maxInflight` — and only the first refuses past its queue. This
// header counted `@ultimat3/scraping`'s pacer as a fourth until 2026-08-23 and it is not one: a
// pacer bounds how OFTEN work starts, not how many run at once, so ten concurrent navigations all
// proceed merely staggered — no `maxConcurrent`, no queue bound, no refusal. Past the bound the
// answer here is a refusal, never a longer queue: an unbounded queue converts a load spike into a
// memory fault and answers it minutes late.
import { UltimateError } from './errors';
export interface FlightGateLimits {
/** Work running at once. */
readonly maxConcurrent: number;
/** Callers allowed to WAIT for a slot. Past this the answer is a refusal, not a longer queue. */
readonly maxQueued: number;
}
export interface FlightGateState extends FlightGateLimits {
readonly active: number;
readonly queued: number;
readonly subject: string;
}
export interface FlightGateOptions {
/** What is bounded, for the refusal's `cause`. */
readonly subject?: string | undefined;
/**
* The refusal to raise instead of `X_FLIGHT_GATE_OVERLOADED`. The seam that lets a package keep
* its own shipped code while delegating the mechanism — `@ultimat3/auth`'s `kdfOverloaded` is
* `X_OVERLOADED` and a client already reads it as a 503.
*/
readonly overflow?: ((state: FlightGateState) => UltimateError) | undefined;
}
export interface FlightGate {
run<T>(work: () => Promise<T>): Promise<T>;
/** Running right now. */
readonly active: number;
/** Waiting for a slot right now. A count that does not fall back to 0 is a leak. */
readonly queued: number;
}
/**
* A slot is HANDED OVER on release rather than released and re-acquired: decrementing first would
* let a caller arriving in the same tick past the ceiling while a waiter's continuation is still a
* queued microtask, which is how a "bounded" pool goes over its bound under exactly the load it
* exists for. `@ultimat3/auth`'s `createKdfGate` states the same rule; this is that function with
* the refusal made injectable.
*/
export function createFlightGate(
limits: FlightGateLimits,
options?: FlightGateOptions,
): FlightGate {
const subject = options?.subject ?? 'in-flight work';
const waiters: Array<() => void> = [];
let active = 0;
const state = (): FlightGateState => ({
maxConcurrent: limits.maxConcurrent,
maxQueued: limits.maxQueued,
active,
queued: waiters.length,
subject,
});
const acquire = async (): Promise<void> => {
if (active < limits.maxConcurrent) {
active += 1;
return;
}
if (waiters.length >= limits.maxQueued) {
const current = state();
throw options?.overflow?.(current) ?? gateOverloaded(current);
}
await new Promise<void>((resume) => {
waiters.push(resume);
});
};
const release = (): void => {
const next = waiters.shift();
if (next === undefined) active -= 1;
else next();
};
return {
get active(): number {
return active;
},
get queued(): number {
return waiters.length;
},
async run<T>(work: () => Promise<T>): Promise<T> {
await acquire();
try {
return await work();
} finally {
release();
}
},
};
}
/**
* `retryAfterSeconds: 1` is the same value and the same field `@ultimat3/auth`'s `kdfOverloaded`
* carries — `@ultimat3/http`'s `retryAfterOf` reads exactly this key onto the `Retry-After`
* header, so a gate refusal and a rate limit answer a client the same way. One second, because a
* gate at its ceiling clears in the time one unit of work takes, not in a minute.
*/
export function gateOverloaded(state: FlightGateState): UltimateError {
return new UltimateError({
code: 'X_FLIGHT_GATE_OVERLOADED',
cause: `${state.active} of ${state.subject} are running at the ceiling of ${state.maxConcurrent} and ${state.queued} more are queued at the limit of ${state.maxQueued}`,
fix: 'retry after the Retry-After header, or widen the ceiling at the createFlightGate({ maxConcurrent, maxQueued }) call site — only if the box has the capacity the extra slots buy',
meta: {
active: state.active,
queued: state.queued,
maxConcurrent: state.maxConcurrent,
maxQueued: state.maxQueued,
subject: state.subject,
retryAfterSeconds: 1,
},
});
}
// Single responsibility: refusing a late answer whose world has moved on. A monotonic counter, the
// generation a caller was issued when it started, and one guard between the two — the piece no
// package in the framework had, and the reason a cancelled reload's response could still land on
// top of the one that replaced it.
import { isUltimateError, UltimateError } from './errors';
export interface GenerationFence {
/** The generation to carry alongside work started NOW. */
generation(): number;
/** Everything issued before this call is superseded. Returns the new generation. */
bump(): number;
/** Throws `X_SUPERSEDED` unless `issued` is still the current generation. */
guard(issued: number): void;
}
/**
* `subject` names what the generation counts, and it reaches the `cause:` — "the live window was
* superseded" is actionable where "generation 3 != 4" is a puzzle.
*/
export function createFence(subject: string): GenerationFence {
let current = 0;
return {
generation: (): number => current,
bump: (): number => {
current += 1;
return current;
},
guard: (issued: number): void => {
// `!==`, never `<`. A generation from the future cannot happen and therefore means the
// caller carried a token from ANOTHER fence — the one case where letting the work land is
// strictly worse than refusing it, because nothing about it was ever fenced.
if (issued === current) return;
throw superseded(subject, issued, current);
},
};
}
/**
* `X_SUPERSEDED` is classified `terminal` in `error-retry.ts`'s core table rather than through a
* `registerErrorRetry()` call here: a module-scope registration is an import-time side effect this
* package would have to declare in `sideEffects`, and `resetErrorRetry()` in any test would drop
* it. The classification is load-bearing — see that table for why an undeclared one is not enough.
*/
function superseded(subject: string, issued: number, current: number): UltimateError {
return new UltimateError({
code: 'X_SUPERSEDED',
cause: `${subject} was issued at generation ${issued} and the fence is now at ${current}, so this answer describes a world that no longer exists`,
fix: 'discard this answer and re-issue the work against fence.generation() — whoever called bump() has already started the replacement',
meta: { subject, issued, current },
});
}
/** Whether a caught value is this refusal. The one reader a caller needs; never `error.code`. */
export function isSuperseded(error: unknown): boolean {
return isUltimateError(error) && error.code === 'X_SUPERSEDED';
}
/**
* What a JSON object IS to this framework: the one predicate that narrows an `unknown` to a keyed
* record. Tier 0 because `@ultimat3/action` and `@ultimat3/query` each declared an identical copy
* in their own `stable.ts`, and the client wire path that needed it is core's now.
*/
/**
* `typeof null === 'object'` and an array is an object, so both are excluded by hand.
*
* A `Date`, a `Map` and a class instance all PASS: this narrows a SHAPE, it does not certify
* provenance. A caller that means "came out of `JSON.parse`" gets that from having called
* `JSON.parse` itself — widening this to reject them would make it a second, quieter validator,
* and the framework's validator is `@ultimat3/schema`.
*/
export function isJsonObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
// Single responsibility: run work again, on the framework's own terms. The executor
// `error-retry.ts` never had — that module declares `terminal | retryable | retry-after` and
// nothing in the tree consulted it before deciding to try again, so four packages each shipped
// their own loop and only `@ultimat3/jobs`' asked the classification at all.
import { type BackoffCurve, backoffDelay, type JitterMode, type Random } from './backoff';
import { systemClock } from './clock';
import { classifyThrown, type ErrorRetry, statedDelayMs } from './error-retry';
export interface RetryPolicy {
/** Total attempts INCLUDING the first. `attempts: 1` means no retry. */
readonly attempts: number;
/** The first delay, in ms. */
readonly base: number;
/** Ceiling for any single delay, in ms — a `retry-after` the responder named included. */
readonly max: number;
/**
* Required, unlike `backoffDelay`'s, and that is the point: a retry loop with no jitter is the
* thundering herd itself, so the mode is a decision each caller makes rather than one it can
* inherit without noticing.
*/
readonly jitter: JitterMode;
readonly curve?: BackoffCurve | undefined;
readonly factor?: number | undefined;
/**
* Wall-clock budget for the WHOLE loop, waits included. A wait that would end past it is never
* started — the caller's own deadline outranks the attempts it was allowed.
*/
readonly timeBudgetMs?: number | undefined;
}
export interface RetryDeps {
/** Injected so a retry schedule is provable without waiting for one. */
sleep(ms: number): Promise<void>;
/** Monotonic ms, read only when `timeBudgetMs` is set. Defaults to the system clock. */
now?: (() => number) | undefined;
random?: Random | undefined;
}
/** Why the loop stopped. Absent while it is still retrying. */
export type RetryStopReason = 'terminal' | 'attempts-exhausted' | 'budget-exhausted';
export interface RetryDecision {
readonly retry: boolean;
readonly delayMs: number;
readonly attempt: number;
readonly nextAttempt: number;
/** The classification consulted, or `undefined` when nobody classified the thrown code. */
readonly classification: ErrorRetry | undefined;
readonly stoppedBy: RetryStopReason | undefined;
}
/**
* Retry, stop, and when — pure, so a caller can print the schedule and a test can pin it without a
* loop. `terminal` stops on the attempt that failed: the same code run again is the same answer,
* and the attempts left are a queue slot, a provider bill, or three more wrong passwords at a site
* that locks the account after three. Everything else keeps the attempt count in charge, and
* `retry-after` replaces only the DELAY, never the ceiling.
*/
export function retryDecision(
policy: RetryPolicy,
attempt: number,
error: unknown,
random?: Random,
): RetryDecision {
const classification = classifyThrown(error);
const stop = (stoppedBy: RetryStopReason): RetryDecision => ({
retry: false,
delayMs: 0,
attempt,
nextAttempt: attempt,
classification,
stoppedBy,
});
if (classification === 'terminal') return stop('terminal');
if (attempt >= policy.attempts) return stop('attempts-exhausted');
const computed = backoffDelay({
attempt,
base: policy.base,
max: policy.max,
factor: policy.factor,
curve: policy.curve,
jitter: policy.jitter,
random,
});
const stated = classification === 'retry-after' ? statedDelayMs(error) : undefined;
return {
retry: true,
// Clamped by the policy's own ceiling, which is what `max` is for: a responder naming a day is
// still a responder this deployment has not agreed to wait a day for.
delayMs: stated === undefined ? computed : Math.min(stated, policy.max),
attempt,
nextAttempt: attempt + 1,
classification,
stoppedBy: undefined,
};
}
/**
* Run `work` until it succeeds, the policy runs out, the classification says stop, or the time
* budget does. `work` is handed the 1-based attempt number.
*
* The LAST error reaches the caller unchanged — never wrapped. An `UltimateError` carries a code, a
* cause and a runnable `fix:`, and a wrapper would replace all three with the fact that something
* was retried, which no reader can act on.
*/
export async function retry<T>(
work: (attempt: number) => Promise<T>,
policy: RetryPolicy,
deps: RetryDeps,
): Promise<T> {
const now = deps.now ?? ((): number => systemClock.monotonic());
// Read once even when no budget is set: a clock call per attempt would be a cost the common case
// does not owe. `startedAt` is only compared against when `timeBudgetMs` is present.
const startedAt = now();
for (let attempt = 1; ; attempt += 1) {
try {
return await work(attempt);
} catch (error) {
const decision = retryDecision(policy, attempt, error, deps.random);
if (!decision.retry) throw error;
const budget = policy.timeBudgetMs;
// Decided BEFORE the wait, never after: a loop that sleeps and then discovers it is out of
// budget has already spent the caller's deadline on a wait nobody could use.
if (budget !== undefined && now() - startedAt + decision.delayMs > budget) throw error;
await deps.sleep(decision.delayMs);
}
}
}
// Single responsibility: is an HTTP status worth sending the same request again? One table, because
// `packages/cache/src/purge-http.ts` and `packages/mail/src/driver-resend.ts` shipped the SAME line
// byte for byte in two packages that cannot import each other, and `@ultimat3/ai`'s gateway shipped
// a third, narrower answer (`429 || >= 500`) for the same question.
/**
* The 4xx that can succeed unchanged. Everything else in the 4xx range is the request's own fault
* and retrying it only burns the budget.
*
* | Status | Why a retry can win |
* |---|---|
* | 408 Request Timeout | the server gave up waiting for a body it never fully read; nothing about the request was refused |
* | 409 Conflict | a concurrent writer won this round — the same write can land once theirs settles |
* | 425 Too Early | the server refused to risk replay of early data; the same request is fine on a completed handshake |
* | 429 Too Many Requests | a throttle, and the one 4xx that names WHEN in `Retry-After` |
*/
export const RETRYABLE_STATUSES: ReadonlySet<number> = new Set([408, 409, 425, 429]);
/**
* `>= 500` is deliberately whole rather than a list. A 501 and a 505 are permanent and a retry
* wastes one attempt on them; a 500/502/503/504 is the transient case a retry exists for, and
* splitting the range is a table that must be right about every future status a proxy invents.
* Both shipped copies drew the line here, so this is their behaviour and not a new one.
*/
export function isRetryableStatus(status: number): boolean {
return status >= 500 || RETRYABLE_STATUSES.has(status);
}
// Single responsibility: N concurrent callers on one key are ONE run of the work. Tier 0 because
// four packages needed it and only one had it — `@ultimat3/cache`'s `single-flight.ts` (this
// function's shape, verbatim), `@ultimat3/realtime`'s `entry.reading`, `@ultimat3/auth`'s
// hand-rolled `inflight ??=` in `jwks.ts`, and `@ultimat3/query`'s per-request memo.
/**
* What a joiner contributes to the load it joined. Without one a joiner is a free rider: it takes
* the leader's value AND the leader's write, so anything it declared about that write is dropped.
*/
export interface FlightJoin<C> {
readonly context: C;
/** Folds a joiner in. Called synchronously as it arrives, so the leader sees it before it writes. */
readonly merge: (current: C, joining: C) => C;
}
/** Returns its own canceller rather than a handle, so no type differs across Bun and the browser. */
export type Scheduler = (fn: () => void, ms: number) => () => void;
export interface SingleFlightOptions {
/**
* How long a key may be held by one load. Optional, and off by default: a load with no deadline
* that never settles holds its key for the life of the process, and every later caller joins a
* promise that will never resolve. Eviction frees the KEY — the work itself is not cancellable
* from here, and pretending otherwise would be a second, false promise.
*/
readonly deadlineMs?: number | undefined;
/** Injected so a deadline is provable without waiting for one. */
readonly schedule?: Scheduler | undefined;
}
export interface SingleFlight {
/**
* `work` receives a reader for the merged context — read it LATE (after the load settles), or
* it answers with only what the leader brought.
*/
run<T, C = undefined>(
key: string,
work: (shared: () => C | undefined) => Promise<T>,
join?: FlightJoin<C>,
): Promise<T>;
/** In-flight loads right now. A number that does not fall back to `0` is a leak. */
readonly size: number;
}
/** The leader's promise, the box its merged context lives in, and its deadline's canceller. */
interface Flight {
readonly running: Promise<unknown>;
readonly shared: { context: unknown };
cancelDeadline: () => void;
}
const defaultScheduler: Scheduler = (fn, ms) => {
const timer = setTimeout(fn, ms);
return (): void => {
clearTimeout(timer);
};
};
export function createSingleFlight(options?: SingleFlightOptions): SingleFlight {
const inflight = new Map<string, Flight>();
const schedule = options?.schedule ?? defaultScheduler;
const deadlineMs = options?.deadlineMs;
// Identity, never key presence. `inflight.delete(key)` from a settling load drops whatever holds
// the key NOW — which, once a deadline can evict, is a different load that has not settled: its
// joiners would then be sharing a promise nothing in the map answers for.
const evict = (key: string, entry: Flight): void => {
if (inflight.get(key) === entry) inflight.delete(key);
};
return {
get size(): number {
return inflight.size;
},
run<T, C = undefined>(
key: string,
work: (shared: () => C | undefined) => Promise<T>,
join?: FlightJoin<C>,
): Promise<T> {
const joined = inflight.get(key);
// Two readers of one key asking for two different `T` is a caller bug this cannot see; the
// value they share is the same object either way, so the cast is the honest one.
if (joined !== undefined) {
if (join !== undefined) {
joined.shared.context = join.merge(joined.shared.context as C, join.context);
}
return joined.running as Promise<T>;
}
const shared: { context: unknown } = { context: join?.context };
// Wrapped so a `work()` that throws SYNCHRONOUSLY still rejects the joiners rather than
// escaping past the map and leaving no entry to clear.
const running: Promise<T> = (async () => await work(() => shared.context as C | undefined))();
const entry: Flight = { running, shared, cancelDeadline: (): void => undefined };
inflight.set(key, entry);
if (deadlineMs !== undefined) {
entry.cancelDeadline = schedule(() => {
evict(key, entry);
}, deadlineMs);
}
const settled = (): void => {
entry.cancelDeadline();
evict(key, entry);
};
// A rejected load MUST clear too, or one failure is cached as a permanent rejection.
void running.then(settled, settled);
return running;
},
};
}
+51
-0

@@ -119,2 +119,9 @@ # @ultimat3/core — agent notes

| loading `.env` | **Bun**, not us | `envFileCandidates()` documents the measured order; there is no `.env.staging` |
| how long to wait, and whether to wait at all | `backoff.ts` + `retry.ts` | one curve and one executor; `jitter` is REQUIRED on a retry policy and defaults to `none` on the arithmetic |
| N callers on one key | `single-flight.ts` | identity-checked eviction, optional injected deadline; `@ultimat3/cache`'s is this shape |
| how many at once | `flight-gate.ts` | hand-over on release, refusal past `maxQueued`, injectable `overflow:` refusal |
| whether an answer still applies | `generation-fence.ts` | `X_SUPERSEDED` / `isSuperseded`; nothing else in the tree had one |
| which HTTP statuses are worth repeating | `retryable-status.ts` | `>= 500` plus 408, 409, 425, 429 — the two byte-identical copies' set |
| the five above, composed into one typed-client call | `client-flight.ts` + `client-wire.ts` | `@ultimat3/action` and `@ultimat3/query` both project a typed client and are both tier 3, so neither could import the other: it shipped as a byte-identical 288-line + 85-line copy in each, policed by a `client-twin.test.ts` in both. Both packages re-export these names, so their public surface is unchanged. Declares NO code of its own — `X_SUPERSEDED`, `X_TIMEOUT` and `X_FLIGHT_GATE_OVERLOADED` are already here |
| is this `unknown` a keyed record? | `json-object.ts` | `isJsonObject`, which was the same three terms in both those packages' `stable.ts`. A `Date` and a class instance PASS: it narrows a shape, it does not certify provenance |
| a value that must not be printed | `secret.ts` | redacted by VALUE; `revealSecret()` is the one way out, on purpose greppable |

@@ -197,2 +204,46 @@ | an `Intl` formatter cache | `intl-cache.ts` (`cachedFormatter`, `canonicalLocale`, `MAX_CACHED_FORMATTERS`) | a locale and a zone arrive from a request header, so the key must be canonical AND the cache bounded — never a second copy of either half |

The **flight layer** is the same rule over control flow rather than over a value, `As of
2026-08-23`: `backoff.ts`, `retry.ts`, `single-flight.ts`, `flight-gate.ts`, `generation-fence.ts`
and `retryable-status.ts` — plus `client-flight.ts` and `client-wire.ts`, which compose them into
one typed-client call and arrived the same way the layer itself did, as two identical copies in two
packages that may not import each other. Measured before it existed — FOUR backoff curves
(`@ultimat3/jobs` equal-jitter, `@ultimat3/ai` full-jitter with `Math.random` inline and therefore
untestable, `@ultimat3/realtime` 0-based-attempt full-jitter, `@ultimat3/db` none at all), FIVE
retryability tables of which `packages/cache/src/purge-http.ts:19` and
`packages/mail/src/driver-resend.ts:27` were byte-identical in two packages that cannot import
each other, FOUR bounded pools and FOUR dedupers. `error-retry.ts` had declared the vocabulary and
**nothing consulted it before retrying**; `retry()` is the executor it never had, and
`classifyThrown` / `statedDelayMs` moved down here beside the table they read (`@ultimat3/jobs`'
`retry-classification.ts` is that pair one tier up and can delegate to it unchanged). Nothing in
the layer imports anything but this package, nothing runs at import time, and every source of
non-determinism — the roll, the sleep, the clock, the timer — is injected, because a schedule
provable only by waiting is a schedule no test pins.
Two rules that are not preferences. `backoffDelay` clamps to `max` **before** jitter: capping after
turns `full` into a distribution whose upper half is a single value at `max`, which is the
correlation jitter exists to remove. `createFlightGate` **hands** its slot to a waiter rather than
releasing it: decrementing first lets a caller arriving in the same tick past the ceiling while the
waiter's continuation is still a queued microtask — `@ultimat3/auth`'s `createKdfGate` states the
same rule, and `overflow:` is the seam that lets it keep throwing its own `X_OVERLOADED` while
delegating the mechanism. `X_FLIGHT_GATE_OVERLOADED` is core's own code and not auth's borrowed
one: `X_OVERLOADED` belongs to `@ultimat3/http` (tier 2), and tier 0 may not borrow upward.
**`client-flight.ts` INVERTS `retryDecision`'s unclassified default, and that inversion is the
point of it having a `transient:` parameter at all.** `retry.ts` sends a throw nobody classified
again until the attempts run out, which is right for a job and wrong for a client: `fetch` rejects
with a bare `TypeError` for a dead network and a `DOMException` named `AbortError` for the caller's
own cancellation, and nothing can tell them apart from the class alone — so inheriting the default
retries a caller's own abort. `@ultimat3/ai` and `@ultimat3/db` each declined the executor outright
over it; this is the third refusal, and the one that keeps the executor by supplying a predicate.
Never "simplify" it back to the default.
**`ClientFlight` must stay `import type`-only in both packages' `client.ts`, and that erasure is
the whole tree-shaking story.** `import { rpc } from '@ultimat3/action'` is 14,759 B minified for
the browser and 20,292 B with `createClientFlight` beside it; `queryClient` is 12,755 B against
17,912 B. A value import from `client.ts` would put `retry.ts`, `single-flight.ts`,
`generation-fence.ts`, `flight-gate.ts` and `backoff.ts` into every caller's chunk. Measure through
the PUBLIC specifier — `Bun.build`, `target: 'browser'`, `minify: true` — and expect ±376 B run to
run: `Bun.build` 1.4.0 drops this package's `schema-error-codes.ts` from some builds even though
`sideEffects` names it (issue #273), which is exactly the size of the schema error titles.
`mcp-exposure.ts` is the same shape for a declaration rather than an algorithm: `isMcpExposed` is

@@ -199,0 +250,0 @@ the ONE answer to "did this primitive opt into being an MCP tool?", asked by `action`, `query`

+1
-1
{
"name": "@ultimat3/core",
"version": "11.1.0",
"version": "11.2.0",
"description": "Ultimate's foundation: errors, context, env, config, clock, ids, logging, telemetry, lifecycle",

@@ -5,0 +5,0 @@ "license": "MIT",

@@ -17,2 +17,11 @@ # 🧱 @ultimat3/core

| is an error worth retrying? one classification per code | `error-retry.ts` |
| how long to wait before the next attempt — one curve, one jitter table | `backoff.ts` |
| the retry executor and the pure decision behind it | `retry.ts` |
| which HTTP statuses are worth repeating | `retryable-status.ts` |
| N callers on one key are ONE run | `single-flight.ts` |
| how many may run at once, and how many may wait | `flight-gate.ts` |
| whether an answer still applies — `X_SUPERSEDED` | `generation-fence.ts` |
| the five composed into one typed-client call — dedup, fence, retry, deadline, ceiling | `client-flight.ts` |
| what a typed client puts on the wire and reads back off it | `client-wire.ts` |
| is this `unknown` a keyed record? | `json-object.ts` |
| typed env validated at boot | `env.ts` |

@@ -477,2 +486,65 @@ | `.env.example` rendered from that schema, and its drift check | `env-example.ts` |

## One flight layer — wait, classify, share, bound, fence
```ts
import {
backoffDelay,
createFence,
createFlightGate,
createSingleFlight,
isRetryableStatus,
} from '@ultimat3/core';
// One ceiling on work whose cost is memory, one run per key, one fence over the answer.
const gate = createFlightGate({ maxConcurrent: 8, maxQueued: 64 }, { subject: 'jwks fetches' });
const flights = createSingleFlight({ deadlineMs: 30_000 });
const fence = createFence('the jwks cache');
export async function jwks(url: string): Promise<Response> {
const issued = fence.generation();
const answer = await gate.run(
async () =>
await flights.run(url, async () => {
const first = await fetch(url);
if (!isRetryableStatus(first.status)) return first;
const waitMs = backoffDelay({ attempt: 1, base: 500, max: 8_000, jitter: 'full' });
await new Promise<void>((wake) => {
setTimeout(wake, waitMs);
});
return await fetch(url);
}),
);
// X_SUPERSEDED if anything called bump() while the fetch was in flight.
fence.guard(issued);
return answer;
}
```
**Eight modules, one tier-0 home** — because the framework had N copies of each and no owner:
four backoff curves (`@ultimat3/jobs`, `@ultimat3/ai`, `@ultimat3/realtime`, and `@ultimat3/db`
with none at all), five retryability tables — two of them byte-identical in packages that cannot
import each other — and three separate concurrency bounds, only one of which refused past its
queue. `error-retry.ts` declared the vocabulary (`terminal | retryable | retry-after`) and
nothing consulted it before deciding to try again. `As of 2026-08-23`.
| Export | The one answer | The question it settles |
|---|---|---|
| `backoffDelay({ attempt, base, max, factor?, curve?, jitter?, random? })` | one curve — `exponential \| linear \| fixed`, `full \| equal \| none` — 1-based `attempt`, clamped to `max` **before** jitter, rounded, and `0` rather than `NaN` | how long to wait. `random` is injectable, so a schedule is a unit test rather than a range |
| `createSingleFlight({ deadlineMs?, schedule? })` → `run(key, work, join?)`, `size` | N callers on one key are ONE run | who pays for a miss. Eviction is identity-checked, so a load that settles late never drops the load that replaced it; `deadlineMs` frees the KEY a wedged load would hold forever — it never cancels the work and never rejects a joiner |
| `createFlightGate({ maxConcurrent, maxQueued }, { subject?, overflow? })` | one bound, one queue, one refusal | how many at once. Past the queue the answer is `X_FLIGHT_GATE_OVERLOADED` (503) and never a longer queue; the slot is HANDED to a waiter, never released and re-acquired |
| `createFence(subject)` → `generation()`, `bump()`, `guard(issued)` | whether an answer still applies | `X_SUPERSEDED` (499) and `isSuperseded(error)` — the piece nothing in the tree had. `guard` compares `!==`, never `<` |
| `isRetryableStatus(status)`, `RETRYABLE_STATUSES` | `>= 500`, plus 408, 409, 425, 429 | which HTTP answers are worth repeating |
| `retry(work, policy, { sleep, now?, random? })`, `retryDecision(policy, attempt, error, random?)` | the executor and the pure decision behind the classification | whether to try again at all. `createClientFlight` is its one caller in the framework; `jobs`, `ai` and `db` each keep their own loop and delegate only the arithmetic and the classification |
| `createClientFlight({ principal?, retry?, deadlineMs?, limit?, … })` → `run(plan)`, `keyFor(url, opts?)`, `bump()`, `generation()` | the five above composed into one typed-client call | dedup, supersession, retry, one wall-clock deadline and a concurrency ceiling, for a call whose dispatch the caller supplies. `@ultimat3/action` and `@ultimat3/query` re-export it verbatim — it is one file because both are tier 3 and neither may import the other |
| `isTransientFailure(error)` | what a CLIENT may send again | a declared `retryable`/`retry-after`, plus a dispatch that produced no response at all. It **inverts** `retryDecision`'s unclassified default on purpose: a caller's own `AbortError` and a foreign `TypeError` are terminal |
| `traceHeaders()`, `problemOf(text)`, `retryForStatus(code, status)`, `FRAMEWORK_CODE` | what a typed client puts on the wire and reads back off it | the W3C header (nothing at all when the span context is incomplete), a total `problem+json` read, and the classification a STATUS is allowed to give when nobody declared one for the code |
`retry`'s `policy.jitter` is **required** where `backoffDelay`'s defaults to `none`: a loop retrying
without jitter IS the thundering herd, so the mode is a decision each caller makes rather than one
it inherits without noticing. `retry` never wraps the last error — a wrapper would replace a code, a
cause and a runnable `fix:` with the fact that something was retried, which no reader can act on.
`classifyThrown` and `statedDelayMs` live in `error-retry.ts`, one import away from the table they
read; `@ultimat3/jobs` re-exports both rather than keeping a second pair.
## One image pipeline, everywhere

@@ -479,0 +551,0 @@

@@ -45,2 +45,6 @@ // Single responsibility: the framework-wide error-code registry (code -> title + docs).

X_ERROR_RETRY_INVALID: 'error retry classification is unknown or already claimed',
// Core's own, and deliberately NOT `@ultimat3/http`'s `X_OVERLOADED`: that code is owned by a
// tier-2 package, and a tier-0 gate borrowing it upward is an import core may not make. The two
// read alike to an operator and the fix lines say which ceiling to widen.
X_FLIGHT_GATE_OVERLOADED: 'a concurrency gate is at its ceiling and its queue is full',
X_ID_INVALID: 'value is not a valid id',

@@ -71,2 +75,3 @@ X_IMAGE_DECODE_FAILED: 'image bytes are malformed, truncated or internally inconsistent',

X_SHUTDOWN_TIMEOUT: 'graceful shutdown exceeded its deadline',
X_SUPERSEDED: 'a later generation superseded this work',
X_TELEMETRY_SAMPLER_ARG_INVALID: 'the trace sampling ratio is not a number between 0 and 1',

@@ -73,0 +78,0 @@ // Core's, though core does not throw it — the twin of `X_ABORTED`, and `@ultimat3/http` already

@@ -6,3 +6,3 @@ // Single responsibility: is this error worth trying again? One classification per code, carried on

import { UltimateError } from './errors';
import { isUltimateError, UltimateError } from './errors';

@@ -52,2 +52,12 @@ /**

X_NOT_IMPLEMENTED: 'terminal',
// A gate at its ceiling with a full queue is the canonical "not now": nothing about the work
// was wrong and the same call succeeds once a slot frees. `retry-after` rather than
// `retryable` because the refusal NAMES a delay — `retryAfterSeconds` on its `meta`, the one
// spelling `@ultimat3/http`'s `retryAfterOf` reads onto the header — so a caller that guesses
// a backoff is guessing against an answer it was already given.
X_FLIGHT_GATE_OVERLOADED: 'retry-after',
// Listed for the same reason `X_NOT_IMPLEMENTED` is, and it matters more here: superseded work
// re-run produces the same refusal by construction, so an UNCLASSIFIED reading would spend a
// job's whole retry policy proving that the world has still moved on.
X_SUPERSEDED: 'terminal',
} as const),

@@ -129,1 +139,44 @@ );

}
/**
* The classification that was DECLARED for this throw, or `undefined` when there is none.
*
* Deliberately not `error.retry` alone. That field is `init.retry ?? retryFor(code)` and `retryFor`
* fails closed, so every unclassified `UltimateError` already carries `terminal` — reading it would
* dead-letter the first attempt of every job in every app whose codes nobody has classified yet. So
* `terminal` counts only when it can have come from somewhere: an explicit per-instance override is
* indistinguishable from the default here, which is why an UNCLASSIFIED code carrying an instance
* `retry: 'terminal'` is read as unclassified. Register the code
* (`registerErrorRetry({ X_YOUR_CODE: 'terminal' })`) to have it honoured.
*
* Lives here rather than beside an executor because it is the reader of the table above, and every
* executor in the framework has to answer it the same way — `@ultimat3/jobs`' `classifyThrown` in
* `retry-classification.ts` is this function, one tier up, and can delegate to it unchanged.
*/
export function classifyThrown(error: unknown): ErrorRetry | undefined {
// The brand check, never `instanceof`: a duplicated module instance in the dependency tree makes
// `instanceof` answer false for an error this framework built.
if (!isUltimateError(error)) return undefined;
const retry: unknown = error.retry;
if (!isErrorRetry(retry)) return undefined;
// Anything other than the fail-closed default can only have come from the code table or from an
// explicit override, so it is somebody's answer either way.
if (retry !== DEFAULT_ERROR_RETRY) return retry;
return declaredErrorRetry(error.code) === undefined ? undefined : retry;
}
/**
* The delay a `retry-after` error NAMED, in ms, or `undefined` when it named none.
*
* `retryAfterSeconds` on the error's `meta` is the framework's ONE spelling for it —
* `@ultimat3/http`'s `rateLimited` writes it, its `retryAfterOf` renders it onto the `Retry-After`
* header, `@ultimat3/auth`'s `kdfOverloaded` carries it — so a retry loop and an HTTP client wait
* the same number. A literal key, not a constant: a computed read of a record is what
* `bun run proto-index` refuses, and `meta` is a caller's object.
*/
export function statedDelayMs(error: unknown): number | undefined {
if (!isUltimateError(error)) return undefined;
const seconds: unknown = error.meta?.['retryAfterSeconds'];
if (typeof seconds !== 'number' || !Number.isFinite(seconds) || seconds < 0) return undefined;
return Math.round(seconds * 1_000);
}

@@ -34,2 +34,3 @@ // The error-contract slice of `@ultimat3/core`'s public surface: `UltimateError` and its shipped

export {
classifyThrown,
DEFAULT_ERROR_RETRY,

@@ -43,2 +44,3 @@ declaredErrorRetry,

retryFor,
statedDelayMs,
} from '../error-retry';

@@ -45,0 +47,0 @@ export type {

@@ -35,4 +35,23 @@ // Single responsibility: the public API of @ultimat3/core. Explicit named exports only —

export { type AsyncContext, asyncContext } from './async-context';
export type { BackoffCurve, BackoffOptions, JitterMode, Random } from './backoff';
export { backoffDelay } from './backoff';
export { CACHE_TIERS, type CacheTierName } from './cache-vocabulary';
export { canonicalJson, fingerprint } from './canonical-json';
/**
* Flight control for a typed client, and OPT-IN by construction: `@ultimat3/action`'s and
* `@ultimat3/query`'s `client.ts` each name `ClientFlight` as a TYPE only, so a caller that never
* mentions `createClientFlight` pays nothing for the fence, the dedup map or the retry loop.
* Both packages re-export these names unchanged; this is the one copy.
*/
export type {
ClientFlight,
ClientFlightOptions,
ClientRetry,
FlightKeyOptions,
FlightPlan,
} from './client-flight';
export { createClientFlight, DEFAULT_CLIENT_RETRY, isTransientFailure } from './client-flight';
/** What a typed client puts on the wire. `retryForStatus` is what fills a failure's `retry`. */
export type { WireAnswer } from './client-wire';
export { FRAMEWORK_CODE, problemOf, retryForStatus, traceHeaders } from './client-wire';
export { type Clock, type FrozenClock, frozenClock, systemClock } from './clock';

@@ -131,2 +150,3 @@ export type {

ConfigInvalidError,
classifyThrown,
DEFAULT_ERROR_RETRY,

@@ -162,2 +182,3 @@ declaredErrorRetry,

singleLine,
statedDelayMs,
stringField,

@@ -375,3 +396,12 @@ toUltimateError,

} from './exports/secrets';
export type {
FlightGate,
FlightGateLimits,
FlightGateOptions,
FlightGateState,
} from './flight-gate';
export { createFlightGate, gateOverloaded } from './flight-gate';
export { formatBytes } from './format-bytes';
export type { GenerationFence } from './generation-fence';
export { createFence, isSuperseded } from './generation-fence';
export type { Brand, Id } from './ids';

@@ -431,2 +461,3 @@ export {

export { cachedFormatter, canonicalLocale, MAX_CACHED_FORMATTERS } from './intl-cache';
export { isJsonObject } from './json-object';
export type {

@@ -488,2 +519,5 @@ HealthPayload,

export { err, isErr, isOk, map, mapErr, ok, tryCatch, unwrap, unwrapOr } from './result';
export type { RetryDecision, RetryDeps, RetryPolicy, RetryStopReason } from './retry';
export { retry, retryDecision } from './retry';
export { isRetryableStatus, RETRYABLE_STATUSES } from './retryable-status';
export type { ResolveRoleOptions, Role, RoleInfo, ScalingSignal } from './roles';

@@ -495,2 +529,4 @@ export { DEFAULT_ROLE, isRole, ROLE_INFO, ROLES, resolveRole } from './roles';

export { defineService, resetServices, type ServiceFactory } from './service';
export type { FlightJoin, Scheduler, SingleFlight, SingleFlightOptions } from './single-flight';
export { createSingleFlight } from './single-flight';
export { timingSafeEqual } from './timing-safe-equal';

@@ -497,0 +533,0 @@ export {