@ultimat3/core
Advanced tools
| // 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); | ||
| } |
+132
| // 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", |
+72
-0
@@ -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 |
+54
-1
@@ -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 { |
+36
-0
@@ -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 { |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
544359
11.39%83
12.16%10126
9.99%604
13.53%