experimental-a2 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +73 -0
- package/dist/actor-client.d.ts +1 -1
- package/dist/{actor-BfQSE0KC.d.ts → actor-ohPC-81x.d.ts} +5 -5
- package/dist/{actor-BfQSE0KC.d.ts.map → actor-ohPC-81x.d.ts.map} +1 -1
- package/dist/actor-react.d.ts +3 -3
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +1 -1
- package/dist/ai-CrLYwNEx.js +2431 -0
- package/dist/ai-CrLYwNEx.js.map +1 -0
- package/dist/{ai-Cai-lCbj.d.ts → ai-HJ9fHfYI.d.ts} +72 -13
- package/dist/ai-HJ9fHfYI.d.ts.map +1 -0
- package/dist/ai-server.d.ts +4 -5
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +755 -290
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +2 -2
- package/dist/ai.js +1 -1999
- package/dist/client-BYzHjkwU.js.map +1 -1
- package/dist/{client-BAEABRZB.d.ts → client-CzyacQpJ.d.ts} +9 -9
- package/dist/client-CzyacQpJ.d.ts.map +1 -0
- package/dist/client.d.ts +1 -1
- package/dist/idempotent-id-BRJVeylj.js +13 -0
- package/dist/idempotent-id-BRJVeylj.js.map +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -12
- package/dist/otel.d.ts +1 -1
- package/dist/react.d.ts +13 -13
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js.map +1 -1
- package/dist/reducer-DEMjEY_O.js.map +1 -1
- package/dist/{reducer-BcS9VDKC.d.ts → reducer-otzHjuJj.d.ts} +3 -3
- package/dist/{reducer-BcS9VDKC.d.ts.map → reducer-otzHjuJj.d.ts.map} +1 -1
- package/dist/scheduler-qstash.d.ts +2 -2
- package/dist/scheduler-qstash.js +1 -1
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.js +1 -1
- package/dist/{server-CjJSGcF7.js → server-BD5ckxZb.js} +39 -26
- package/dist/server-BD5ckxZb.js.map +1 -0
- package/dist/{server-Bp5Nd1pF.d.ts → server-DbD7IJVK.d.ts} +5 -5
- package/dist/{server-Bp5Nd1pF.d.ts.map → server-DbD7IJVK.d.ts.map} +1 -1
- package/dist/server.d.ts +3 -3
- package/dist/server.js +1 -1
- package/dist/{store-D_yhNdPz.d.ts → store-DyZM6fS5.d.ts} +3 -2
- package/dist/{store-D_yhNdPz.d.ts.map → store-DyZM6fS5.d.ts.map} +1 -1
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +1 -1
- package/dist/{store-redis-notify-D2EI6gwX.js → store-redis-notify-BVLUfI3j.js} +14 -13
- package/dist/store-redis-notify-BVLUfI3j.js.map +1 -0
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.js +1 -1
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/{telemetry-CpeclqB2.d.ts → telemetry-B5jzpy6y.d.ts} +2 -2
- package/dist/telemetry-B5jzpy6y.d.ts.map +1 -0
- package/docs/concepts/04-state.mdx +9 -4
- package/docs/guides/06-ai-agents.mdx +134 -43
- package/docs/reference/01-api.mdx +104 -27
- package/examples/playground/app/agent/[agentId]/agent-client.tsx +20 -28
- package/examples/playground/app/agent/[agentId]/agent-queue.test.tsx +123 -0
- package/examples/playground/app/agent/[agentId]/agent-queue.tsx +37 -43
- package/examples/playground/app/agent/[agentId]/compaction-panel.tsx +44 -24
- package/examples/playground/app/agent/compaction-timeline.test.ts +47 -7
- package/examples/playground/app/agent/compaction-timeline.ts +13 -3
- package/examples/playground/app/chat/[chatId]/chat-client.tsx +1 -5
- package/examples/playground/app/chat/[chatId]/session.ts +17 -2
- package/examples/playground/app/chat/model.test.ts +4 -4
- package/examples/playground/app/chat/model.ts +18 -6
- package/examples/playground/app/chat/server.ts +6 -3
- package/examples/playground/package.json +3 -1
- package/package.json +1 -1
- package/src/ai-client-state.ts +0 -2
- package/src/ai-context-schema.ts +296 -0
- package/src/ai-context.ts +680 -0
- package/src/ai-control-server.ts +160 -131
- package/src/ai-control-state.ts +6 -0
- package/src/ai-control.ts +6 -7
- package/src/ai-coordinator.ts +92 -38
- package/src/ai-id.ts +6 -0
- package/src/ai-input-tokens.ts +75 -0
- package/src/ai-message-projection.ts +348 -0
- package/src/ai-server.ts +232 -334
- package/src/ai-stored-state.ts +484 -0
- package/src/ai.ts +252 -418
- package/src/client.ts +21 -15
- package/src/react.ts +25 -19
- package/src/reducer.ts +2 -1
- package/src/server.ts +73 -56
- package/src/store-redis-core.ts +15 -17
- package/src/store.ts +1 -0
- package/src/telemetry.ts +2 -1
- package/dist/ai-Cai-lCbj.d.ts.map +0 -1
- package/dist/ai-control-CcD4hh3y.js +0 -119
- package/dist/ai-control-CcD4hh3y.js.map +0 -1
- package/dist/ai.js.map +0 -1
- package/dist/client-BAEABRZB.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/server-CjJSGcF7.js.map +0 -1
- package/dist/store-redis-notify-D2EI6gwX.js.map +0 -1
- package/dist/telemetry-CpeclqB2.d.ts.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store-N8PXxDAS.js","names":[],"sources":["../src/store.ts"],"sourcesContent":["/**\n * The A2Store interface — the storage contract every store backend\n * implements. See specs/a2-implementation.md §2–3.\n *\n * This is the whole storage contract: append, read, dispatch claims,\n * failure markers, snapshots, and the live stream. The scheduler\n * needs nothing extra — the armed queue message is its own state, and\n * the store is the only thing it consults.\n */\n\nimport type { PresencePatch } from './contract.ts'\n\n/** A stored event, as the public API exposes it. */\nexport type Event = {\n id: string\n type: string\n payload: unknown\n /** Position in the session's event log, from 1. */\n index: number\n sessionId: string\n createdAt: Date\n}\n\n/** The handler dispatch whose append first persisted a child event. */\nexport type EventCause = {\n index: number\n attempt: number\n /** Size of one named handler append, used to reject truncated retries. */\n batchSize?: number\n}\n\n/**\n * What the store persists: immutable event history, including its causal edge,\n * plus derived dispatch and failure bookkeeping. The bookkeeping is disposable;\n * the event and its cause are not.\n */\nexport type StoredEvent = Event & {\n /** Same-session handler dispatch that appended this event; null means root. */\n cause: EventCause | null\n /** Session-scoped serial execution key resolved when the event is appended. */\n lane: string | null\n /** Adapter clock time recorded by append settlement, completion, or manual skip; null while pending. */\n processedAt: Date | null\n /** Dispatch attempt that completed this event; null without dispatch or while pending. */\n processedByAttempt: number | null\n /** Exact ordered child ids atomically returned by the completing attempt. */\n returnedEventIds: string[] | null\n /** Adapter clock time recorded by the first durable dispatch claim. */\n firstClaimedAt: Date | null\n /** Adapter clock time recorded by the most recent durable dispatch claim. */\n lastClaimedAt: Date | null\n /** Durable dispatch claims, including claims abandoned by hard kills. */\n attemptCount: number\n /** Current dispatch holder; null when the event is not claimed. */\n claimHolder: string | null\n /** Adapter clock expiry for the current dispatch claim. */\n claimExpiresAt: Date | null\n /** Caught handler failures. This alone drives dead-lettering. */\n failureCount: number\n /** Adapter clock time recorded by the most recent caught handler failure. */\n lastFailedAt: Date | null\n /** Dispatch attempt that produced the most recent caught handler failure. */\n lastFailedAttempt: number | null\n /** The last handler failure, stringified. */\n lastError: string | null\n /** Adapter clock time recorded when dead-lettered; null otherwise. */\n failedAt: Date | null\n}\n\n/**\n * One live presence value, as `A2Store.presence.read` returns it. The\n * row is the LWW unit — one participant's one field. `at` is the\n * sender's stamp, the LWW comparator. `expiresAt` is the storage's\n * own clock at the applied set plus its `ttlMs` — never derived from\n * the sender stamp; a row is live strictly before it.\n */\nexport type PresenceRow = {\n participant: string\n field: string\n value: unknown\n seen: number\n at: Date\n expiresAt: Date\n}\n\n/** One consistent cache-plus-tail read for a reducer fold. */\nexport type StoreStateRead = {\n /** The current head checkpoint, even when an older historical snapshot is selected. */\n headIndex: number | null\n /** The greatest cached fold at or before the requested snapshot frontier. */\n snapshot: { index: number; state: unknown } | null\n /** Immutable events strictly after `snapshot.index`, or the full log on a miss. */\n events: Event[]\n}\n\nexport type StoreSnapshotWrite = {\n index: number\n state: unknown\n /** Unfinished trigger events that durably retain this exact checkpoint. */\n pinEventIndexes?: readonly number[]\n}\n\nexport type StoreStateReadRequest = {\n sessionId: string\n throughIndex?: number\n snapshotThroughIndex?: number\n}\n/** The result of atomically claiming every currently eligible event. */\nexport type StoreClaimAvailableResult =\n | { outcome: 'claimed'; events: StoredEvent[] }\n | { outcome: 'busy'; retryAt: Date }\n | { outcome: 'settled' }\n\n/** A completion may lose to a newer claim or an earlier completion. */\nexport type CompleteAttemptResult =\n { outcome: 'completed'; events: StoredEvent[] } | { outcome: 'superseded' }\n\n/** The result of one claim-renewal heartbeat, both lists in append order. */\nexport type RenewClaimsResult = {\n /** Listed claims that remain owned by the holder. */\n renewed: number[]\n /** Listed claims whose event a newer attempt has durably taken. */\n superseded: number[]\n}\n\n/** The result of atomically recording a caught handler failure. */\nexport type FailAttemptResult = {\n outcome: 'failed' | 'dead_lettered' | 'superseded'\n failureCount: number\n}\n\n/** Input to `A2Store.append` — already validated by the machine. */\nexport type AppendEvent = {\n type: string\n payload: unknown\n /** Caller-supplied idempotency key; generated when absent. */\n id?: string\n /** Internal causal edge supplied atomically by handler `session.append`. */\n cause?: EventCause\n /** Session-scoped serial execution key resolved before persistence. */\n lane?: string\n /** Internal hint: settle this event in the append transaction; no handler is registered. */\n settled?: true\n}\n\n/** A handler-returned event with the deterministic id retries require. */\nexport type ReturnedEvent = AppendEvent & { id: string }\n\n/** The rows written by an append and the session's pending state. */\nexport type StoreAppendResult = {\n events: StoredEvent[]\n /** Whether the session contains an event without a completion marker. */\n hasPending: boolean\n}\n\n/**\n * An injectable clock. Adapters take one so tests can drive claim\n * expiry, failure timestamps, and (later) stuck-session detection\n * deterministically — against real storage, no mocking.\n */\nexport type Clock = {\n now(): Date\n}\n\n/** An injectable id source for generated event ids. */\nexport type IdSource = () => string\n\nexport const SYSTEM_CLOCK: Clock = {\n now: () => new Date(),\n}\n\nexport const RANDOM_IDS: IdSource = () => crypto.randomUUID()\n\nexport interface A2Store {\n /**\n * Accepts a batch; the batch is atomic — one transaction, consecutive\n * `index`es, all-or-nothing. The idempotency key covers the whole\n * operation, not each item: if *every* event's `id` already exists in\n * this session, this is a retry of a committed batch whose ack was\n * lost — return the existing rows as success. If only *some* ids\n * exist, the caller mixed an already-sent batch with fresh events —\n * always a caller bug — so throw `A2Error('PARTIAL_DUPLICATE_BATCH')`.\n * Events carrying `settled: true` get `processedAt` in this same atomic\n * operation, with no dispatch claim or `processedByAttempt`. The result's\n * `hasPending` reflects the whole session in the same atomic operation,\n * including older events and idempotent retries.\n *\n * An event carrying `cause` is a handler append, fenced by attempt\n * currency: accept it only while `cause.attempt` is still the parent\n * event's latest attempt and the parent is not dead-lettered; otherwise\n * throw `A2Error('SUPERSEDED_ATTEMPT')` and write nothing. The check is\n * part of this atomic operation and must serialize against a concurrent\n * `claimAvailable` — an unlocked read of the parent admits write skew.\n * The idempotent-replay path runs first, so a batch whose ids all exist\n * replays regardless of the current attempt.\n */\n append(sessionId: string, events: AppendEvent[]): Promise<StoreAppendResult>\n\n /** Events for one session, oldest first. Bounds form `(afterIndex, throughIndex]`. */\n read(\n sessionId: string,\n opts?: { afterIndex?: number; throughIndex?: number },\n ): Promise<StoredEvent[]>\n\n /** Public history without dispatch metadata; core falls back to read when absent. */\n readHistory?(\n sessionId: string,\n opts?: { afterIndex?: number; throughIndex?: number },\n ): Promise<Event[]>\n\n /**\n * Atomically claims every eligible pending event. Unlaned events are all\n * independently eligible. Within a lane, only the lowest-index unfinished\n * event is eligible. A live claim produces `busy` only when it is the sole\n * remaining obstacle to actionable work. Claimed events are returned in event-log\n * order. Excluded rows remain lane barriers.\n */\n claimAvailable(options: {\n sessionId: string\n holder: string\n ttlMs: number\n expiresAtMs?: number\n excludeIndexes?: readonly number[]\n }): Promise<StoreClaimAvailableResult>\n\n /**\n * Renews the listed live claims still owned by `holder`. Each listed claim\n * carries the attempt ordinal the holder owns. `renewed` lists the claims\n * that remain owned after the operation; `superseded` lists the claims\n * whose event's `attemptCount` has durably passed the listed attempt. An\n * expired claim no successor has taken appears in neither list — its\n * attempt may still complete (`completeAttempt` is the fence), so it is\n * not reported as lost. Renewal never revives an expired claim.\n */\n renewClaims(options: {\n sessionId: string\n holder: string\n claims: readonly { index: number; attempt: number }[]\n ttlMs: number\n expiresAtMs?: number\n }): Promise<RenewClaimsResult>\n\n /**\n * Atomically completes one current attempt and appends its returned events.\n * Retrying a committed completion with the same attempt and deterministic\n * child ids returns the existing children. A stale attempt never appends.\n */\n completeAttempt(options: {\n sessionId: string\n index: number\n attempt: number\n events: ReturnedEvent[]\n }): Promise<CompleteAttemptResult>\n\n /**\n * Atomically records a caught failure for one claimed attempt. A stale\n * attempt cannot poison a processed event or a newer dispatch. Accepted\n * failures record the operation's clock time.\n */\n failAttempt(options: {\n sessionId: string\n index: number\n attempt: number\n error: string\n maxFailures: number\n }): Promise<FailAttemptResult>\n\n /**\n * Reads a reducer snapshot and its event tail as one consistent adapter\n * operation. On a cache miss, `snapshot` is null and `events` is the full\n * log. The snapshot is untrusted; core may reject it and issue a full\n * `read()` when its state schema no longer accepts the cached value.\n */\n readState(\n sessionId: string,\n reducerName: string,\n options?: { throughIndex?: number; snapshotThroughIndex?: number },\n ): Promise<StoreStateRead>\n\n /**\n * Optional batched `readState` — one consistent snapshot-plus-tail read\n * per session id, aligned positionally with the input (duplicates\n * allowed). Each element has its own frontier; the batch makes no\n * cross-session consistency claim. Core falls back to parallel\n * `readState` calls when absent.\n */\n readStates?(\n requests: readonly StoreStateReadRequest[],\n reducerName: string,\n ): Promise<StoreStateRead[]>\n\n /**\n * Atomically writes disposable reducer checkpoints. The greatest index\n * advances the head cache. Older checkpoints survive only when at least one\n * listed trigger event is still unfinished; completion and dead-lettering\n * release that event's pins and collect unreferenced historical checkpoints.\n */\n putSnapshots(\n sessionId: string,\n reducerName: string,\n snapshots: readonly StoreSnapshotWrite[],\n ): Promise<void>\n\n /**\n * Optional ephemeral-plane capability (specs/a2-implementation.md\n * §15.1) — optional like snapshots are. Values arrive already\n * validated by core; adapters store them opaquely. Presence never\n * touches the event log: no index, no history row, no recovery arm.\n */\n presence?: {\n /**\n * Field-wise last-writer-wins merge of one participant's values:\n * a field whose existing row has a strictly newer `at` (the\n * sender's stamp) is left untouched; `null` deletes the row. Every\n * applied write refreshes that field's expiry to the storage's own\n * clock plus `ttlMs` — the sender stamp orders writes but never\n * anchors their lifetime.\n */\n set(\n ns: string,\n participant: string,\n values: Record<string, unknown | null>,\n meta: { seen: number; at: Date; ttlMs: number },\n ): Promise<void>\n\n /** The current map, pruned of rows at or past their `expiresAt`. */\n read(ns: string): Promise<PresenceRow[]>\n\n /**\n * Adapter-managed patch delivery. Without it, live feeds re-read\n * presence on their poll cadence. HTTP Redis uses adaptive polling\n * here when its endpoint does not support subscriptions.\n */\n subscribe?(ns: string, onPatch: (patch: PresencePatch) => void): () => void\n }\n\n /**\n * A live feed of one session's events, starting after `startAfter`\n * (exclusive). Transport is the backend's choice — in-process pub/sub,\n * polling, LISTEN/NOTIFY — callers never branch on which. The iterable\n * ends when the consumer calls `return()` (e.g. a disconnecting SSE\n * client) and must deliver events appended after subscription.\n */\n stream(\n sessionId: string,\n opts?: { startAfter?: number },\n ): AsyncIterable<Event>\n}\n"],"mappings":";AAuKA,MAAa,eAAsB,EACjC,2BAAW,IAAI,KAAK,EACtB;AAEA,MAAa,mBAA6B,OAAO,WAAW"}
|
|
1
|
+
{"version":3,"file":"store-N8PXxDAS.js","names":[],"sources":["../src/store.ts"],"sourcesContent":["/**\n * The A2Store interface — the storage contract every store backend\n * implements. See specs/a2-implementation.md §2–3.\n *\n * This is the whole storage contract: append, read, dispatch claims,\n * failure markers, snapshots, and the live stream. The scheduler\n * needs nothing extra — the armed queue message is its own state, and\n * the store is the only thing it consults.\n */\n\nimport type { PresencePatch } from './contract.ts'\n\n/** A stored event, as the public API exposes it. */\nexport type Event = {\n id: string\n type: string\n payload: unknown\n /** Position in the session's event log, from 1. */\n index: number\n sessionId: string\n createdAt: Date\n}\n\n/** The handler dispatch whose append first persisted a child event. */\nexport type EventCause = {\n index: number\n attempt: number\n /** Size of one named handler append, used to reject truncated retries. */\n batchSize?: number\n}\n\n/**\n * What the store persists: immutable event history, including its causal edge,\n * plus derived dispatch and failure bookkeeping. The bookkeeping is disposable;\n * the event and its cause are not.\n */\nexport type StoredEvent = Event & {\n /** Same-session handler dispatch that appended this event; null means root. */\n cause: EventCause | null\n /** Session-scoped serial execution key resolved when the event is appended. */\n lane: string | null\n /** Adapter clock time recorded by append settlement, completion, or manual skip; null while pending. */\n processedAt: Date | null\n /** Dispatch attempt that completed this event; null without dispatch or while pending. */\n processedByAttempt: number | null\n /** Exact ordered child ids atomically returned by the completing attempt. */\n returnedEventIds: string[] | null\n /** Adapter clock time recorded by the first durable dispatch claim. */\n firstClaimedAt: Date | null\n /** Adapter clock time recorded by the most recent durable dispatch claim. */\n lastClaimedAt: Date | null\n /** Durable dispatch claims, including claims abandoned by hard kills. */\n attemptCount: number\n /** Current dispatch holder; null when the event is not claimed. */\n claimHolder: string | null\n /** Adapter clock expiry for the current dispatch claim. */\n claimExpiresAt: Date | null\n /** Caught handler failures. This alone drives dead-lettering. */\n failureCount: number\n /** Adapter clock time recorded by the most recent caught handler failure. */\n lastFailedAt: Date | null\n /** Dispatch attempt that produced the most recent caught handler failure. */\n lastFailedAttempt: number | null\n /** The last handler failure, stringified. */\n lastError: string | null\n /** Adapter clock time recorded when dead-lettered; null otherwise. */\n failedAt: Date | null\n}\n\n/**\n * One live presence value, as `A2Store.presence.read` returns it. The\n * row is the LWW unit — one participant's one field. `at` is the\n * sender's stamp, the LWW comparator. `expiresAt` is the storage's\n * own clock at the applied set plus its `ttlMs` — never derived from\n * the sender stamp; a row is live strictly before it.\n */\nexport type PresenceRow = {\n participant: string\n field: string\n value: unknown\n seen: number\n at: Date\n expiresAt: Date\n}\n\n/** One consistent cache-plus-tail read for a reducer fold. */\nexport type StoreStateRead = {\n /** The current head checkpoint, even when an older historical snapshot is selected. */\n headIndex: number | null\n /** The greatest cached fold at or before the requested snapshot frontier. */\n snapshot: { index: number; state: unknown } | null\n /** Immutable events strictly after `snapshot.index`, or the full log on a miss. */\n events: Event[]\n}\n\nexport type StoreSnapshotWrite = {\n index: number\n state: unknown\n /** Unfinished trigger events that durably retain this exact checkpoint. */\n pinEventIndexes?: readonly number[]\n}\n\nexport type StoreStateReadRequest = {\n sessionId: string\n throughIndex?: number\n snapshotThroughIndex?: number\n}\n/** The result of atomically claiming every currently eligible event. */\nexport type StoreClaimAvailableResult =\n | { outcome: 'claimed'; events: StoredEvent[] }\n | { outcome: 'busy'; retryAt: Date }\n | { outcome: 'settled' }\n\n/** A completion may lose to a newer claim or an earlier completion. */\nexport type CompleteAttemptResult =\n { outcome: 'completed'; events: StoredEvent[] } | { outcome: 'superseded' }\n\n/** The result of one claim-renewal heartbeat, both lists in append order. */\nexport type RenewClaimsResult = {\n /** Listed claims that remain owned by the holder. */\n renewed: number[]\n /** Listed claims whose event a newer attempt has durably taken. */\n superseded: number[]\n}\n\n/** The result of atomically recording a caught handler failure. */\nexport type FailAttemptResult = {\n outcome: 'failed' | 'dead_lettered' | 'superseded'\n failureCount: number\n}\n\n/** Input to `A2Store.append` — already validated by the machine. */\nexport type AppendEvent = {\n type: string\n payload: unknown\n /** Caller-supplied idempotency key; generated when absent. */\n id?: string\n /** Internal causal edge supplied atomically by handler `session.append`. */\n cause?: EventCause\n /** Session-scoped serial execution key resolved before persistence. */\n lane?: string\n /** Internal hint: settle this event in the append transaction; no handler is registered. */\n settled?: true\n}\n\n/** A handler-returned event with the deterministic id retries require. */\nexport type ReturnedEvent = AppendEvent & { id: string }\n\n/** The rows written by an append and the session's pending state. */\nexport type StoreAppendResult = {\n events: StoredEvent[]\n /** Whether the session contains an event without a completion marker. */\n hasPending: boolean\n}\n\n/**\n * An injectable clock. Adapters take one so tests can drive claim\n * expiry, failure timestamps, and (later) stuck-session detection\n * deterministically — against real storage, no mocking.\n */\nexport type Clock = {\n now(): Date\n}\n\n/** An injectable id source for generated event ids. */\nexport type IdSource = () => string\n\nexport const SYSTEM_CLOCK: Clock = {\n now: () => new Date(),\n}\n\nexport const RANDOM_IDS: IdSource = () => crypto.randomUUID()\n\nexport interface A2Store {\n /**\n * Accepts a batch; the batch is atomic — one transaction, consecutive\n * `index`es, all-or-nothing. The idempotency key covers the whole\n * operation, not each item: if *every* event's `id` already exists in\n * this session, this is a retry of a committed batch whose ack was\n * lost — return the existing rows as success. If only *some* ids\n * exist, the caller mixed an already-sent batch with fresh events —\n * always a caller bug — so throw `A2Error('PARTIAL_DUPLICATE_BATCH')`.\n * Events carrying `settled: true` get `processedAt` in this same atomic\n * operation, with no dispatch claim or `processedByAttempt`. The result's\n * `hasPending` reflects the whole session in the same atomic operation,\n * including older events and idempotent retries.\n *\n * An event carrying `cause` is a handler append, fenced by attempt\n * currency: accept it only while `cause.attempt` is still the parent\n * event's latest attempt and the parent is not dead-lettered; otherwise\n * throw `A2Error('SUPERSEDED_ATTEMPT')` and write nothing. The check is\n * part of this atomic operation and must serialize against a concurrent\n * `claimAvailable` — an unlocked read of the parent admits write skew.\n * The idempotent-replay path runs first, so a batch whose ids all exist\n * replays regardless of the current attempt.\n */\n append(sessionId: string, events: AppendEvent[]): Promise<StoreAppendResult>\n\n /** Events for one session, oldest first. Bounds form `(afterIndex, throughIndex]`. */\n read(\n sessionId: string,\n opts?: { afterIndex?: number; throughIndex?: number },\n ): Promise<StoredEvent[]>\n\n /** Public history without dispatch metadata; core falls back to read when absent. */\n readHistory?(\n sessionId: string,\n opts?: { afterIndex?: number; throughIndex?: number },\n ): Promise<Event[]>\n\n /**\n * Atomically claims every eligible pending event. Unlaned events are all\n * independently eligible. Within a lane, only the lowest-index unfinished\n * event is eligible. A live claim produces `busy` only when it is the sole\n * remaining obstacle to actionable work. Claimed events are returned in event-log\n * order. Excluded rows remain lane barriers.\n */\n claimAvailable(options: {\n sessionId: string\n holder: string\n ttlMs: number\n expiresAtMs?: number\n excludeIndexes?: readonly number[]\n }): Promise<StoreClaimAvailableResult>\n\n /**\n * Renews the listed live claims still owned by `holder`. Each listed claim\n * carries the attempt ordinal the holder owns. `renewed` lists the claims\n * that remain owned after the operation; `superseded` lists the claims\n * whose event's `attemptCount` has durably passed the listed attempt. An\n * expired claim no successor has taken appears in neither list — its\n * attempt may still complete (`completeAttempt` is the fence), so it is\n * not reported as lost. Renewal never revives an expired claim.\n */\n renewClaims(options: {\n sessionId: string\n holder: string\n claims: readonly { index: number; attempt: number }[]\n ttlMs: number\n expiresAtMs?: number\n }): Promise<RenewClaimsResult>\n\n /**\n * Atomically completes one current attempt and appends its returned events.\n * Retrying a committed completion with the same attempt and deterministic\n * child ids returns the existing children. A stale attempt never appends.\n */\n completeAttempt(options: {\n sessionId: string\n index: number\n attempt: number\n events: ReturnedEvent[]\n }): Promise<CompleteAttemptResult>\n\n /**\n * Atomically records a caught failure for one claimed attempt. A stale\n * attempt cannot poison a processed event or a newer dispatch. Accepted\n * failures record the operation's clock time.\n */\n failAttempt(options: {\n sessionId: string\n index: number\n attempt: number\n error: string\n maxFailures: number\n }): Promise<FailAttemptResult>\n\n /**\n * Reads a reducer snapshot and its event tail as one consistent adapter\n * operation. On a cache miss, `snapshot` is null and `events` is the full\n * log. The snapshot is untrusted; core may reject it and issue a full\n * `read()` when its state schema no longer accepts the cached value.\n * Operational failures propagate without falling back to a full-log read.\n */\n readState(\n sessionId: string,\n reducerName: string,\n options?: { throughIndex?: number; snapshotThroughIndex?: number },\n ): Promise<StoreStateRead>\n\n /**\n * Optional batched `readState` — one consistent snapshot-plus-tail read\n * per session id, aligned positionally with the input (duplicates\n * allowed). Each element has its own frontier; the batch makes no\n * cross-session consistency claim. Core falls back to parallel\n * `readState` calls when absent.\n */\n readStates?(\n requests: readonly StoreStateReadRequest[],\n reducerName: string,\n ): Promise<StoreStateRead[]>\n\n /**\n * Atomically writes disposable reducer checkpoints. The greatest index\n * advances the head cache. Older checkpoints survive only when at least one\n * listed trigger event is still unfinished; completion and dead-lettering\n * release that event's pins and collect unreferenced historical checkpoints.\n */\n putSnapshots(\n sessionId: string,\n reducerName: string,\n snapshots: readonly StoreSnapshotWrite[],\n ): Promise<void>\n\n /**\n * Optional ephemeral-plane capability (specs/a2-implementation.md\n * §15.1) — optional like snapshots are. Values arrive already\n * validated by core; adapters store them opaquely. Presence never\n * touches the event log: no index, no history row, no recovery arm.\n */\n presence?: {\n /**\n * Field-wise last-writer-wins merge of one participant's values:\n * a field whose existing row has a strictly newer `at` (the\n * sender's stamp) is left untouched; `null` deletes the row. Every\n * applied write refreshes that field's expiry to the storage's own\n * clock plus `ttlMs` — the sender stamp orders writes but never\n * anchors their lifetime.\n */\n set(\n ns: string,\n participant: string,\n values: Record<string, unknown | null>,\n meta: { seen: number; at: Date; ttlMs: number },\n ): Promise<void>\n\n /** The current map, pruned of rows at or past their `expiresAt`. */\n read(ns: string): Promise<PresenceRow[]>\n\n /**\n * Adapter-managed patch delivery. Without it, live feeds re-read\n * presence on their poll cadence. HTTP Redis uses adaptive polling\n * here when its endpoint does not support subscriptions.\n */\n subscribe?(ns: string, onPatch: (patch: PresencePatch) => void): () => void\n }\n\n /**\n * A live feed of one session's events, starting after `startAfter`\n * (exclusive). Transport is the backend's choice — in-process pub/sub,\n * polling, LISTEN/NOTIFY — callers never branch on which. The iterable\n * ends when the consumer calls `return()` (e.g. a disconnecting SSE\n * client) and must deliver events appended after subscription.\n */\n stream(\n sessionId: string,\n opts?: { startAfter?: number },\n ): AsyncIterable<Event>\n}\n"],"mappings":";AAuKA,MAAa,eAAsB,EACjC,2BAAW,IAAI,KAAK,EACtB;AAEA,MAAa,mBAA6B,OAAO,WAAW"}
|
package/dist/store-memory.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { r as Clock, s as IdSource, t as A2Store } from "./store-
|
|
1
|
+
import { r as Clock, s as IdSource, t as A2Store } from "./store-DyZM6fS5.js";
|
|
2
2
|
//#region src/store-memory.d.ts
|
|
3
3
|
type MemoryStoreOptions = {
|
|
4
4
|
/** Injectable clock — every stored timestamp comes from here. */
|
package/dist/store-postgres.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { r as Clock, s as IdSource, t as A2Store } from "./store-
|
|
1
|
+
import { r as Clock, s as IdSource, t as A2Store } from "./store-DyZM6fS5.js";
|
|
2
2
|
//#region src/store-postgres.d.ts
|
|
3
3
|
/** The result shape this backend reads: just rows. */
|
|
4
4
|
type PostgresQueryResult = {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { r as Clock, s as IdSource, t as A2Store } from "./store-
|
|
1
|
+
import { r as Clock, s as IdSource, t as A2Store } from "./store-DyZM6fS5.js";
|
|
2
2
|
//#region src/store-redis-http.d.ts
|
|
3
3
|
type RedisHttpStoreOptions = {
|
|
4
4
|
/** REST endpoint, e.g. `process.env.UPSTASH_REDIS_REST_URL`. */
|
package/dist/store-redis-http.js
CHANGED
|
@@ -2,7 +2,7 @@ import { t as A2Error } from "./errors-DCk6ch5n.js";
|
|
|
2
2
|
import { r as NOTIFY_TIMINGS } from "./internal-Dq2qYxou.js";
|
|
3
3
|
import { t as defaultSleep } from "./store-polling-CmxUbV93.js";
|
|
4
4
|
import { n as SYSTEM_CLOCK, t as RANDOM_IDS } from "./store-N8PXxDAS.js";
|
|
5
|
-
import { t as createRedisNotifyStore } from "./store-redis-notify-
|
|
5
|
+
import { t as createRedisNotifyStore } from "./store-redis-notify-BVLUfI3j.js";
|
|
6
6
|
//#region src/redis-http-subscriptions.ts
|
|
7
7
|
function redisHttpSubscriptions({ url, token, onReconnect, fallback }) {
|
|
8
8
|
const subscriptions = /* @__PURE__ */ new Map();
|
|
@@ -620,20 +620,21 @@ return live
|
|
|
620
620
|
*/
|
|
621
621
|
const READ_STATE_LUA = `
|
|
622
622
|
local head = redis.call('GET', KEYS[1])
|
|
623
|
+
local head_index = head and cjson.decode(head)['index'] or nil
|
|
624
|
+
if head and (type(head_index) ~= 'number' or head_index < 0 or head_index > 9007199254740991 or head_index ~= math.floor(head_index)) then
|
|
625
|
+
return redis.error_reply('invalid snapshot head index')
|
|
626
|
+
end
|
|
623
627
|
local cap = ARGV[1]
|
|
624
628
|
local snapshot = nil
|
|
625
|
-
if
|
|
629
|
+
if head_index and (cap == '' or head_index <= tonumber(cap)) then
|
|
626
630
|
snapshot = head
|
|
627
|
-
|
|
628
|
-
local upper = cap == '' and '+inf' or cap
|
|
629
|
-
local historical = redis.call('ZREVRANGEBYSCORE', KEYS[2], upper, '-inf', 'LIMIT', 0, 1)
|
|
630
|
-
if #historical > 0 then
|
|
631
|
-
if not snapshot or tonumber(cjson.decode(historical[1])['index']) > tonumber(cjson.decode(snapshot)['index']) then
|
|
632
|
-
snapshot = historical[1]
|
|
633
|
-
end
|
|
631
|
+
else
|
|
632
|
+
local upper = cap == '' and '+inf' or cap
|
|
633
|
+
local historical = redis.call('ZREVRANGEBYSCORE', KEYS[2], upper, '-inf', 'LIMIT', 0, 1)
|
|
634
|
+
if #historical > 0 then snapshot = historical[1] end
|
|
634
635
|
end
|
|
635
636
|
local frontier = redis.call('GET', KEYS[3]) or '0'
|
|
636
|
-
return {
|
|
637
|
+
return { head_index or '', snapshot or '', frontier }
|
|
637
638
|
`;
|
|
638
639
|
const fieldMap = (fields) => {
|
|
639
640
|
const map = {};
|
|
@@ -1103,15 +1104,15 @@ function createRedisStoreCore(options) {
|
|
|
1103
1104
|
], [stateOptions?.snapshotThroughIndex ?? stateOptions?.throughIndex ?? ""]);
|
|
1104
1105
|
const rawHead = reply[0];
|
|
1105
1106
|
const raw = reply[1];
|
|
1106
|
-
const
|
|
1107
|
+
const headIndex = rawHead === "" ? null : Number(rawHead);
|
|
1107
1108
|
const parsed = raw === "" ? null : JSON.parse(raw);
|
|
1108
1109
|
const currentFrontier = Number(reply[2]);
|
|
1109
|
-
if (!Number.isSafeInteger(currentFrontier) || currentFrontier < 0 ||
|
|
1110
|
+
if (!Number.isSafeInteger(currentFrontier) || currentFrontier < 0 || headIndex !== null && (!Number.isSafeInteger(headIndex) || headIndex < 0) || parsed !== null && (!Number.isSafeInteger(parsed.index) || parsed.index < 0)) throw new A2Error("STORE_UNAVAILABLE", `redis state frontier for session '${sessionId}' is invalid`);
|
|
1110
1111
|
const frontier = Math.min(currentFrontier, stateOptions?.throughIndex ?? currentFrontier);
|
|
1111
1112
|
const snapshot = parsed;
|
|
1112
1113
|
const events = await readEventRange(sessionId, snapshot?.index ?? 0, frontier);
|
|
1113
1114
|
return {
|
|
1114
|
-
headIndex
|
|
1115
|
+
headIndex,
|
|
1115
1116
|
snapshot: snapshot ? {
|
|
1116
1117
|
index: snapshot.index,
|
|
1117
1118
|
state: snapshot.state
|
|
@@ -1579,4 +1580,4 @@ function createRedisNotifyStore(options) {
|
|
|
1579
1580
|
//#endregion
|
|
1580
1581
|
export { createRedisNotifyStore as t };
|
|
1581
1582
|
|
|
1582
|
-
//# sourceMappingURL=store-redis-notify-
|
|
1583
|
+
//# sourceMappingURL=store-redis-notify-BVLUfI3j.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store-redis-notify-BVLUfI3j.js","names":[],"sources":["../src/presence-recovery.ts","../src/store-redis-core.ts","../src/store-redis-notify.ts"],"sourcesContent":["import type { PresencePatch } from './contract.ts'\nimport type { PresenceRow } from './store.ts'\n\ntype Field = Pick<PresenceRow, 'participant' | 'field' | 'seen' | 'at'>\nconst key = ({\n participant,\n field,\n}: Pick<Field, 'participant' | 'field'>): string =>\n JSON.stringify([participant, field])\n\nexport function presenceRecovery({\n read,\n deliver,\n isActive,\n}: {\n read: () => Promise<PresenceRow[]>\n deliver: (patch: PresencePatch) => void\n isActive: () => boolean\n}): {\n read(): Promise<PresenceRow[]>\n reconnect(): Promise<void>\n update(patch: PresencePatch): void\n} {\n const fields = new Map<string, Field>()\n const reading = new Set<Set<string>>()\n let generation = 0\n let latest: Promise<PresenceRow[]> | undefined\n\n const remember = ({ participant, field, seen, at }: Field): void => {\n const id = key({ participant, field })\n const previous = fields.get(id)\n if (\n !previous ||\n previous.at.getTime() < at.getTime() ||\n (previous.at.getTime() === at.getTime() && previous.seen !== seen)\n )\n fields.set(id, { participant, field, seen, at })\n }\n const snapshot = async ({\n reconcile,\n }: {\n reconcile: boolean\n }): Promise<PresenceRow[]> => {\n const run = generation\n const before = new Map(fields)\n const changed = new Set<string>()\n reading.add(changed)\n try {\n const rows = await read()\n if (!isActive()) return rows\n if (run !== generation) return latest!\n const live = new Set<string>()\n for (const row of rows) {\n const id = key(row)\n live.add(id)\n if (changed.has(id)) continue\n remember(row)\n if (reconcile)\n deliver({\n participant: row.participant,\n values: { [row.field]: row.value },\n seen: row.seen,\n at: row.at,\n })\n }\n if (reconcile) {\n for (const [id, prior] of before) {\n if (live.has(id) || changed.has(id) || fields.get(id) !== prior)\n continue\n fields.delete(id)\n deliver({\n participant: prior.participant,\n values: { [prior.field]: null },\n seen: prior.seen,\n at: prior.at,\n })\n }\n }\n return rows\n } finally {\n reading.delete(changed)\n if (reading.size === 0) latest = undefined\n }\n }\n\n return {\n async read() {\n let pending = latest\n while (pending) {\n // oxlint-disable-next-line no-await-in-loop -- follow replacement reconnects before starting a fresh read\n await pending\n if (latest === pending) break\n pending = latest\n }\n return snapshot({ reconcile: false })\n },\n async reconnect() {\n generation += 1\n latest = snapshot({ reconcile: true })\n await latest\n },\n update(patch) {\n for (const [field, value] of Object.entries(patch.values)) {\n if (value === undefined) continue\n const id = key({ participant: patch.participant, field })\n const previous = fields.get(id)\n if (previous && previous.at.getTime() > patch.at.getTime()) continue\n for (const changed of reading) changed.add(id)\n if (value === null) fields.delete(id)\n else\n remember({\n participant: patch.participant,\n field,\n seen: patch.seen,\n at: patch.at,\n })\n }\n },\n }\n}\n","/**\n * The shared Redis store core — every operation except `stream` and\n * `close`, expressed over one injected `call` function so both\n * transports reuse it: `store-redis` (Redis protocol via `ioredis`) and\n * `store-redis-http` (provider REST APIs).\n *\n * The stream IS the store: each event is a stream entry whose ID is its\n * dense a2 index (`${index}-0`), so `XRANGE` is the catch-up read.\n * Mutable drain/failure bookkeeping lives in a side hash (stream\n * entries are immutable); every write path is one atomic Lua script.\n * The conformance suite in test/conformance is the executable\n * contract, run against a real spawned `redis-server`.\n *\n * Timestamps and claim expiry come from the injected clock so tests\n * can time-travel against a real server. Cluster mode is out: the\n * atomic scripts span keys.\n */\n\nimport type { PresencePatch } from './contract.ts'\nimport { A2Error } from './errors.ts'\nimport { idempotentReplay } from './idempotent-replay.ts'\nimport { nullProtoRecord } from './internal.ts'\nimport { decodeReturnedEventIds } from './store-codec.ts'\nimport type {\n A2Store,\n Clock,\n CompleteAttemptResult,\n Event,\n EventCause,\n IdSource,\n StoreClaimAvailableResult,\n PresenceRow,\n StoredEvent,\n} from './store.ts'\n\n/** Issues any Redis command; the seam both transports implement. */\nexport type RedisCall = (\n command: string,\n ...args: Array<string | number>\n) => Promise<unknown>\n\nexport type RedisStoreCoreOptions = {\n call: RedisCall\n clock: Clock\n ids: IdSource\n keyPrefix: string\n presenceSource: string\n}\n\nexport type RedisStoreCore = Omit<A2Store, 'stream' | 'presence'> & {\n /**\n * The storage half of the presence capability, shared by both\n * transports. `set` returns the applied patch for local loopback,\n * or null when every field lost the LWW race. The script publishes\n * the applied fields for remote subscribers.\n */\n presence: {\n set(\n ns: string,\n participant: string,\n values: Record<string, unknown | null>,\n meta: { seen: number; at: Date; ttlMs: number },\n ): Promise<PresencePatch | null>\n read(ns: string): Promise<PresenceRow[]>\n }\n /** XRANGE + meta merge — the full-row catch-up read. */\n readRange(\n sessionId: string,\n afterIndex: number,\n throughIndex?: number,\n ): Promise<StoredEvent[]>\n /** One bounded XRANGE page of public events for live feeds. */\n readEvents(sessionId: string, afterIndex: number): Promise<Event[]>\n /**\n * Declare the session watched for `ttlMs` — writes publish wake-ups\n * only while this presence marker lives. Live feeds refresh it\n * ahead of expiry; an expired marker degrades latency to the safety\n * cadence, never delivery.\n */\n markWatched(sessionId: string, ttlMs: number): Promise<void>\n}\n\nconst READ_PAGE_SIZE = 256\nconst READ_CONCURRENCY = 16\n\n// ── Lua scripts (each runs atomically) ───────────────────────────────\n\n/**\n * KEYS: store stream, counter, global id hash, metadata, pending, ready,\n * watched.\n * ARGV: sessionId, nowMs, n, then per event:\n * id, type, payloadJson, causeJson, lane, settled; finally notify channel.\n * Returns ['empty', hasPending] | ['ok', firstIndex, hasPending] |\n * ['dup', hasPending, ...indexes] | ['partial', count] |\n * ['foreign', eventId] | ['no_parent', idx] | ['superseded', idx].\n */\nconst APPEND_LUA = `\nlocal function lane_field(lane, suffix)\n return '__lane:' .. tostring(string.len(lane)) .. ':' .. lane .. ':' .. suffix\nend\nlocal function enqueue(field, idx, lane)\n redis.call('ZADD', KEYS[5], idx, field)\n if lane == '' then\n redis.call('ZADD', KEYS[6], idx, field)\n return\n end\n local tail_key = lane_field(lane, 'tail')\n local tail = redis.call('HGET', KEYS[4], tail_key)\n if tail then\n redis.call('HSET', KEYS[4], tail .. ':next', field)\n else\n redis.call('HSET', KEYS[4], lane_field(lane, 'head'), field)\n redis.call('ZADD', KEYS[6], idx, field)\n end\n redis.call('HSET', KEYS[4], tail_key, field)\nend\nlocal n = tonumber(ARGV[3])\nlocal base = tonumber(redis.call('GET', KEYS[2]) or '0')\nlocal has_pending = redis.call('ZCARD', KEYS[5]) > 0 and '1' or '0'\nif n == 0 then return { 'empty', has_pending } end\nlocal dups = {}\nfor i = 1, n do\n local id = ARGV[3 + (i - 1) * 6 + 1]\n local existing = redis.call('HGET', KEYS[3], id)\n if existing then\n local rec = cjson.decode(existing)\n if rec[1] ~= ARGV[1] then return { 'foreign', id } end\n dups[#dups + 1] = tostring(rec[2])\n end\nend\nif #dups == n then\n local res = { 'dup', has_pending }\n for i = 1, #dups do res[#res + 1] = dups[i] end\n return res\nend\nif #dups > 0 then return { 'partial', tostring(#dups) } end\nfor i = 1, n do\n local cause = ARGV[3 + (i - 1) * 6 + 4]\n if cause ~= '' then\n local parsed = cjson.decode(cause)\n local pidx = tonumber(parsed.index)\n if pidx < 1 or pidx > base then\n return { 'no_parent', tostring(parsed.index) }\n end\n local field = tostring(pidx)\n local dispatches = tonumber(redis.call('HGET', KEYS[4], field .. ':d') or '0')\n if dispatches ~= tonumber(parsed.attempt) or\n redis.call('HEXISTS', KEYS[4], field .. ':f') == 1 then\n return { 'superseded', field }\n end\n end\nend\nfor i = 1, n do\n local id = ARGV[3 + (i - 1) * 6 + 1]\n local typ = ARGV[3 + (i - 1) * 6 + 2]\n local payload = ARGV[3 + (i - 1) * 6 + 3]\n local cause = ARGV[3 + (i - 1) * 6 + 4]\n local lane = ARGV[3 + (i - 1) * 6 + 5]\n local settled = ARGV[3 + (i - 1) * 6 + 6]\n local idx = base + i\n redis.call('XADD', KEYS[1], tostring(idx) .. '-0',\n 'id', id, 'type', typ, 'payload', payload, 'created_at', ARGV[2],\n 'cause', cause, 'lane', lane)\n redis.call('HSET', KEYS[3], id, cjson.encode({ ARGV[1], idx }))\n redis.call('HSET', KEYS[4], tostring(idx) .. ':l', lane)\n if settled == '1' then\n redis.call('HSET', KEYS[4], tostring(idx) .. ':p', ARGV[2])\n else\n enqueue(tostring(idx), idx, lane)\n end\nend\nredis.call('SET', KEYS[2], tostring(base + n))\nhas_pending = redis.call('ZCARD', KEYS[5]) > 0 and '1' or '0'\nif redis.call('EXISTS', KEYS[7]) == 1 then\n redis.pcall('PUBLISH', ARGV[4 + n * 6], tostring(base + n))\nend\nreturn { 'ok', tostring(base + 1), has_pending }\n`\n\n/**\n * KEYS: counter, meta, pending, ready, event snapshot pins, snapshot pin counts.\n * ARGV: idx, attempt, error, maxFailures, nowMs.\n */\nconst FAIL_ATTEMPT_LUA = `\nlocal function release_snapshot_pins()\n local refs = redis.call('SMEMBERS', KEYS[5])\n for _, ref in ipairs(refs) do\n local count = redis.call('HINCRBY', KEYS[6], ref, -1)\n if count <= 0 then\n redis.call('HDEL', KEYS[6], ref)\n local checkpoint = cjson.decode(ref)\n redis.call('ZREMRANGEBYSCORE', checkpoint[1], checkpoint[2], checkpoint[2])\n end\n end\n redis.call('DEL', KEYS[5])\n if redis.call('HLEN', KEYS[6]) == 0 then redis.call('DEL', KEYS[6]) end\nend\nlocal function lane_field(lane, suffix)\n return '__lane:' .. tostring(string.len(lane)) .. ':' .. lane .. ':' .. suffix\nend\nlocal function settle(field)\n redis.call('ZREM', KEYS[3], field)\n redis.call('ZREM', KEYS[4], field)\n local lane = redis.call('HGET', KEYS[2], field .. ':l') or ''\n if lane == '' then return end\n local head_key = lane_field(lane, 'head')\n if redis.call('HGET', KEYS[2], head_key) ~= field then return end\n local next_field = redis.call('HGET', KEYS[2], field .. ':next')\n while next_field and redis.call('HEXISTS', KEYS[2], next_field .. ':p') == 1 do\n next_field = redis.call('HGET', KEYS[2], next_field .. ':next')\n end\n if next_field then\n redis.call('HSET', KEYS[2], head_key, next_field)\n if redis.call('HEXISTS', KEYS[2], next_field .. ':f') == 0 then\n redis.call('ZADD', KEYS[4], tonumber(next_field), next_field)\n end\n else\n redis.call('HDEL', KEYS[2], head_key, lane_field(lane, 'tail'))\n end\nend\nlocal idx = tonumber(ARGV[1])\nif idx < 1 or idx > tonumber(redis.call('GET', KEYS[1]) or '0') then\n return { 'missing', '0' }\nend\nlocal field = ARGV[1]\nlocal failures = tonumber(redis.call('HGET', KEYS[2], field .. ':a') or '0')\nlocal dispatches = tonumber(redis.call('HGET', KEYS[2], field .. ':d') or '0')\nif redis.call('HEXISTS', KEYS[2], field .. ':p') == 1 or dispatches ~= tonumber(ARGV[2]) then\n return { 'superseded', tostring(failures) }\nend\nif redis.call('HEXISTS', KEYS[2], field .. ':f') == 1 then\n return { 'dead_lettered', tostring(failures) }\nend\nif redis.call('HEXISTS', KEYS[2], field .. ':lfa') == 1 and\n tonumber(redis.call('HGET', KEYS[2], field .. ':lfa')) == tonumber(ARGV[2]) and\n redis.call('HEXISTS', KEYS[2], field .. ':ch') == 0 then\n return { 'failed', tostring(failures) }\nend\nif redis.call('HEXISTS', KEYS[2], field .. ':ch') == 0 then\n return { 'superseded', tostring(failures) }\nend\nfailures = redis.call('HINCRBY', KEYS[2], field .. ':a', 1)\nredis.call('HSET', KEYS[2],\n field .. ':e', ARGV[3],\n field .. ':lf', ARGV[5],\n field .. ':lfa', ARGV[2])\nredis.call('HDEL', KEYS[2], field .. ':ch', field .. ':ce')\nif failures >= tonumber(ARGV[4]) then\n redis.call('HSET', KEYS[2], field .. ':f', ARGV[5])\n redis.call('ZREM', KEYS[4], field)\n release_snapshot_pins()\n return { 'dead_lettered', tostring(failures) }\nend\nreturn { 'failed', tostring(failures) }\n`\n\n/**\n * KEYS: store, counter, meta, pending, ready.\n * ARGV: holder, nowMs, expiresAtMs, excludedCount, excluded indexes.\n */\nconst CLAIM_AVAILABLE_LUA = `\nif redis.call('ZCARD', KEYS[4]) == 0 then return { 'settled' } end\nlocal ready = redis.call('ZRANGE', KEYS[5], 0, -1)\nlocal excluded = {}\nfor i = 1, tonumber(ARGV[4]) do excluded[ARGV[4 + i]] = true end\nlocal claimed = {}\nlocal retry_at = nil\nfor _, field in ipairs(ready) do\n if not excluded[field] then\n local expires = tonumber(redis.call('HGET', KEYS[3], field .. ':ce') or '0')\n if expires > tonumber(ARGV[2]) then\n if not retry_at or expires < retry_at then retry_at = expires end\n else\n local entries = redis.call('XRANGE', KEYS[1], field .. '-0', field .. '-0')\n if #entries == 0 then return { 'missing', field } end\n local attempt = redis.call('HINCRBY', KEYS[3], field .. ':d', 1)\n redis.call('HSETNX', KEYS[3], field .. ':fc', ARGV[2])\n redis.call('HSET', KEYS[3],\n field .. ':lc', ARGV[2],\n field .. ':ch', ARGV[1],\n field .. ':ce', ARGV[3])\n claimed[#claimed + 1] = {\n tostring(attempt),\n redis.call('HGET', KEYS[3], field .. ':a') or '0',\n redis.call('HGET', KEYS[3], field .. ':e') or false,\n redis.call('HGET', KEYS[3], field .. ':fc') or false,\n ARGV[2],\n redis.call('HGET', KEYS[3], field .. ':lf') or false,\n redis.call('HGET', KEYS[3], field .. ':lfa') or false,\n ARGV[1],\n ARGV[3],\n entries[1]\n }\n end\n end\nend\nif #claimed > 0 then\n local result = { 'claimed', tostring(#claimed) }\n for _, claim in ipairs(claimed) do\n for _, value in ipairs(claim) do result[#result + 1] = value end\n end\n return result\nend\nif retry_at then return { 'busy', tostring(retry_at) } end\nreturn { 'settled' }\n`\n\n/**\n * KEYS: counter, meta.\n * ARGV: holder, nowMs, expiresAtMs, n, then per claim: index, attempt.\n * Returns [renewedCount, ...renewed, ...superseded].\n */\nconst RENEW_CLAIMS_LUA = `\nlocal total = tonumber(redis.call('GET', KEYS[1]) or '0')\nlocal renewed = {}\nlocal superseded = {}\nfor i = 1, tonumber(ARGV[4]) do\n local field = ARGV[4 + (i - 1) * 2 + 1]\n local attempt = tonumber(ARGV[4 + (i - 1) * 2 + 2])\n local idx = tonumber(field)\n if idx >= 1 and idx <= total then\n local dispatches = tonumber(redis.call('HGET', KEYS[2], field .. ':d') or '0')\n if dispatches > attempt then\n superseded[#superseded + 1] = field\n else\n local expires = tonumber(redis.call('HGET', KEYS[2], field .. ':ce') or '0')\n if tonumber(ARGV[3]) > tonumber(ARGV[2]) and\n redis.call('HEXISTS', KEYS[2], field .. ':p') == 0 and\n redis.call('HEXISTS', KEYS[2], field .. ':f') == 0 and\n redis.call('HGET', KEYS[2], field .. ':ch') == ARGV[1] and\n expires > tonumber(ARGV[2]) then\n redis.call('HSET', KEYS[2], field .. ':ce', ARGV[3])\n renewed[#renewed + 1] = field\n end\n end\n end\nend\nlocal result = { tostring(#renewed) }\nfor i = 1, #renewed do result[#result + 1] = renewed[i] end\nfor i = 1, #superseded do result[#result + 1] = superseded[i] end\nreturn result\n`\n\n/**\n * KEYS: store, counter, ids, meta, pending, ready, watched,\n * event snapshot pins, snapshot pin counts.\n * ARGV: sessionId, nowMs, parent index, attempt, n, then per child:\n * id, type, payloadJson, lane, settled; finally notify channel.\n */\nconst COMPLETE_ATTEMPT_LUA = `\nlocal function release_snapshot_pins()\n local refs = redis.call('SMEMBERS', KEYS[8])\n for _, ref in ipairs(refs) do\n local count = redis.call('HINCRBY', KEYS[9], ref, -1)\n if count <= 0 then\n redis.call('HDEL', KEYS[9], ref)\n local checkpoint = cjson.decode(ref)\n redis.call('ZREMRANGEBYSCORE', checkpoint[1], checkpoint[2], checkpoint[2])\n end\n end\n redis.call('DEL', KEYS[8])\n if redis.call('HLEN', KEYS[9]) == 0 then redis.call('DEL', KEYS[9]) end\nend\nlocal function lane_field(lane, suffix)\n return '__lane:' .. tostring(string.len(lane)) .. ':' .. lane .. ':' .. suffix\nend\nlocal function settle(field)\n redis.call('ZREM', KEYS[5], field)\n redis.call('ZREM', KEYS[6], field)\n local lane = redis.call('HGET', KEYS[4], field .. ':l') or ''\n if lane == '' then return end\n local head_key = lane_field(lane, 'head')\n if redis.call('HGET', KEYS[4], head_key) ~= field then return end\n local next_field = redis.call('HGET', KEYS[4], field .. ':next')\n while next_field and redis.call('HEXISTS', KEYS[4], next_field .. ':p') == 1 do\n next_field = redis.call('HGET', KEYS[4], next_field .. ':next')\n end\n if next_field then\n redis.call('HSET', KEYS[4], head_key, next_field)\n if redis.call('HEXISTS', KEYS[4], next_field .. ':f') == 0 then\n redis.call('ZADD', KEYS[6], tonumber(next_field), next_field)\n end\n else\n redis.call('HDEL', KEYS[4], head_key, lane_field(lane, 'tail'))\n end\nend\nlocal function enqueue(field, idx, lane)\n redis.call('ZADD', KEYS[5], idx, field)\n if lane == '' then\n redis.call('ZADD', KEYS[6], idx, field)\n return\n end\n local tail_key = lane_field(lane, 'tail')\n local tail = redis.call('HGET', KEYS[4], tail_key)\n if tail then\n redis.call('HSET', KEYS[4], tail .. ':next', field)\n else\n redis.call('HSET', KEYS[4], lane_field(lane, 'head'), field)\n redis.call('ZADD', KEYS[6], idx, field)\n end\n redis.call('HSET', KEYS[4], tail_key, field)\nend\nlocal parent = ARGV[3]\nlocal attempt = tonumber(ARGV[4])\nlocal n = tonumber(ARGV[5])\nlocal total = tonumber(redis.call('GET', KEYS[2]) or '0')\nif tonumber(parent) < 1 or tonumber(parent) > total then return { 'missing' } end\nlocal dispatches = tonumber(redis.call('HGET', KEYS[4], parent .. ':d') or '0')\nlocal processed = redis.call('HGET', KEYS[4], parent .. ':p')\nlocal processed_attempt = tonumber(redis.call('HGET', KEYS[4], parent .. ':pa') or '0')\nif processed then\n if processed_attempt ~= attempt then return { 'superseded' } end\n local raw_recorded_ids = redis.call('HGET', KEYS[4], parent .. ':ri')\n if not raw_recorded_ids then return { 'partial' } end\n local recorded_ids = cjson.decode(raw_recorded_ids)\n if #recorded_ids ~= n then return { 'partial' } end\n local indexes = {}\n for i = 1, n do\n local id = ARGV[5 + (i - 1) * 5 + 1]\n if recorded_ids[i] ~= id then return { 'partial' } end\n local existing = redis.call('HGET', KEYS[3], id)\n if not existing then return { 'partial' } end\n local rec = cjson.decode(existing)\n if rec[1] ~= ARGV[1] then return { 'partial' } end\n local entries = redis.call('XRANGE', KEYS[1], tostring(rec[2]) .. '-0', tostring(rec[2]) .. '-0')\n if #entries == 0 then return { 'partial' } end\n local values = entries[1][2]\n local cause = nil\n for j = 1, #values, 2 do\n if values[j] == 'cause' then cause = values[j + 1] break end\n end\n if not cause or cause == '' then return { 'partial' } end\n local decoded = cjson.decode(cause)\n if tonumber(decoded['index']) ~= tonumber(parent) or\n tonumber(decoded['attempt']) ~= attempt then return { 'partial' } end\n indexes[#indexes + 1] = tostring(rec[2])\n end\n local result = { 'duplicate' }\n for _, idx in ipairs(indexes) do result[#result + 1] = idx end\n return result\nend\nif dispatches ~= attempt then return { 'superseded' } end\nif redis.call('HEXISTS', KEYS[4], parent .. ':ch') == 0 or\n redis.call('HEXISTS', KEYS[4], parent .. ':f') == 1 or\n tonumber(redis.call('HGET', KEYS[4], parent .. ':lfa') or '0') == attempt then\n return { 'superseded' }\nend\nfor i = 1, n do\n local id = ARGV[5 + (i - 1) * 5 + 1]\n if redis.call('HEXISTS', KEYS[3], id) == 1 then return { 'partial' } end\nend\nlocal returned_ids = {}\nfor i = 1, n do returned_ids[i] = ARGV[5 + (i - 1) * 5 + 1] end\nlocal returned_json = '[]'\nif n > 0 then returned_json = cjson.encode(returned_ids) end\nredis.call('HSET', KEYS[4],\n parent .. ':p', ARGV[2],\n parent .. ':pa', ARGV[4],\n parent .. ':ri', returned_json)\nredis.call('HDEL', KEYS[4], parent .. ':ch', parent .. ':ce')\nsettle(parent)\nrelease_snapshot_pins()\nlocal cause = cjson.encode({ index = tonumber(parent), attempt = attempt })\nfor i = 1, n do\n local id = ARGV[5 + (i - 1) * 5 + 1]\n local typ = ARGV[5 + (i - 1) * 5 + 2]\n local payload = ARGV[5 + (i - 1) * 5 + 3]\n local lane = ARGV[5 + (i - 1) * 5 + 4]\n local settled = ARGV[5 + (i - 1) * 5 + 5]\n local idx = total + i\n redis.call('XADD', KEYS[1], tostring(idx) .. '-0',\n 'id', id, 'type', typ, 'payload', payload, 'created_at', ARGV[2],\n 'cause', cause, 'lane', lane)\n redis.call('HSET', KEYS[3], id, cjson.encode({ ARGV[1], idx }))\n redis.call('HSET', KEYS[4], tostring(idx) .. ':l', lane)\n if settled == '1' then\n redis.call('HSET', KEYS[4], tostring(idx) .. ':p', ARGV[2])\n else\n enqueue(tostring(idx), idx, lane)\n end\nend\nredis.call('SET', KEYS[2], tostring(total + n))\nif n > 0 and redis.call('EXISTS', KEYS[7]) == 1 then\n redis.pcall('PUBLISH', ARGV[6 + n * 5], tostring(total + n))\nend\nreturn { 'ok', tostring(total + 1) }\n`\n\n/**\n * KEYS: head, history, event counter, event meta, pin counts.\n * ARGV: nowMs, event pin key prefix, write count, then per write:\n * index, snapshot JSON, pin count, pin event indexes.\n */\nconst PUT_SNAPSHOTS_LUA = `\nlocal total = tonumber(redis.call('GET', KEYS[3]) or '0')\nlocal writes = {}\nlocal offset = 4\nfor i = 1, tonumber(ARGV[3]) do\n local index = tonumber(ARGV[offset])\n local snapshot = ARGV[offset + 1]\n local pin_count = tonumber(ARGV[offset + 2])\n local ref = cjson.encode({ KEYS[2], tostring(index) })\n for p = 1, pin_count do\n local event_index = tonumber(ARGV[offset + 2 + p])\n if event_index >= 1 and event_index <= total and\n redis.call('HEXISTS', KEYS[4], tostring(event_index) .. ':p') == 0 and\n redis.call('HEXISTS', KEYS[4], tostring(event_index) .. ':f') == 0 then\n local pin_key = ARGV[2] .. tostring(event_index)\n if redis.call('SADD', pin_key, ref) == 1 then\n redis.call('HINCRBY', KEYS[5], ref, 1)\n end\n end\n end\n writes[#writes + 1] = { index = index, snapshot = snapshot, ref = ref }\n offset = offset + 3 + pin_count\nend\n\nlocal head = redis.call('GET', KEYS[1])\nlocal head_index = nil\nif head then head_index = tonumber(cjson.decode(head)['index']) end\nlocal latest = writes[#writes]\nlocal writes_head = not head_index or latest.index >= head_index\nlocal advances = not head_index or latest.index > head_index\nlocal final_head_index = advances and latest.index or head_index\n\nif advances and head and tonumber(redis.call('HGET', KEYS[5], cjson.encode({ KEYS[2], tostring(head_index) })) or '0') > 0 then\n redis.call('ZREMRANGEBYSCORE', KEYS[2], head_index, head_index)\n redis.call('ZADD', KEYS[2], head_index, head)\nend\n\nfor _, write in ipairs(writes) do\n if write.index < final_head_index then\n redis.call('ZREMRANGEBYSCORE', KEYS[2], write.index, write.index)\n if tonumber(redis.call('HGET', KEYS[5], write.ref) or '0') > 0 then\n redis.call('ZADD', KEYS[2], write.index, write.snapshot)\n end\n end\nend\n\nif writes_head then\n redis.call('SET', KEYS[1], latest.snapshot)\n redis.call('ZREMRANGEBYSCORE', KEYS[2], latest.index, latest.index)\nend\nreturn 1\n`\n\n/**\n * KEYS: presence hash. ARGV: atMs, ttlMs, n, then per entry:\n * hashField, valueJson ('' deletes), patchFieldJson; finally channel and\n * message prefix. Wire fragments stay JSON text to preserve arrays\n * and numeric precision through Lua. Field-wise LWW by the sender's\n * `at`: a stored entry with a strictly newer stamp survives; ties go\n * to the incoming write (memory-tier parity). Returns the applied\n * 1-based entry positions. The whole-key PEXPIRE is the coarse bound,\n * sound because core sends one uniform ttlMs; the per-entry `e` stamp\n * is the fine bound (the injected storage clock at the applied set\n * plus ttlMs — never the sender stamp), pruned on read against the\n * same clock.\n */\nconst PRESENCE_SET_LUA = `\nlocal at = tonumber(ARGV[1])\nlocal applied = {}\nlocal published = {}\nlocal n = tonumber(ARGV[3])\nfor i = 1, n do\n local field = ARGV[3 + (i - 1) * 3 + 1]\n local value = ARGV[3 + (i - 1) * 3 + 2]\n local existing = redis.call('HGET', KEYS[1], field)\n if not existing or tonumber(cjson.decode(existing)['a']) <= at then\n if value == '' then\n redis.call('HDEL', KEYS[1], field)\n else\n redis.call('HSET', KEYS[1], field, value)\n end\n applied[#applied + 1] = tostring(i)\n published[#published + 1] = ARGV[3 + (i - 1) * 3 + 3]\n end\nend\nif #applied > 0 then\n redis.call('PEXPIRE', KEYS[1], ARGV[2])\n redis.pcall('PUBLISH', ARGV[4 + n * 3], ARGV[5 + n * 3] .. table.concat(published, ',') .. '}}')\nend\nreturn applied\n`\n\n/**\n * KEYS: presence hash. ARGV: nowMs (the injected clock, so tests can\n * time-travel expiry against a real server). Returns the live\n * [field, value, ...] pairs; entries at or past their fine-bound\n * expiry are HDELed in the same atomic step, so a stale reader can\n * never sweep a concurrently refreshed field.\n */\nconst PRESENCE_READ_LUA = `\nlocal now = tonumber(ARGV[1])\nlocal rows = redis.call('HGETALL', KEYS[1])\nlocal live = {}\nfor i = 1, #rows, 2 do\n if tonumber(cjson.decode(rows[i + 1])['e']) <= now then\n redis.call('HDEL', KEYS[1], rows[i])\n else\n live[#live + 1] = rows[i]\n live[#live + 1] = rows[i + 1]\n end\nend\nreturn live\n`\n\n/**\n * KEYS: head, history, event counter. ARGV: snapshot frontier or ''.\n * Returns one atomic multiversion-cache-plus-log-frontier read.\n */\nconst READ_STATE_LUA = `\nlocal head = redis.call('GET', KEYS[1])\nlocal head_index = head and cjson.decode(head)['index'] or nil\nif head and (type(head_index) ~= 'number' or head_index < 0 or head_index > 9007199254740991 or head_index ~= math.floor(head_index)) then\n return redis.error_reply('invalid snapshot head index')\nend\nlocal cap = ARGV[1]\nlocal snapshot = nil\nif head_index and (cap == '' or head_index <= tonumber(cap)) then\n snapshot = head\nelse\n local upper = cap == '' and '+inf' or cap\n local historical = redis.call('ZREVRANGEBYSCORE', KEYS[2], upper, '-inf', 'LIMIT', 0, 1)\n if #historical > 0 then snapshot = historical[1] end\nend\nlocal frontier = redis.call('GET', KEYS[3]) or '0'\nreturn { head_index or '', snapshot or '', frontier }\n`\n\n// ── wire helpers ─────────────────────────────────────────────────────\n\ntype StreamEntry = [id: string, fields: string[]]\n\nconst fieldMap = (fields: string[]): Record<string, string> => {\n const map: Record<string, string> = {}\n for (let i = 0; i + 1 < fields.length; i += 2) {\n map[fields[i]!] = fields[i + 1]!\n }\n return map\n}\n\nconst STORED_EVENT_META_SUFFIXES = [\n 'p',\n 'pa',\n 'ri',\n 'ch',\n 'ce',\n 'f',\n 'fc',\n 'lc',\n 'lf',\n 'lfa',\n 'd',\n 'a',\n 'e',\n] as const\n\nconst entryIndex = (entryId: string): number =>\n Number(entryId.slice(0, entryId.indexOf('-')))\n\nconst toEvent = (sessionId: string, entry: StreamEntry): Event => {\n const fields = fieldMap(entry[1])\n return {\n id: fields['id']!,\n type: fields['type']!,\n payload: JSON.parse(fields['payload']!),\n index: entryIndex(entry[0]),\n sessionId,\n createdAt: new Date(Number(fields['created_at'])),\n }\n}\n\nconst json = (value: unknown): string => JSON.stringify(value) ?? 'null'\n\nconst toCause = (raw: string | undefined): EventCause | null => {\n if (raw === '' || raw === undefined) return null\n const value = JSON.parse(raw) as {\n index?: unknown\n attempt?: unknown\n batchSize?: unknown\n }\n if (\n !Number.isInteger(value.index) ||\n Number(value.index) < 1 ||\n !Number.isInteger(value.attempt) ||\n Number(value.attempt) < 1\n ) {\n throw new TypeError('stored event has an invalid cause')\n }\n if (\n value.batchSize !== undefined &&\n (!Number.isInteger(value.batchSize) || Number(value.batchSize) < 1)\n ) {\n throw new TypeError('stored event has an invalid cause')\n }\n return {\n index: Number(value.index),\n attempt: Number(value.attempt),\n ...(value.batchSize === undefined\n ? {}\n : { batchSize: Number(value.batchSize) }),\n }\n}\n\nconst toClaimAvailableResult = (\n sessionId: string,\n reply: unknown[],\n): StoreClaimAvailableResult => {\n const outcome = String(reply[0])\n if (outcome === 'claimed') {\n const count = Number(reply[1])\n const events: StoredEvent[] = []\n for (let i = 0; i < count; i++) {\n const offset = 2 + i * 10\n const entry = reply[offset + 9] as StreamEntry\n const event = toEvent(sessionId, entry)\n const fields = fieldMap(entry[1])\n events.push(\n Object.assign(event, {\n cause: toCause(fields['cause']),\n lane: fields['lane'] === '' ? null : fields['lane']!,\n processedAt: null,\n processedByAttempt: null,\n returnedEventIds: null,\n firstClaimedAt:\n reply[offset + 3] === null\n ? null\n : new Date(Number(reply[offset + 3])),\n lastClaimedAt: new Date(Number(reply[offset + 4])),\n attemptCount: Number(reply[offset]),\n claimHolder: String(reply[offset + 7]),\n claimExpiresAt: new Date(Number(reply[offset + 8])),\n failureCount: Number(reply[offset + 1]),\n lastFailedAt:\n reply[offset + 5] === null\n ? null\n : new Date(Number(reply[offset + 5])),\n lastFailedAttempt:\n reply[offset + 6] === null ? null : Number(reply[offset + 6]),\n lastError:\n reply[offset + 2] === null ? null : String(reply[offset + 2]),\n failedAt: null,\n }),\n )\n }\n return {\n outcome: 'claimed',\n events,\n }\n }\n if (outcome === 'busy') {\n return { outcome, retryAt: new Date(Number(reply[1])) }\n }\n if (outcome === 'settled') return { outcome }\n throw new TypeError(\n `claim referenced a missing event in session '${sessionId}'`,\n )\n}\n\nconst wrap = async <T>(fn: () => Promise<T>): Promise<T> => {\n try {\n return await fn()\n } catch (err) {\n if (err instanceof A2Error || err instanceof TypeError) throw err\n throw new A2Error('STORE_UNAVAILABLE', 'redis store operation failed', {\n cause: err,\n })\n }\n}\n\nexport function createRedisStoreCore(\n options: RedisStoreCoreOptions,\n): RedisStoreCore {\n const { call, clock } = options\n const generateId = options.ids\n const prefix = options.keyPrefix\n\n const storeKey = (s: string): string => `${prefix}:${s}:log`\n const countKey = (s: string): string => `${prefix}:${s}:count`\n const metaKey = (s: string): string => `${prefix}:${s}:meta`\n const pendingKey = (s: string): string => `${prefix}:${s}:pending`\n const readyKey = (s: string): string => `${prefix}:${s}:ready`\n const snapKey = (s: string, r: string): string => `${prefix}:${s}:snap:${r}`\n const snapHistoryKey = (s: string, r: string): string =>\n `${prefix}:${s}:snap-history:${r}`\n const snapshotEventPinsKey = (s: string, index: number): string =>\n `${prefix}:${s}:snapshot-pins:${index}`\n const snapshotEventPinsPrefix = (s: string): string =>\n `${prefix}:${s}:snapshot-pins:`\n const snapshotPinCountsKey = (s: string): string =>\n `${prefix}:${s}:snapshot-pin-counts`\n const watchedKey = (s: string): string => `${prefix}:${s}:watched`\n const presenceKey = (s: string): string => `${prefix}:${s}:presence`\n const idsKey = `${prefix}:ids`\n\n const evalScript = (\n script: string,\n keys: string[],\n args: Array<string | number>,\n ): Promise<unknown> => call('EVAL', script, keys.length, ...keys, ...args)\n\n const readFrontier = async (sessionId: string): Promise<number> => {\n const frontier = Number((await call('GET', countKey(sessionId))) ?? 0)\n if (!Number.isSafeInteger(frontier) || frontier < 0) {\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n `redis event count for session '${sessionId}' is invalid`,\n )\n }\n return frontier\n }\n\n const readRangePage = async (\n sessionId: string,\n afterIndex: number,\n throughIndex: number,\n limit: number,\n ): Promise<StoredEvent[]> => {\n if (throughIndex <= afterIndex) return []\n const entries = (await call(\n 'XRANGE',\n storeKey(sessionId),\n `${afterIndex + 1}-0`,\n `${throughIndex}-0`,\n 'COUNT',\n limit,\n )) as StreamEntry[]\n if (entries.length === 0) return []\n\n const metaFields = entries.flatMap((entry) => {\n const index = entryIndex(entry[0])\n return STORED_EVENT_META_SUFFIXES.map((suffix) => `${index}:${suffix}`)\n })\n const metaValues =\n ((await call('HMGET', metaKey(sessionId), ...metaFields)) as Array<\n string | null\n > | null) ?? []\n const meta: Record<string, string> = {}\n for (let i = 0; i < metaFields.length; i += 1) {\n const value = metaValues[i]\n if (value !== null && value !== undefined) {\n meta[metaFields[i]!] = value\n }\n }\n return entries.map((entry) => {\n const event = toEvent(sessionId, entry)\n const idx = event.index\n const fields = fieldMap(entry[1])\n const processedAt = meta[`${idx}:p`]\n const processedByAttempt = meta[`${idx}:pa`]\n const returnedEventIds = meta[`${idx}:ri`]\n const claimHolder = meta[`${idx}:ch`]\n const claimExpiresAt = meta[`${idx}:ce`]\n const failedAt = meta[`${idx}:f`]\n const firstClaimedAt = meta[`${idx}:fc`]\n const lastClaimedAt = meta[`${idx}:lc`]\n const lastFailedAt = meta[`${idx}:lf`]\n const lastFailedAttempt = meta[`${idx}:lfa`]\n return Object.assign(event, {\n cause: toCause(fields['cause']),\n lane: fields['lane'] === '' ? null : fields['lane']!,\n processedAt:\n processedAt === undefined ? null : new Date(Number(processedAt)),\n processedByAttempt:\n processedByAttempt === undefined ? null : Number(processedByAttempt),\n returnedEventIds:\n returnedEventIds === undefined\n ? null\n : decodeReturnedEventIds(returnedEventIds),\n firstClaimedAt:\n firstClaimedAt === undefined\n ? null\n : new Date(Number(firstClaimedAt)),\n lastClaimedAt:\n lastClaimedAt === undefined ? null : new Date(Number(lastClaimedAt)),\n attemptCount: Number(meta[`${idx}:d`] ?? 0),\n claimHolder: claimHolder ?? null,\n claimExpiresAt:\n claimExpiresAt === undefined\n ? null\n : new Date(Number(claimExpiresAt)),\n failureCount: Number(meta[`${idx}:a`] ?? 0),\n lastFailedAt:\n lastFailedAt === undefined ? null : new Date(Number(lastFailedAt)),\n lastFailedAttempt:\n lastFailedAttempt === undefined ? null : Number(lastFailedAttempt),\n lastError: meta[`${idx}:e`] ?? null,\n failedAt: failedAt === undefined ? null : new Date(Number(failedAt)),\n })\n })\n }\n\n const readEventPages = async (\n sessionId: string,\n afterIndex: number,\n throughIndex: number,\n ): Promise<Event[]> => {\n if (throughIndex <= afterIndex) return []\n const events: Event[] = []\n let nextIndex = afterIndex + 1\n while (nextIndex <= throughIndex) {\n // oxlint-disable-next-line no-await-in-loop -- each page advances the dense frozen range\n const entries = (await call(\n 'XRANGE',\n storeKey(sessionId),\n `${nextIndex}-0`,\n `${throughIndex}-0`,\n 'COUNT',\n READ_PAGE_SIZE,\n )) as StreamEntry[]\n if (entries.length === 0) {\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n `redis event range for session '${sessionId}' ended before index ${throughIndex}`,\n )\n }\n for (let position = 0; position < entries.length; position += 1) {\n const index = entryIndex(entries[position]![0])\n if (index !== nextIndex + position || index > throughIndex) {\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n `redis event range for session '${sessionId}' is not contiguous`,\n )\n }\n }\n events.push(...entries.map((entry) => toEvent(sessionId, entry)))\n nextIndex = events.at(-1)!.index + 1\n }\n return events\n }\n\n const readEventRange = async (\n sessionId: string,\n afterIndex: number,\n throughIndex: number,\n ): Promise<Event[]> => {\n const events: Event[] = []\n for (\n let start = afterIndex;\n start < throughIndex;\n start += READ_PAGE_SIZE * READ_CONCURRENCY\n ) {\n const ranges = Array.from(\n {\n length: Math.min(\n READ_CONCURRENCY,\n Math.ceil((throughIndex - start) / READ_PAGE_SIZE),\n ),\n },\n (_, position) => {\n const after = start + position * READ_PAGE_SIZE\n return readEventPages(\n sessionId,\n after,\n Math.min(after + READ_PAGE_SIZE, throughIndex),\n )\n },\n )\n // oxlint-disable-next-line no-await-in-loop -- bound in-flight reads and settle every page before returning\n const results = await Promise.allSettled(ranges)\n for (const result of results) {\n if (result.status === 'rejected') throw result.reason\n events.push(...result.value)\n }\n }\n return events\n }\n\n /** Bounded XRANGE pages restore the full-read contract on capped providers. */\n const readRange = async (\n sessionId: string,\n afterIndex: number,\n throughIndex?: number,\n ): Promise<StoredEvent[]> => {\n if (throughIndex !== undefined && throughIndex <= afterIndex) return []\n const currentFrontier = await readFrontier(sessionId)\n const frontier = Math.min(throughIndex ?? currentFrontier, currentFrontier)\n if (frontier <= afterIndex) return []\n\n const rows: StoredEvent[] = []\n let nextIndex = afterIndex + 1\n while (nextIndex <= frontier) {\n // oxlint-disable-next-line no-await-in-loop -- each page advances the dense frozen range\n const page = await readRangePage(\n sessionId,\n nextIndex - 1,\n frontier,\n READ_PAGE_SIZE,\n )\n if (page.length === 0) {\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n `redis event range for session '${sessionId}' ended before index ${frontier}`,\n )\n }\n for (let position = 0; position < page.length; position += 1) {\n if (page[position]!.index !== nextIndex + position) {\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n `redis event range for session '${sessionId}' is not contiguous`,\n )\n }\n }\n const lastIndex = page.at(-1)!.index\n if (lastIndex < nextIndex || lastIndex > frontier) {\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n `redis event range for session '${sessionId}' did not advance`,\n )\n }\n rows.push(...page)\n nextIndex = lastIndex + 1\n }\n return rows\n }\n\n return {\n async append(sessionId, events) {\n return wrap(async () => {\n const supplied = events.filter((e) => e.id !== undefined)\n const suppliedIds = supplied.map((e) => e.id as string)\n if (new Set(suppliedIds).size !== suppliedIds.length) {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n 'batch contains the same event id more than once',\n )\n }\n const now = clock.now().getTime()\n const withIds = events.map((e) => ({\n id: e.id ?? generateId(),\n type: e.type,\n payload: e.payload,\n cause: e.cause ? { ...e.cause } : null,\n lane: e.lane ?? null,\n settled: e.settled === true,\n }))\n const args: Array<string | number> = [\n sessionId,\n now,\n withIds.length,\n ...withIds.flatMap((e) => [\n e.id,\n e.type,\n json(e.payload),\n e.cause ? json(e.cause) : '',\n e.lane ?? '',\n e.settled ? '1' : '0',\n ]),\n `${prefix}:${sessionId}:notify`,\n ]\n const reply = (await evalScript(\n APPEND_LUA,\n [\n storeKey(sessionId),\n countKey(sessionId),\n idsKey,\n metaKey(sessionId),\n pendingKey(sessionId),\n readyKey(sessionId),\n watchedKey(sessionId),\n ],\n args,\n )) as string[]\n\n const [verdict] = reply\n if (verdict === 'empty') {\n return { events: [], hasPending: reply[1] === '1' }\n }\n if (verdict === 'foreign') {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n `event id '${reply[1]}' already exists in another session`,\n )\n }\n if (verdict === 'no_parent') {\n throw new TypeError(\n `no event at index ${reply[1]} in session '${sessionId}'`,\n )\n }\n if (verdict === 'superseded') {\n throw new A2Error(\n 'SUPERSEDED_ATTEMPT',\n `a newer attempt owns event ${reply[1]} in session '${sessionId}'`,\n )\n }\n if (verdict === 'partial') {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n `batch mixes ${reply[1]} already-appended and ${events.length - Number(reply[1])} fresh events`,\n )\n }\n if (verdict === 'dup') {\n // A retry of a committed batch whose ack was lost — return\n // the original rows, ordered by index like every adapter.\n const hasPending = reply[1] === '1'\n const wanted = new Set(reply.slice(2).map(Number))\n const min = Math.min(...wanted)\n const rows = await readRange(sessionId, min - 1)\n const existing = rows.filter((row) => wanted.has(row.index))\n return {\n events: idempotentReplay(events, existing),\n hasPending,\n }\n }\n const base = Number(reply[1]) - 1\n return {\n events: withIds.map((e, i) => ({\n id: e.id,\n type: e.type,\n payload: structuredClone(e.payload),\n index: base + 1 + i,\n sessionId,\n createdAt: new Date(now),\n cause: e.cause,\n lane: e.lane,\n processedAt: e.settled ? new Date(now) : null,\n processedByAttempt: null,\n returnedEventIds: null,\n firstClaimedAt: null,\n lastClaimedAt: null,\n attemptCount: 0,\n claimHolder: null,\n claimExpiresAt: null,\n failureCount: 0,\n lastFailedAt: null,\n lastFailedAttempt: null,\n lastError: null,\n failedAt: null,\n })),\n hasPending: reply[2] === '1',\n }\n })\n },\n\n async read(sessionId, opts) {\n return wrap(async () => {\n const rows = await readRange(\n sessionId,\n opts?.afterIndex ?? 0,\n opts?.throughIndex,\n )\n return rows\n })\n },\n\n async readHistory(sessionId, opts) {\n return wrap(async () => {\n const after = opts?.afterIndex ?? 0\n if (opts?.throughIndex !== undefined && opts.throughIndex <= after)\n return []\n const frontier = await readFrontier(sessionId)\n return readEventRange(\n sessionId,\n after,\n Math.min(opts?.throughIndex ?? frontier, frontier),\n )\n })\n },\n\n async claimAvailable({\n sessionId,\n holder,\n ttlMs,\n expiresAtMs,\n excludeIndexes,\n }) {\n return wrap(async () => {\n const now = clock.now().getTime()\n const expiresAt = expiresAtMs ?? now + ttlMs\n const excluded = excludeIndexes ?? []\n const reply = (await evalScript(\n CLAIM_AVAILABLE_LUA,\n [\n storeKey(sessionId),\n countKey(sessionId),\n metaKey(sessionId),\n pendingKey(sessionId),\n readyKey(sessionId),\n ],\n [holder, now, expiresAt, excluded.length, ...excluded],\n )) as unknown[]\n return toClaimAvailableResult(sessionId, reply)\n })\n },\n\n async renewClaims({ sessionId, holder, claims, ttlMs, expiresAtMs }) {\n return wrap(async () => {\n const now = clock.now().getTime()\n const expiresAt = expiresAtMs ?? now + ttlMs\n const reply = (await evalScript(\n RENEW_CLAIMS_LUA,\n [countKey(sessionId), metaKey(sessionId)],\n [\n holder,\n now,\n expiresAt,\n claims.length,\n ...claims.flatMap((claim) => [claim.index, claim.attempt]),\n ],\n )) as string[]\n const renewedCount = Number(reply[0])\n const values = reply.slice(1).map(Number)\n return {\n renewed: values.slice(0, renewedCount).toSorted((a, b) => a - b),\n superseded: values.slice(renewedCount).toSorted((a, b) => a - b),\n }\n })\n },\n\n async completeAttempt({ sessionId, index, attempt, events }) {\n return wrap(async () => {\n const ids = events.map((event) => event.id)\n if (new Set(ids).size !== ids.length) {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n 'batch contains the same event id more than once',\n )\n }\n const now = clock.now().getTime()\n const reply = (await evalScript(\n COMPLETE_ATTEMPT_LUA,\n [\n storeKey(sessionId),\n countKey(sessionId),\n idsKey,\n metaKey(sessionId),\n pendingKey(sessionId),\n readyKey(sessionId),\n watchedKey(sessionId),\n snapshotEventPinsKey(sessionId, index),\n snapshotPinCountsKey(sessionId),\n ],\n [\n sessionId,\n now,\n index,\n attempt,\n events.length,\n ...events.flatMap((event) => [\n event.id,\n event.type,\n json(event.payload),\n event.lane ?? '',\n event.settled === true ? '1' : '0',\n ]),\n `${prefix}:${sessionId}:notify`,\n ],\n )) as string[]\n const outcome = reply[0]\n if (outcome === 'missing') {\n throw new TypeError(\n `no event at index ${index} in session '${sessionId}'`,\n )\n }\n if (outcome === 'partial') {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n 'returned event ids do not match the completed attempt',\n )\n }\n if (outcome === 'superseded') return { outcome }\n\n const cause = { index, attempt }\n if (outcome === 'ok') {\n const base = Number(reply[1]) - 1\n return {\n outcome: 'completed',\n events: events.map((event, i) => ({\n id: event.id,\n type: event.type,\n payload: structuredClone(event.payload),\n index: base + 1 + i,\n sessionId,\n createdAt: new Date(now),\n cause,\n lane: event.lane ?? null,\n processedAt: event.settled === true ? new Date(now) : null,\n processedByAttempt: null,\n returnedEventIds: null,\n firstClaimedAt: null,\n lastClaimedAt: null,\n attemptCount: 0,\n claimHolder: null,\n claimExpiresAt: null,\n failureCount: 0,\n lastFailedAt: null,\n lastFailedAttempt: null,\n lastError: null,\n failedAt: null,\n })),\n } satisfies CompleteAttemptResult\n }\n if (outcome === 'duplicate') {\n if (events.length === 0) {\n return { outcome: 'completed', events: [] }\n }\n const wanted = new Set(reply.slice(1).map(Number))\n const min = Math.min(...wanted)\n const rows = await readRange(sessionId, min - 1)\n const byIndex = new Map(rows.map((row) => [row.index, row]))\n const existing = reply\n .slice(1)\n .map((value) => byIndex.get(Number(value))!)\n return {\n outcome: 'completed',\n events: idempotentReplay(events, existing),\n }\n }\n throw new TypeError(`unexpected completion outcome '${outcome}'`)\n })\n },\n\n async failAttempt({ sessionId, index, attempt, error, maxFailures }) {\n return wrap(async () => {\n const reply = (await evalScript(\n FAIL_ATTEMPT_LUA,\n [\n countKey(sessionId),\n metaKey(sessionId),\n pendingKey(sessionId),\n readyKey(sessionId),\n snapshotEventPinsKey(sessionId, index),\n snapshotPinCountsKey(sessionId),\n ],\n [index, attempt, error, maxFailures, clock.now().getTime()],\n )) as string[]\n const outcome = reply[0]\n if (outcome === 'missing') {\n throw new TypeError(\n `no event at index ${index} in session '${sessionId}'`,\n )\n }\n return {\n outcome: outcome as 'failed' | 'dead_lettered' | 'superseded',\n failureCount: Number(reply[1]),\n }\n })\n },\n\n async readState(sessionId, reducerName, stateOptions) {\n return wrap(async () => {\n const reply = (await evalScript(\n READ_STATE_LUA,\n [\n snapKey(sessionId, reducerName),\n snapHistoryKey(sessionId, reducerName),\n countKey(sessionId),\n ],\n [\n stateOptions?.snapshotThroughIndex ??\n stateOptions?.throughIndex ??\n '',\n ],\n )) as [number | string, string, string]\n const rawHead = reply[0]\n const raw = reply[1]\n const headIndex = rawHead === '' ? null : Number(rawHead)\n const parsed =\n raw === ''\n ? null\n : (JSON.parse(raw) as { index: number; state: unknown })\n const currentFrontier = Number(reply[2])\n if (\n !Number.isSafeInteger(currentFrontier) ||\n currentFrontier < 0 ||\n (headIndex !== null &&\n (!Number.isSafeInteger(headIndex) || headIndex < 0)) ||\n (parsed !== null &&\n (!Number.isSafeInteger(parsed.index) || parsed.index < 0))\n ) {\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n `redis state frontier for session '${sessionId}' is invalid`,\n )\n }\n const frontier = Math.min(\n currentFrontier,\n stateOptions?.throughIndex ?? currentFrontier,\n )\n const snapshot = parsed\n const events = await readEventRange(\n sessionId,\n snapshot?.index ?? 0,\n frontier,\n )\n return {\n headIndex,\n snapshot: snapshot\n ? { index: snapshot.index, state: snapshot.state }\n : null,\n events,\n }\n })\n },\n\n async putSnapshots(sessionId, reducerName, writes) {\n const byIndex = new Map<\n number,\n { index: number; state: unknown; pinEventIndexes: Set<number> }\n >()\n for (const write of writes) {\n const existing = byIndex.get(write.index)\n if (existing) {\n existing.state = write.state\n for (const eventIndex of write.pinEventIndexes ?? []) {\n existing.pinEventIndexes.add(eventIndex)\n }\n } else {\n byIndex.set(write.index, {\n index: write.index,\n state: write.state,\n pinEventIndexes: new Set(write.pinEventIndexes ?? []),\n })\n }\n }\n const normalized = [...byIndex.values()].toSorted(\n (a, b) => a.index - b.index,\n )\n if (normalized.length === 0) return\n await wrap(async () => {\n const args: Array<string | number> = [\n clock.now().getTime(),\n snapshotEventPinsPrefix(sessionId),\n normalized.length,\n ]\n for (const write of normalized) {\n args.push(\n write.index,\n json({\n index: write.index,\n state: write.state,\n updatedAt: clock.now().getTime(),\n }),\n write.pinEventIndexes.size,\n ...write.pinEventIndexes,\n )\n }\n await evalScript(\n PUT_SNAPSHOTS_LUA,\n [\n snapKey(sessionId, reducerName),\n snapHistoryKey(sessionId, reducerName),\n countKey(sessionId),\n metaKey(sessionId),\n snapshotPinCountsKey(sessionId),\n ],\n args,\n )\n })\n },\n\n presence: {\n async set(ns, participant, values, meta) {\n return wrap(async () => {\n const entries = Object.entries(values)\n if (entries.length === 0) return null\n const atMs = meta.at.getTime()\n // Expiry anchors on the injected storage clock — the sender's\n // `a` stamp orders writes, the `e` stamp bounds their\n // lifetime, and the two clocks never mix.\n const expiresAtMs = clock.now().getTime() + meta.ttlMs\n // The hash field is JSON `[participant, field]`: both are\n // caller strings, so no separator character is\n // collision-safe — the JSON array is.\n const applied = (await evalScript(\n PRESENCE_SET_LUA,\n [presenceKey(ns)],\n [\n atMs,\n meta.ttlMs,\n entries.length,\n ...entries.flatMap(([field, value]) => [\n JSON.stringify([participant, field]),\n value === null\n ? ''\n : json({\n v: value,\n s: meta.seen,\n a: atMs,\n e: expiresAtMs,\n }),\n `${json(field)}:${json(value)}`,\n ]),\n presenceKey(ns),\n `${json({ src: options.presenceSource, participant, seen: meta.seen, at: atMs }).slice(0, -1)},\"values\":{`,\n ],\n )) as string[]\n if (applied.length === 0) return null\n // Field-keyed and caller-named — null-prototype, like every\n // presence map (see `nullProtoRecord`).\n const appliedValues: Record<string, unknown> = nullProtoRecord()\n for (const position of applied) {\n const [field, value] = entries[Number(position) - 1]!\n appliedValues[field] = structuredClone(value)\n }\n return {\n participant,\n values: appliedValues,\n seen: meta.seen,\n at: new Date(atMs),\n }\n })\n },\n\n async read(ns) {\n return wrap(async () => {\n const reply = (await evalScript(\n PRESENCE_READ_LUA,\n [presenceKey(ns)],\n [clock.now().getTime()],\n )) as string[]\n const rows: PresenceRow[] = []\n for (let i = 0; i + 1 < reply.length; i += 2) {\n const [participant, field] = JSON.parse(reply[i]!) as [\n string,\n string,\n ]\n const entry = JSON.parse(reply[i + 1]!) as {\n v: unknown\n s: number\n a: number\n e: number\n }\n rows.push({\n participant,\n field,\n value: entry.v,\n seen: entry.s,\n at: new Date(entry.a),\n expiresAt: new Date(entry.e),\n })\n }\n return rows\n })\n },\n },\n\n async readRange(sessionId, afterIndex, throughIndex) {\n return wrap(() => readRange(sessionId, afterIndex, throughIndex))\n },\n\n async readEvents(sessionId, afterIndex) {\n return wrap(async () => {\n const entries = (await call(\n 'XRANGE',\n storeKey(sessionId),\n `${afterIndex + 1}-0`,\n '+',\n 'COUNT',\n READ_PAGE_SIZE,\n )) as StreamEntry[]\n return entries.map((entry) => toEvent(sessionId, entry))\n })\n },\n\n async markWatched(sessionId, ttlMs) {\n await wrap(() => call('SET', watchedKey(sessionId), '1', 'PX', ttlMs))\n },\n }\n}\n","import type { PresencePatch } from './contract.ts'\nimport { A2Error } from './errors.ts'\nimport { NOTIFY_TIMINGS } from './internal.ts'\nimport { defaultSleep, pollingStream, type Sleeper } from './store-polling.ts'\nimport { pollPresence } from './store-presence-polling.ts'\nimport { presenceRecovery } from './presence-recovery.ts'\nimport { createStreamActivity, streamActivities } from './stream-activity.ts'\nimport { createRedisStoreCore } from './store-redis-core.ts'\nimport type { A2Store, Clock, Event, IdSource } from './store.ts'\n\nexport type RedisConnection = {\n call(command: string, ...args: Array<string | number>): Promise<unknown>\n duplicate(): RedisConnection\n on(\n event: 'message',\n listener: (channel: string, message: string) => void,\n ): unknown\n disconnect(): void\n}\n\ntype PresenceWireMessage = {\n src: string\n participant: string\n values: Record<string, unknown>\n seen: number\n at: number\n}\n\ntype PresenceSubscription = {\n listeners: Set<(patch: PresencePatch) => void>\n poll?: ReturnType<typeof pollPresence> | undefined\n recovery?: ReturnType<typeof presenceRecovery> | undefined\n}\n\nexport function createRedisNotifyStore(options: {\n client: () => Promise<RedisConnection>\n closeClient: () => Promise<void>\n clock: Clock\n ids: IdSource\n keyPrefix: string\n onReconnect?: (listener: (channel: string) => void) => void\n polling?: () => boolean\n}): A2Store & { close(): Promise<void> } {\n const { client, clock, ids, keyPrefix: prefix } = options\n const notifyChannel = (sessionId: string): string =>\n `${prefix}:${sessionId}:notify`\n const presenceChannel = (ns: string): string => `${prefix}:${ns}:presence`\n\n const instanceId = crypto.randomUUID()\n const core = createRedisStoreCore({\n call: async (command, ...args) => {\n const c = await client()\n return c.call(command, ...args)\n },\n clock,\n ids,\n keyPrefix: prefix,\n presenceSource: instanceId,\n })\n\n let severed = false\n let subscriber: Promise<RedisConnection> | null = null\n /** channel → parked-feed wake-ups, armed before each catch-up read. */\n const wakers = new Map<string, Set<() => void>>()\n /** channel → refcounted subscription held for each live iterator. */\n const subscriptions = new Map<\n string,\n { refs: number; ready: Promise<unknown> }\n >()\n /**\n * channel → last presence-marker refresh, throttling the SET.\n * Deliberately real time (`Date.now()`), not the injected clock:\n * the marker's PX expiry runs on server real time, and the throttle\n * must tick with the TTL it refreshes or time-traveling tests would\n * desync the two. Process-local and never stored, so the injected\n * clock's every-stored-timestamp pledge is untouched.\n */\n const watched = new Map<string, { at: number; ready: Promise<void> }>()\n const presenceSubscriptions = new Map<string, PresenceSubscription>()\n const pollingStreams = new Set<AsyncIterator<Event>>()\n\n options.onReconnect?.((channel) => {\n for (const wake of wakers.get(channel) ?? []) wake()\n void presenceSubscriptions\n .get(channel)\n ?.recovery?.reconnect()\n .catch(() => {})\n })\n\n const deliverRemotePresence = ({\n subscription,\n message,\n }: {\n subscription: PresenceSubscription\n message: string\n }): void => {\n let parsed: PresenceWireMessage\n try {\n parsed = JSON.parse(message) as PresenceWireMessage\n } catch {\n return\n }\n if (parsed.src === instanceId) return\n const patch: PresencePatch = {\n participant: parsed.participant,\n values: parsed.values,\n seen: parsed.seen,\n at: new Date(parsed.at),\n }\n subscription.recovery?.update(patch)\n for (const listener of subscription.listeners) listener(patch)\n }\n\n const getSubscriber = (): Promise<RedisConnection> => {\n subscriber ??= (async () => {\n const conn = (await client()).duplicate()\n conn.on('message', (channel, message) => {\n // wake() only resolves a promise; removal happens later via\n // disarm(), so iterating the live set is safe.\n const set = wakers.get(channel)\n if (set) {\n for (const wake of set) wake()\n }\n const subscription = presenceSubscriptions.get(channel)\n if (subscription) deliverRemotePresence({ subscription, message })\n })\n return conn\n })()\n return subscriber\n }\n\n /**\n * The subscriber, gated on connection readiness — for channel\n * commands only (`close` uses the ungated promise, so it can never\n * hang on an unreachable server). SUBSCRIBE carries redis's\n * ok-loading flag, so ioredis writes it mid-handshake, ahead of the\n * ready check's INFO — which then finds the connection already in\n * subscriber mode and fails the check (a spurious error event plus\n * a reconnect). ECHO has no such flag: it resolves only once the\n * connection is ready, so every channel command chained behind it\n * lands after the check.\n */\n let subscriberGate: Promise<RedisConnection> | null = null\n const gatedSubscriber = (): Promise<RedisConnection> => {\n subscriberGate ??= getSubscriber().then(async (conn) => {\n await conn.call('echo', 'ready')\n return conn\n })\n return subscriberGate\n }\n\n const acquireChannel = async (channel: string): Promise<void> => {\n const existing = subscriptions.get(channel)\n if (existing) {\n existing.refs += 1\n try {\n await existing.ready\n } catch (err) {\n existing.refs -= 1\n throw err\n }\n return\n }\n const lease: { refs: number; ready: Promise<void> } = {\n refs: 1,\n // Lowercase on purpose: ioredis keys its subscriber-mode\n // bookkeeping (and reconnect resubscription) on the exact\n // command name.\n ready: gatedSubscriber().then(async (conn) => {\n await conn.call('subscribe', channel)\n if (subscriptions.get(channel) !== lease || severed) return\n const presence = presenceSubscriptions.get(channel)\n if (options.polling?.() && presence) {\n presence.recovery = undefined\n const ns = channel.slice(prefix.length + 1, -':presence'.length)\n presence.poll = pollPresence({\n read: () => core.presence.read(ns),\n deliver: (patch) => {\n for (const listener of presence.listeners) listener(patch)\n },\n })\n await presence.poll.ready\n }\n return undefined\n }),\n }\n subscriptions.set(channel, lease)\n try {\n await lease.ready\n } catch (err) {\n if (subscriptions.get(channel) === lease) {\n subscriptions.delete(channel)\n const presence = presenceSubscriptions.get(channel)\n presence?.poll?.stop()\n if (presence) presence.poll = undefined\n }\n throw err\n }\n }\n\n const releaseChannel = (channel: string): void => {\n const lease = subscriptions.get(channel)\n if (!lease) return\n lease.refs -= 1\n if (lease.refs > 0) return\n subscriptions.delete(channel)\n watched.delete(channel)\n void gatedSubscriber()\n .then((conn) => conn.call('unsubscribe', channel))\n .catch(() => {})\n }\n\n const armWaker = (\n channel: string,\n ): { wakeup: Promise<void>; wake: () => void; disarm: () => void } => {\n let wake!: () => void\n const wakeup = new Promise<void>((resolve) => {\n wake = resolve // assigned synchronously by the executor\n })\n let set = wakers.get(channel)\n if (!set) {\n set = new Set()\n wakers.set(channel, set)\n }\n set.add(wake)\n return {\n wakeup,\n wake,\n disarm: () => {\n set.delete(wake)\n if (set.size === 0) wakers.delete(channel)\n },\n }\n }\n\n return {\n append: core.append,\n read: core.read,\n readHistory: core.readHistory!,\n claimAvailable: core.claimAvailable,\n renewClaims: core.renewClaims,\n completeAttempt: core.completeAttempt,\n failAttempt: core.failAttempt,\n readState: core.readState,\n putSnapshots: core.putSnapshots,\n\n presence: {\n async set(ns, participant, values, meta) {\n const patch = await core.presence.set(ns, participant, values, meta)\n if (!patch) return\n const channel = presenceChannel(ns)\n const subscription = presenceSubscriptions.get(channel)\n if (subscription) {\n subscription.poll?.update(patch)\n subscription.recovery?.update(patch)\n for (const listener of subscription.listeners) listener(patch)\n }\n },\n\n async read(ns) {\n const channel = presenceChannel(ns)\n await subscriptions.get(channel)?.ready\n return (\n presenceSubscriptions.get(channel)?.recovery?.read() ??\n core.presence.read(ns)\n )\n },\n\n subscribe(ns, onPatch) {\n const channel = presenceChannel(ns)\n let subscription = presenceSubscriptions.get(channel)\n if (!subscription) {\n subscription = { listeners: new Set() }\n presenceSubscriptions.set(channel, subscription)\n if (options.onReconnect && !options.polling?.()) {\n const current = subscription\n subscription.recovery = presenceRecovery({\n read: () => core.presence.read(ns),\n deliver: (patch) => {\n for (const listener of current.listeners) listener(patch)\n },\n isActive: () =>\n !severed && presenceSubscriptions.get(channel) === current,\n })\n }\n }\n subscription.listeners.add(onPatch)\n // Fire-and-forget on the shared refcounted subscription: it\n // only carries remote patches, so settling late (or failing)\n // degrades cross-instance latency, never same-process\n // delivery.\n void acquireChannel(channel).catch(() => {})\n let stopped = false\n return () => {\n if (stopped) return\n stopped = true\n subscription.listeners.delete(onPatch)\n if (subscription.listeners.size === 0) {\n subscription.poll?.stop()\n presenceSubscriptions.delete(channel)\n }\n releaseChannel(channel)\n }\n },\n },\n\n stream(sessionId, opts) {\n const startAfter = opts?.startAfter ?? 0\n const channel = notifyChannel(sessionId)\n\n return {\n [Symbol.asyncIterator](): AsyncIterator<Event> {\n let last = startAfter\n let buffer: Event[] = []\n let closed = false\n let subscribed = false\n let pending: Sleeper | null = null\n let interrupt: (() => void) | null = null\n let poller: AsyncIterator<Event> | undefined\n let stopActivity: (() => void) | undefined\n const activity = createStreamActivity(() => {\n if (poller) streamActivities.get(poller)?.activate()\n })\n\n const release = (): void => {\n stopActivity?.()\n if (poller) {\n void poller.return?.()\n pollingStreams.delete(poller)\n }\n if (!subscribed) return\n subscribed = false\n releaseChannel(channel)\n }\n\n const iterator: AsyncIterator<Event> = {\n async next(): Promise<IteratorResult<Event>> {\n for (;;) {\n if (closed) {\n release()\n return { value: undefined, done: true }\n }\n const row = buffer.shift()\n if (row) {\n last = row.index\n return { value: row, done: false }\n }\n if (severed) {\n release()\n throw new A2Error('STORE_UNAVAILABLE', 'the store was closed')\n }\n const waker = armWaker(channel)\n interrupt = waker.wake\n try {\n if (!subscribed) {\n // Subscribe before reading: anything appended\n // after the read lands as a wake-up, so nothing\n // can slip between catch-up and park.\n subscribed = true\n // oxlint-disable-next-line no-await-in-loop -- one-time setup\n await acquireChannel(channel)\n if (closed || severed) continue\n }\n if (options.polling?.()) {\n if (!poller) {\n poller = pollingStream(\n (after) => core.readEvents(sessionId, after),\n { startAfter: last },\n )[Symbol.asyncIterator]()\n pollingStreams.add(poller)\n stopActivity = streamActivities\n .get(poller)\n ?.subscribe(activity.setIdle)\n }\n // oxlint-disable-next-line no-await-in-loop -- delegate the fallback to the shared poller\n const result = await poller.next()\n if (!result.done)\n presenceSubscriptions\n .get(presenceChannel(sessionId))\n ?.poll?.wake()\n if (severed)\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n 'the store was closed',\n )\n return result\n }\n // Mark watched before reading, for the same reason:\n // a write after this read sees the marker and\n // publishes. Throttled — one SET per safety period\n // per session per process, shared across feeds.\n let marker = watched.get(channel)\n if (\n !marker ||\n Date.now() - marker.at >= NOTIFY_TIMINGS.safetyReadMs\n ) {\n marker = {\n at: Date.now(),\n ready: core.markWatched(\n sessionId,\n NOTIFY_TIMINGS.safetyReadMs * 3,\n ),\n }\n watched.set(channel, marker)\n }\n // Every viewer waits until writers can observe the marker.\n // oxlint-disable-next-line no-await-in-loop -- live-feed loop\n await marker.ready\n if (closed || severed) continue\n // oxlint-disable-next-line no-await-in-loop -- live-feed loop\n buffer = await core.readEvents(sessionId, last)\n if (buffer.length === 0 && !closed && !severed) {\n pending = defaultSleep(NOTIFY_TIMINGS.safetyReadMs)\n // oxlint-disable-next-line no-await-in-loop -- live-feed loop\n await Promise.race([waker.wakeup, pending.promise])\n pending.cancel()\n pending = null\n }\n } catch (err) {\n release()\n if (closed) return { value: undefined, done: true }\n if (err instanceof A2Error) throw err\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n 'redis store operation failed',\n { cause: err },\n )\n } finally {\n waker.disarm()\n interrupt = null\n }\n }\n },\n async return(): Promise<IteratorResult<Event>> {\n closed = true\n pending?.cancel()\n interrupt?.() // wakes an in-flight next() immediately\n release()\n return { value: undefined, done: true }\n },\n }\n streamActivities.set(iterator, activity)\n return iterator\n },\n }\n },\n\n async close() {\n severed = true\n for (const subscription of presenceSubscriptions.values())\n subscription.poll?.stop()\n presenceSubscriptions.clear()\n for (const poller of pollingStreams) void poller.return?.()\n pollingStreams.clear()\n for (const set of wakers.values()) {\n for (const wake of set) wake()\n }\n if (subscriber) {\n const sub = await subscriber.catch(() => null)\n sub?.disconnect()\n }\n await options.closeClient()\n },\n }\n}\n"],"mappings":"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"}
|
package/dist/store-redis.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { r as Clock, s as IdSource, t as A2Store } from "./store-
|
|
1
|
+
import { r as Clock, s as IdSource, t as A2Store } from "./store-DyZM6fS5.js";
|
|
2
2
|
//#region src/store-redis-notify.d.ts
|
|
3
3
|
type RedisConnection = {
|
|
4
4
|
call(command: string, ...args: Array<string | number>): Promise<unknown>;
|
package/dist/store-redis.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { t as A2Error } from "./errors-DCk6ch5n.js";
|
|
2
2
|
import { t as retryableLazy } from "./retryable-lazy-DZWmHpii.js";
|
|
3
3
|
import { n as SYSTEM_CLOCK, t as RANDOM_IDS } from "./store-N8PXxDAS.js";
|
|
4
|
-
import { t as createRedisNotifyStore } from "./store-redis-notify-
|
|
4
|
+
import { t as createRedisNotifyStore } from "./store-redis-notify-BVLUfI3j.js";
|
|
5
5
|
//#region src/store-redis.ts
|
|
6
6
|
/**
|
|
7
7
|
* experimental-a2/store-redis — the Redis-protocol store backend, on Redis
|
package/dist/store-sqlite.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { r as Clock, s as IdSource, t as A2Store } from "./store-
|
|
1
|
+
import { r as Clock, s as IdSource, t as A2Store } from "./store-DyZM6fS5.js";
|
|
2
2
|
//#region src/store-sqlite.d.ts
|
|
3
3
|
type SqliteStoreOptions = {
|
|
4
4
|
/** Database file path. Defaults to `.a2/dev.db`; `:memory:` works. */
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
/** Attribute values a2 emits. */
|
|
9
9
|
type A2AttributeValue = string | number | boolean;
|
|
10
10
|
/** The spans a2 emits today. The catalogue grows with the surface. */
|
|
11
|
-
type A2SpanName = "a2.append" | "a2.drain" | "a2.event" | "a2.state";
|
|
11
|
+
type A2SpanName = "a2.append" | "a2.drain" | "a2.event" | "a2.state" | "a2.snapshot";
|
|
12
12
|
type A2SpanHandle = {
|
|
13
13
|
/** Attach or update an attribute mid-span (e.g. the outcome). */
|
|
14
14
|
setAttribute(key: string, value: A2AttributeValue): void;
|
|
@@ -30,4 +30,4 @@ type A2Telemetry = {
|
|
|
30
30
|
};
|
|
31
31
|
//#endregion
|
|
32
32
|
export { A2Telemetry as i, A2SpanHandle as n, A2SpanName as r, A2AttributeValue as t };
|
|
33
|
-
//# sourceMappingURL=telemetry-
|
|
33
|
+
//# sourceMappingURL=telemetry-B5jzpy6y.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetry-B5jzpy6y.d.ts","names":[],"sources":["../src/telemetry.ts"],"mappings":";;;;;;;;KAQY;;KAGA;KAGA;;EAEV,aAAa,aAAa,OAAO;;;;;;EAMjC,YAAY;;KAGF;;;;;;;EAOV,KAAK,GACH,MAAM,YACN,YAAY,eAAe,mBAC3B,KAAK,MAAM,iBAAiB,QAAQ,KACnC,QAAQ"}
|
|
@@ -138,7 +138,10 @@ around or write a stale snapshot behind the current head. If no suitable
|
|
|
138
138
|
checkpoint exists, A2 folds the bounded log.
|
|
139
139
|
|
|
140
140
|
The snapshot and its remaining event tail come back in one consistent store
|
|
141
|
-
operation. A missing
|
|
141
|
+
operation. A missing snapshot or one rejected by `stateSchema` rebuilds from the
|
|
142
|
+
log. An operational storage-read failure propagates with its backend diagnostics;
|
|
143
|
+
it does not trigger a full-log read. Existing A2 errors pass through, and other
|
|
144
|
+
errors become `STORE_UNAVAILABLE` with the original error as their cause.
|
|
142
145
|
`state()` is observational: it never runs handlers or waits for pending work.
|
|
143
146
|
Its index marks committed history, not handler completion.
|
|
144
147
|
|
|
@@ -186,9 +189,11 @@ name. That's the entire cache invalidation story: one string.
|
|
|
186
189
|
|
|
187
190
|
Snapshot write-back runs in platform `waitUntil` after the state is ready. One
|
|
188
191
|
same-tick group of reads for a reducer and session reads the widest required log
|
|
189
|
-
range once, folds it once, and writes the needed prefixes together. A failed
|
|
190
|
-
|
|
191
|
-
|
|
192
|
+
range once, folds it once, and writes the needed prefixes together. A failed cache
|
|
193
|
+
write does not fail the completed state read. A2 reports it through the
|
|
194
|
+
`a2.snapshot` telemetry span, or `console.error` by default, with session, reducer,
|
|
195
|
+
checkpoint index, and batch count. It does not attach snapshot contents. The next
|
|
196
|
+
read folds the missing tail again.
|
|
192
197
|
|
|
193
198
|
Deleting every snapshot is always safe. The log rebuilds them.
|
|
194
199
|
|