experimental-a2 0.4.0 → 0.5.1
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 +66 -0
- package/dist/{ai-B4YhEnfw.d.ts → ai-D_PGS-JR.d.ts} +3 -2
- package/dist/ai-D_PGS-JR.d.ts.map +1 -0
- package/dist/ai-server.browser.js +2 -0
- package/dist/ai-server.browser.js.map +1 -0
- package/dist/ai-server.d.ts +4 -3
- package/dist/ai-server.d.ts.map +1 -0
- package/dist/ai-server.js +4 -2
- package/dist/ai-server.js.map +1 -0
- package/dist/ai.d.ts +1 -1
- package/dist/ai.js +3 -1
- package/dist/ai.js.map +1 -0
- package/dist/cli-B3VuxoDe.js +2 -0
- package/dist/cli-B3VuxoDe.js.map +1 -0
- package/dist/cli-bin.js +2 -0
- package/dist/cli-bin.js.map +1 -0
- package/dist/cli.d.ts +2 -1
- package/dist/cli.d.ts.map +1 -0
- package/dist/{client-Bt4tAKi9.js → client-BKlyLiOU.js} +295 -85
- package/dist/client-BKlyLiOU.js.map +1 -0
- package/dist/{client-BrfDXQ8A.d.ts → client-D7mvIXrF.d.ts} +40 -4
- package/dist/client-D7mvIXrF.d.ts.map +1 -0
- package/dist/client.d.ts +2 -2
- package/dist/client.js +1 -1
- package/dist/contract-48bUMgcL.js +2 -0
- package/dist/contract-48bUMgcL.js.map +1 -0
- package/dist/contract-jIfaR085.d.ts +2 -1
- package/dist/contract-jIfaR085.d.ts.map +1 -0
- package/dist/devtools-J_jZ2vQf.d.ts +2 -1
- package/dist/devtools-J_jZ2vQf.d.ts.map +1 -0
- package/dist/devtools-kJJaORn-.js +2 -0
- package/dist/devtools-kJJaORn-.js.map +1 -0
- package/dist/devtools-server.browser.js +2 -0
- package/dist/devtools-server.browser.js.map +1 -0
- package/dist/devtools-server.d.ts +2 -1
- package/dist/devtools-server.d.ts.map +1 -0
- package/dist/devtools-server.js +2 -0
- package/dist/devtools-server.js.map +1 -0
- package/dist/errors-BQuJpe82.js +2 -0
- package/dist/errors-BQuJpe82.js.map +1 -0
- package/dist/errors-W6nwJ-fm.d.ts +2 -1
- package/dist/errors-W6nwJ-fm.d.ts.map +1 -0
- package/dist/http.d.ts +121 -72
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +503 -178
- package/dist/http.js.map +1 -0
- package/dist/idempotent-replay-DuqEkYA7.js +2 -0
- package/dist/idempotent-replay-DuqEkYA7.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/inspection-DaxB5jM2.js +2 -0
- package/dist/inspection-DaxB5jM2.js.map +1 -0
- package/dist/{internal-aEotMzu_.js → internal-DstsI6Re.js} +3 -1
- package/dist/internal-DstsI6Re.js.map +1 -0
- package/dist/otel.d.ts +3 -2
- package/dist/otel.d.ts.map +1 -0
- package/dist/otel.js +2 -0
- package/dist/otel.js.map +1 -0
- package/dist/platform-B4TnJtWu.js +2 -0
- package/dist/platform-B4TnJtWu.js.map +1 -0
- package/dist/react.d.ts +12 -3
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +5 -1
- package/dist/react.js.map +1 -0
- package/dist/retryable-lazy-DZWmHpii.js +2 -0
- package/dist/retryable-lazy-DZWmHpii.js.map +1 -0
- package/dist/scheduler-qstash.d.ts +4 -3
- package/dist/scheduler-qstash.d.ts.map +1 -0
- package/dist/scheduler-qstash.js +4 -2
- package/dist/scheduler-qstash.js.map +1 -0
- package/dist/scheduler-task-BpzhPnRS.js +2 -0
- package/dist/scheduler-task-BpzhPnRS.js.map +1 -0
- package/dist/scheduler-vercel.d.ts +4 -3
- package/dist/scheduler-vercel.d.ts.map +1 -0
- package/dist/scheduler-vercel.js +4 -2
- package/dist/scheduler-vercel.js.map +1 -0
- package/dist/{server-YtPq7hjw.d.ts → server-DpvjhdoE.d.ts} +6 -5
- package/dist/server-DpvjhdoE.d.ts.map +1 -0
- package/dist/{server-CcNnFnoW.js → server-Duw6MVlB.js} +112 -54
- package/dist/server-Duw6MVlB.js.map +1 -0
- package/dist/server.browser.js +2 -0
- package/dist/server.browser.js.map +1 -0
- package/dist/server.d.ts +2 -2
- package/dist/server.js +1 -1
- package/dist/{store-C3sNAaBT.d.ts → store-DysUkTH3.d.ts} +10 -1
- package/dist/store-DysUkTH3.d.ts.map +1 -0
- package/dist/store-N8PXxDAS.js +2 -0
- package/dist/store-N8PXxDAS.js.map +1 -0
- package/dist/store-codec-DTG0Ftek.js +2 -0
- package/dist/store-codec-DTG0Ftek.js.map +1 -0
- package/dist/store-memory.d.ts +3 -2
- package/dist/store-memory.d.ts.map +1 -0
- package/dist/store-memory.js +19 -11
- package/dist/store-memory.js.map +1 -0
- package/dist/{store-polling-DgrrAE3d.js → store-polling-dSeLxzfb.js} +3 -1
- package/dist/store-polling-dSeLxzfb.js.map +1 -0
- package/dist/store-postgres.d.ts +3 -2
- package/dist/store-postgres.d.ts.map +1 -0
- package/dist/store-postgres.js +57 -1
- package/dist/store-postgres.js.map +1 -0
- package/dist/{store-redis-core-DWqx3F47.js → store-redis-core-BFLwz0Wj.js} +3 -1
- package/dist/store-redis-core-BFLwz0Wj.js.map +1 -0
- package/dist/store-redis-http.d.ts +3 -2
- package/dist/store-redis-http.d.ts.map +1 -0
- package/dist/store-redis-http.js +4 -2
- package/dist/store-redis-http.js.map +1 -0
- package/dist/store-redis.d.ts +3 -2
- package/dist/store-redis.d.ts.map +1 -0
- package/dist/store-redis.js +5 -3
- package/dist/store-redis.js.map +1 -0
- package/dist/store-sqlite.d.ts +3 -2
- package/dist/store-sqlite.d.ts.map +1 -0
- package/dist/store-sqlite.js +3 -1
- package/dist/store-sqlite.js.map +1 -0
- package/dist/{telemetry-BjYHTfh2.d.ts → telemetry-CpeclqB2.d.ts} +4 -3
- package/dist/telemetry-CpeclqB2.d.ts.map +1 -0
- package/dist/testing.browser.js +2 -0
- package/dist/testing.browser.js.map +1 -0
- package/dist/testing.d.ts +2 -1
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +2 -0
- package/dist/testing.js.map +1 -0
- package/dist/validate-XKT4FSNn.js +2 -0
- package/dist/validate-XKT4FSNn.js.map +1 -0
- package/dist/{wire-DCUZBUlT.js → wire-BFQmSJ-9.js} +77 -15
- package/dist/wire-BFQmSJ-9.js.map +1 -0
- package/docs/guides/03-react.mdx +59 -39
- package/docs/guides/06-ai-agents.mdx +5 -27
- package/docs/guides/09-presence.mdx +19 -40
- package/docs/guides/10-transports.mdx +49 -40
- package/docs/reference/01-api.mdx +107 -26
- package/docs/reference/02-errors.mdx +4 -2
- package/package.json +2 -1
- package/src/ai-coordinator.ts +358 -0
- package/src/ai-projector.ts +524 -0
- package/src/ai-sdk-step.ts +261 -0
- package/src/ai-server.browser.ts +5 -0
- package/src/ai-server.ts +1719 -0
- package/src/ai.ts +2155 -0
- package/src/cache-indexeddb.ts +10 -0
- package/src/cli-bin.ts +5 -0
- package/src/cli.ts +1046 -0
- package/src/client.ts +1826 -0
- package/src/contract.ts +206 -0
- package/src/deterministic-id.ts +72 -0
- package/src/devtools-app.ts +989 -0
- package/src/devtools-server.browser.ts +5 -0
- package/src/devtools-server.ts +604 -0
- package/src/devtools.ts +716 -0
- package/src/errors.ts +50 -0
- package/src/http.ts +394 -0
- package/src/idempotent-replay.ts +53 -0
- package/src/index.ts +37 -0
- package/src/inspection.ts +39 -0
- package/src/internal.ts +426 -0
- package/src/otel.ts +59 -0
- package/src/platform.ts +60 -0
- package/src/push-envelope.ts +137 -0
- package/src/react.ts +284 -0
- package/src/reducer.ts +108 -0
- package/src/retryable-lazy.ts +27 -0
- package/src/scheduler-qstash.ts +915 -0
- package/src/scheduler-task.ts +106 -0
- package/src/scheduler-vercel.ts +437 -0
- package/src/server.browser.ts +12 -0
- package/src/server.ts +2700 -0
- package/src/session-socket.ts +548 -0
- package/src/sse.ts +141 -0
- package/src/standard-schema.ts +77 -0
- package/src/store-codec.ts +10 -0
- package/src/store-memory.ts +788 -0
- package/src/store-polling.ts +102 -0
- package/src/store-postgres.ts +1212 -0
- package/src/store-redis-core.ts +1494 -0
- package/src/store-redis-http.ts +116 -0
- package/src/store-redis.ts +458 -0
- package/src/store-sqlite.ts +1108 -0
- package/src/store.ts +385 -0
- package/src/telemetry.ts +54 -0
- package/src/testing.browser.ts +5 -0
- package/src/testing.ts +185 -0
- package/src/validate.ts +39 -0
- package/src/wire.ts +454 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store-memory.js","names":[],"sources":["../src/store-memory.ts"],"sourcesContent":["/**\n * experimental-a2/store-memory — in-memory store backend (the test default).\n *\n * Implements the A2Store interface with zero dependencies. See\n * specs/a2-implementation.md §3 for the contract; the conformance suite\n * in test/conformance is the executable version of it.\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 {\n RANDOM_IDS,\n SYSTEM_CLOCK,\n type A2Store,\n type AppendEvent,\n type Clock,\n type Event,\n type EventCause,\n type IdSource,\n type PresenceRow,\n type StoredEvent,\n type StoredSessionSummary,\n type StoreStateRead,\n} from './store.ts'\n\nexport type MemoryStoreOptions = {\n /** Injectable clock — every stored timestamp comes from here. */\n clock?: Clock\n /** Injectable id source for generated event ids. */\n ids?: IdSource\n}\n\ntype Row = {\n id: string\n type: string\n payload: unknown\n index: number\n sessionId: string\n createdAt: Date\n cause: EventCause | null\n lane: string | null\n processedAt: Date | null\n processedByAttempt: number | null\n returnedEventIds: string[] | null\n firstClaimedAt: Date | null\n lastClaimedAt: Date | null\n attemptCount: number\n claimHolder: string | null\n claimExpiresAt: Date | null\n failureCount: number\n lastFailedAt: Date | null\n lastFailedAttempt: number | null\n lastError: string | null\n failedAt: Date | null\n}\n\ntype PresenceEntry = {\n value: unknown\n seen: number\n at: Date\n expiresAt: Date\n}\n\ntype LaneQueue = { rows: Row[]; head: number }\ntype SessionDispatch = {\n ready: Map<number, Row>\n lanes: Map<string, LaneQueue>\n}\n\nconst toStored = (row: Row): StoredEvent => ({\n id: row.id,\n type: row.type,\n payload: structuredClone(row.payload),\n index: row.index,\n sessionId: row.sessionId,\n createdAt: new Date(row.createdAt),\n cause: row.cause ? { ...row.cause } : null,\n lane: row.lane,\n processedAt: row.processedAt ? new Date(row.processedAt) : null,\n processedByAttempt: row.processedByAttempt,\n returnedEventIds: row.returnedEventIds ? [...row.returnedEventIds] : null,\n firstClaimedAt: row.firstClaimedAt ? new Date(row.firstClaimedAt) : null,\n lastClaimedAt: row.lastClaimedAt ? new Date(row.lastClaimedAt) : null,\n attemptCount: row.attemptCount,\n claimHolder: row.claimHolder,\n claimExpiresAt: row.claimExpiresAt ? new Date(row.claimExpiresAt) : null,\n failureCount: row.failureCount,\n lastFailedAt: row.lastFailedAt ? new Date(row.lastFailedAt) : null,\n lastFailedAttempt: row.lastFailedAttempt,\n lastError: row.lastError,\n failedAt: row.failedAt ? new Date(row.failedAt) : null,\n})\n\nconst toEvent = (row: Row): Event => ({\n id: row.id,\n type: row.type,\n payload: structuredClone(row.payload),\n index: row.index,\n sessionId: row.sessionId,\n createdAt: new Date(row.createdAt),\n})\n\nexport function memory(options: MemoryStoreOptions = {}): A2Store {\n const clock = options.clock ?? SYSTEM_CLOCK\n const generateId = options.ids ?? RANDOM_IDS\n\n const sessions = new Map<string, Row[]>()\n const dispatch = new Map<string, SessionDispatch>()\n /** Global unique index on event ids, like the SQL schema's. */\n const byEventId = new Map<string, Row>()\n /** Keyed by `${sessionId}\\u0000${reducerName}` — a pure cache. */\n const snapshots = new Map<\n string,\n { index: number; state: unknown; updatedAt: Date }\n >()\n const streamSubscribers = new Map<string, Set<(row: Row) => void>>()\n /** ns → participant → field → latest surviving write. */\n const presenceRows = new Map<\n string,\n Map<string, Map<string, PresenceEntry>>\n >()\n const presenceSubscribers = new Map<\n string,\n Set<(patch: PresencePatch) => void>\n >()\n\n const notifyStreams = (sessionId: string, rows: Row[]): void => {\n const subs = streamSubscribers.get(sessionId)\n if (!subs) return\n for (const listener of subs) {\n for (const row of rows) listener(row)\n }\n }\n\n const rowsOf = (sessionId: string): Row[] => {\n let rows = sessions.get(sessionId)\n if (!rows) {\n rows = []\n sessions.set(sessionId, rows)\n }\n return rows\n }\n\n const dispatchOf = (sessionId: string): SessionDispatch => {\n let state = dispatch.get(sessionId)\n if (!state) {\n state = { ready: new Map(), lanes: new Map() }\n dispatch.set(sessionId, state)\n }\n return state\n }\n\n const enqueue = (sessionId: string, row: Row): void => {\n if (row.processedAt !== null) return\n const state = dispatchOf(sessionId)\n if (row.lane === null) {\n state.ready.set(row.index, row)\n return\n }\n let queue = state.lanes.get(row.lane)\n if (!queue) {\n queue = { rows: [], head: 0 }\n state.lanes.set(row.lane, queue)\n }\n queue.rows.push(row)\n if (queue.rows.length === 1) state.ready.set(row.index, row)\n }\n\n const settle = (sessionId: string, row: Row): void => {\n const state = dispatchOf(sessionId)\n state.ready.delete(row.index)\n if (row.lane === null) return\n const queue = state.lanes.get(row.lane)!\n if (queue.rows[queue.head] !== row) return\n queue.head += 1\n if (queue.head === queue.rows.length) {\n state.lanes.delete(row.lane)\n return\n }\n const next = queue.rows[queue.head]!\n if (next.failedAt === null) state.ready.set(next.index, next)\n }\n\n const find = (sessionId: string, index: number): Row => {\n const row = sessions.get(sessionId)?.[index - 1]\n if (!row) {\n throw new TypeError(\n `no event at index ${index} in session '${sessionId}'`,\n )\n }\n return row\n }\n\n const appendResult = (sessionId: string, events: StoredEvent[]) => ({\n events,\n hasPending: (sessions.get(sessionId) ?? []).some(\n (row) => row.processedAt === null,\n ),\n })\n\n const claim = (\n row: Row,\n holder: string,\n now: Date,\n expiresAt: Date,\n ): StoredEvent => {\n row.attemptCount += 1\n row.firstClaimedAt ??= new Date(now)\n row.lastClaimedAt = new Date(now)\n row.claimHolder = holder\n row.claimExpiresAt = new Date(expiresAt)\n return toStored(row)\n }\n\n const checkBatchIds = (\n sessionId: string,\n events: readonly (AppendEvent & { id?: string })[],\n ): Row[] => {\n const supplied = events.filter(\n (candidate): candidate is AppendEvent & { id: string } =>\n candidate.id !== undefined,\n )\n const suppliedIds = new Set(supplied.map((candidate) => candidate.id))\n if (suppliedIds.size !== supplied.length) {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n 'batch contains the same event id more than once',\n )\n }\n const existing = supplied\n .map((candidate) => byEventId.get(candidate.id))\n .filter((row): row is Row => row !== undefined)\n const foreign = existing.find((row) => row.sessionId !== sessionId)\n if (foreign) {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n `event id '${foreign.id}' already exists in another session`,\n )\n }\n return existing\n }\n\n const insert = (\n sessionId: string,\n events: readonly AppendEvent[],\n now: Date,\n cause?: EventCause,\n ): Row[] => {\n const rows = rowsOf(sessionId)\n const base = rows.length === 0 ? 0 : rows[rows.length - 1]!.index\n const inserted = events.map((candidate, offset): Row => ({\n id: candidate.id ?? generateId(),\n type: candidate.type,\n payload: structuredClone(candidate.payload),\n index: base + 1 + offset,\n sessionId,\n createdAt: new Date(now),\n cause: cause\n ? { ...cause }\n : candidate.cause\n ? { ...candidate.cause }\n : null,\n lane: candidate.lane ?? null,\n processedAt: candidate.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 for (const row of inserted) {\n if (byEventId.has(row.id)) {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n `event id '${row.id}' already exists`,\n )\n }\n }\n for (const row of inserted) {\n rows.push(row)\n byEventId.set(row.id, row)\n enqueue(sessionId, row)\n }\n return inserted\n }\n\n const readStateOf = (\n sessionId: string,\n reducerName: string,\n ): StoreStateRead => {\n const snap = snapshots.get(`${sessionId}\\u0000${reducerName}`)\n const snapshot = snap\n ? { index: snap.index, state: structuredClone(snap.state) }\n : null\n const afterIndex = snapshot?.index ?? 0\n return {\n snapshot,\n events: (sessions.get(sessionId) ?? [])\n .filter((row) => row.index > afterIndex)\n .map(toEvent),\n }\n }\n\n return {\n async append(sessionId, events) {\n if (events.length === 0) return appendResult(sessionId, [])\n\n // Batch-scoped idempotency (a2-implementation.md §3): all ids\n // already present → lost-ack retry, return the original rows;\n // some present → the caller mixed sent and fresh events.\n const existing = checkBatchIds(sessionId, events)\n if (existing.length > 0) {\n if (existing.length === events.length) {\n return appendResult(\n sessionId,\n idempotentReplay(events, existing.map(toStored)),\n )\n }\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n `batch mixes ${existing.length} already-appended and ${events.length - existing.length} fresh events`,\n )\n }\n\n // Attempt-currency fence: a fresh handler append commits only while\n // its causal attempt is still the parent's latest (store.ts `append`).\n const rows = sessions.get(sessionId) ?? []\n for (const event of events) {\n if (!event.cause) continue\n const parent = rows[event.cause.index - 1]\n if (!parent) {\n throw new TypeError(\n `no event at index ${event.cause.index} in session '${sessionId}'`,\n )\n }\n if (\n parent.attemptCount !== event.cause.attempt ||\n parent.failedAt !== null\n ) {\n throw new A2Error(\n 'SUPERSEDED_ATTEMPT',\n `attempt ${event.cause.attempt} no longer owns event ${event.cause.index} in session '${sessionId}'`,\n )\n }\n }\n\n const now = clock.now()\n const inserted = insert(sessionId, events, now)\n notifyStreams(sessionId, inserted)\n return appendResult(sessionId, inserted.map(toStored))\n },\n\n async read(sessionId, opts) {\n const rows = sessions.get(sessionId) ?? []\n const start = Math.max(0, opts?.afterIndex ?? 0)\n const end = Math.max(0, opts?.throughIndex ?? rows.length)\n return rows.slice(start, end).map(toStored)\n },\n\n async claimAvailable({\n sessionId,\n holder,\n ttlMs,\n expiresAtMs,\n excludeIndexes,\n }) {\n const claimedAt = clock.now()\n const now = claimedAt.getTime()\n const expiresAt = new Date(expiresAtMs ?? now + ttlMs)\n const excluded = new Set(excludeIndexes ?? [])\n const eligible: Row[] = []\n let retryAt: Date | null = null\n\n for (const row of dispatchOf(sessionId).ready.values()) {\n if (row.failedAt !== null || excluded.has(row.index)) continue\n if (row.claimExpiresAt && row.claimExpiresAt.getTime() > now) {\n if (!retryAt || row.claimExpiresAt < retryAt) {\n retryAt = new Date(row.claimExpiresAt)\n }\n continue\n }\n eligible.push(row)\n }\n\n if (eligible.length === 0) {\n return retryAt ? { outcome: 'busy', retryAt } : { outcome: 'settled' }\n }\n eligible.sort((a, b) => a.index - b.index)\n return {\n outcome: 'claimed',\n events: eligible.map((row) => claim(row, holder, claimedAt, expiresAt)),\n }\n },\n\n async renewClaims({ sessionId, holder, claims, ttlMs, expiresAtMs }) {\n const now = clock.now()\n const expiresAt = new Date(expiresAtMs ?? now.getTime() + ttlMs)\n const requested = new Map(claims.map((c) => [c.index, c.attempt]))\n const renewed: number[] = []\n const superseded: number[] = []\n for (const row of sessions.get(sessionId) ?? []) {\n const attempt = requested.get(row.index)\n if (attempt === undefined) continue\n if (row.attemptCount > attempt) {\n superseded.push(row.index)\n continue\n }\n if (\n row.processedAt === null &&\n row.failedAt === null &&\n row.claimHolder === holder &&\n row.claimExpiresAt !== null &&\n row.claimExpiresAt > now\n ) {\n row.claimExpiresAt = new Date(expiresAt)\n renewed.push(row.index)\n }\n }\n return { renewed, superseded }\n },\n\n async completeAttempt({ sessionId, index, attempt, events }) {\n const parent = find(sessionId, index)\n const cause = { index, attempt }\n if (parent.processedAt !== null) {\n if (parent.processedByAttempt !== attempt) {\n return { outcome: 'superseded' }\n }\n const requestedIds = events.map((event) => event.id)\n if (\n parent.returnedEventIds === null ||\n parent.returnedEventIds.length !== requestedIds.length ||\n parent.returnedEventIds.some(\n (id, offset) => id !== requestedIds[offset],\n )\n ) {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n 'completed attempt does not match the returned event batch',\n )\n }\n const existing = checkBatchIds(sessionId, events)\n if (\n existing.length !== events.length ||\n existing.some(\n (row) =>\n row.cause?.index !== index || row.cause.attempt !== attempt,\n )\n ) {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n 'completed attempt does not match the returned event batch',\n )\n }\n return {\n outcome: 'completed',\n events: idempotentReplay(events, existing.map(toStored)),\n }\n }\n if (\n parent.attemptCount !== attempt ||\n parent.failedAt !== null ||\n parent.claimHolder === null\n ) {\n return { outcome: 'superseded' }\n }\n\n const existing = checkBatchIds(sessionId, events)\n if (existing.length > 0) {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n 'returned event batch contains already-appended ids',\n )\n }\n const now = clock.now()\n const inserted = insert(sessionId, events, now, cause)\n parent.processedAt = new Date(now)\n parent.processedByAttempt = attempt\n parent.returnedEventIds = events.map((event) => event.id)\n parent.claimHolder = null\n parent.claimExpiresAt = null\n settle(sessionId, parent)\n notifyStreams(sessionId, inserted)\n return { outcome: 'completed', events: inserted.map(toStored) }\n },\n\n async failAttempt({ sessionId, index, attempt, error, maxFailures }) {\n const row = find(sessionId, index)\n if (\n row.processedAt ||\n row.attemptCount !== attempt ||\n (row.claimHolder === null && row.lastFailedAttempt !== attempt)\n ) {\n return { outcome: 'superseded', failureCount: row.failureCount }\n }\n if (row.lastFailedAttempt === attempt && row.claimHolder === null) {\n return {\n outcome: row.failedAt ? 'dead_lettered' : 'failed',\n failureCount: row.failureCount,\n }\n }\n if (row.failedAt) {\n return { outcome: 'dead_lettered', failureCount: row.failureCount }\n }\n row.failureCount += 1\n row.lastError = error\n const failedAt = clock.now()\n row.lastFailedAt = new Date(failedAt)\n row.lastFailedAttempt = attempt\n row.claimHolder = null\n row.claimExpiresAt = null\n if (row.failureCount >= maxFailures) {\n row.failedAt = new Date(failedAt)\n dispatchOf(sessionId).ready.delete(row.index)\n return {\n outcome: 'dead_lettered',\n failureCount: row.failureCount,\n }\n }\n return { outcome: 'failed', failureCount: row.failureCount }\n },\n\n async readState(sessionId, reducerName) {\n return readStateOf(sessionId, reducerName)\n },\n\n async readStates(sessionIds, reducerName) {\n return sessionIds.map((sessionId) => readStateOf(sessionId, reducerName))\n },\n\n async putSnapshot(sessionId, reducerName, index, state) {\n const key = `${sessionId}\\u0000${reducerName}`\n const existing = snapshots.get(key)\n // Guarded upsert: a slower concurrent writer must never clobber a\n // further-along snapshot. A lost race costs a few refolded events\n // next read — never correctness.\n if (existing && existing.index >= index) return\n snapshots.set(key, {\n index,\n state: structuredClone(state),\n updatedAt: clock.now(),\n })\n },\n\n presence: {\n async set(ns, participant, values, meta) {\n let participants = presenceRows.get(ns)\n if (!participants) {\n participants = new Map()\n presenceRows.set(ns, participants)\n }\n let fields = participants.get(participant)\n if (!fields) {\n fields = new Map()\n participants.set(participant, fields)\n }\n\n // Expiry anchors on the storage's own clock — the sender's\n // `at` orders writes but never extends or shortens a lifetime.\n const expiresAtMs = clock.now().getTime() + meta.ttlMs\n // Field-keyed and caller-named — null-prototype, like every\n // presence map (see `nullProtoRecord`).\n const applied: Record<string, unknown> = nullProtoRecord()\n let appliedCount = 0\n for (const [field, value] of Object.entries(values)) {\n // Field-wise LWW by `at`; ties go to the incoming write, so\n // same-stamp sets keep set-then-read intuition.\n const existing = fields.get(field)\n if (existing && existing.at > meta.at) continue\n if (value === null) {\n fields.delete(field)\n } else {\n fields.set(field, {\n value: structuredClone(value),\n seen: meta.seen,\n at: new Date(meta.at),\n expiresAt: new Date(expiresAtMs),\n })\n }\n applied[field] = structuredClone(value)\n appliedCount += 1\n }\n if (fields.size === 0) participants.delete(participant)\n if (participants.size === 0) presenceRows.delete(ns)\n\n // Only applied fields broadcast — a losing write repaints nothing,\n // so subscribers stay consistent with what read() returns.\n if (appliedCount === 0) return\n const subs = presenceSubscribers.get(ns)\n if (!subs) return\n const patch: PresencePatch = {\n participant,\n values: applied,\n seen: meta.seen,\n at: new Date(meta.at),\n }\n for (const listener of subs) listener(patch)\n },\n\n async read(ns) {\n const participants = presenceRows.get(ns)\n if (!participants) return []\n const now = clock.now()\n const rows: PresenceRow[] = []\n for (const [participant, fields] of participants) {\n for (const [field, entry] of fields) {\n // Expiry is enforced lazily on read — no timers; a silent\n // participant's rows vanish the next time anyone looks.\n if (entry.expiresAt <= now) {\n fields.delete(field)\n continue\n }\n rows.push({\n participant,\n field,\n value: structuredClone(entry.value),\n seen: entry.seen,\n at: new Date(entry.at),\n expiresAt: new Date(entry.expiresAt),\n })\n }\n if (fields.size === 0) participants.delete(participant)\n }\n if (participants.size === 0) presenceRows.delete(ns)\n return rows\n },\n\n subscribe(ns, onPatch) {\n let subs = presenceSubscribers.get(ns)\n if (!subs) {\n subs = new Set()\n presenceSubscribers.set(ns, subs)\n }\n subs.add(onPatch)\n return () => {\n subs.delete(onPatch)\n if (subs.size === 0) presenceSubscribers.delete(ns)\n }\n },\n },\n\n inspect: {\n async listSessions(inspectionOptions) {\n const ids = [...sessions.keys()]\n .filter(\n (sessionId) =>\n sessionId.startsWith(inspectionOptions.prefix) &&\n (inspectionOptions.cursor === undefined ||\n sessionId > inspectionOptions.cursor),\n )\n .toSorted()\n const visible = ids.slice(0, inspectionOptions.limit)\n const summaries = visible.map((sessionId): StoredSessionSummary => {\n const rows = sessions.get(sessionId)!\n const first = rows[0]!\n let updatedAt = first.createdAt\n let pendingCount = 0\n let failedCount = 0\n let attemptCount = 0\n let failureCount = 0\n for (const row of rows) {\n attemptCount += row.attemptCount\n failureCount += row.failureCount\n if (row.processedAt === null && row.failedAt === null) {\n pendingCount += 1\n }\n if (row.failedAt !== null) failedCount += 1\n for (const timestamp of [\n row.createdAt,\n row.firstClaimedAt,\n row.lastClaimedAt,\n row.lastFailedAt,\n row.processedAt,\n row.failedAt,\n ]) {\n if (timestamp && timestamp > updatedAt) updatedAt = timestamp\n }\n }\n return {\n sessionId,\n eventCount: rows.length,\n pendingCount,\n failedCount,\n attemptCount,\n failureCount,\n firstEventAt: new Date(first.createdAt),\n updatedAt: new Date(updatedAt),\n }\n })\n return {\n sessions: summaries,\n cursor: ids.length > visible.length ? (visible.at(-1) ?? null) : null,\n }\n },\n async readEvents(sessionId, pageOptions) {\n const rows = sessions.get(sessionId) ?? []\n const throughIndex = Math.min(\n pageOptions.throughIndex ?? rows.length,\n rows.length,\n )\n return {\n events: rows\n .slice(pageOptions.afterIndex, throughIndex)\n .slice(0, pageOptions.limit)\n .map(toStored),\n throughIndex,\n }\n },\n async listSnapshots(sessionId) {\n const prefix = `${sessionId}\\u0000`\n return [...snapshots.entries()]\n .filter(([key]) => key.startsWith(prefix))\n .map(([key, snapshot]) => ({\n reducerName: key.slice(prefix.length),\n index: snapshot.index,\n updatedAt: new Date(snapshot.updatedAt),\n }))\n .toSorted((a, b) => a.reducerName.localeCompare(b.reducerName))\n },\n },\n\n stream(sessionId, opts) {\n const startAfter = opts?.startAfter ?? 0\n return {\n [Symbol.asyncIterator](): AsyncIterator<Event> {\n let last = startAfter\n const buffer: Row[] = []\n let wake: (() => void) | null = null\n let closed = false\n\n const onRow = (row: Row): void => {\n buffer.push(row)\n wake?.()\n }\n let subs = streamSubscribers.get(sessionId)\n if (!subs) {\n subs = new Set()\n streamSubscribers.set(sessionId, subs)\n }\n subs.add(onRow)\n // Seed with history — synchronously, in the same tick as the\n // subscription, so nothing can slip between the two.\n const existing = sessions.get(sessionId) ?? []\n buffer.unshift(...existing.filter((row) => row.index > startAfter))\n\n const unsubscribe = (): void => {\n subs.delete(onRow)\n if (subs.size === 0) streamSubscribers.delete(sessionId)\n }\n\n return {\n async next(): Promise<IteratorResult<Event>> {\n for (;;) {\n while (buffer.length > 0) {\n const row = buffer.shift()!\n if (row.index <= last) continue\n last = row.index\n return { value: toStored(row), done: false }\n }\n if (closed) return { value: undefined, done: true }\n // oxlint-disable-next-line no-await-in-loop -- wait-for-wake\n await new Promise<void>((resolve) => {\n wake = resolve\n })\n wake = null\n }\n },\n async return(): Promise<IteratorResult<Event>> {\n closed = true\n unsubscribe()\n wake?.()\n return { value: undefined, done: true }\n },\n }\n },\n }\n },\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as POLL_TIMINGS } from "./internal-
|
|
1
|
+
import { i as POLL_TIMINGS } from "./internal-DstsI6Re.js";
|
|
2
2
|
//#region src/store-polling.ts
|
|
3
3
|
/**
|
|
4
4
|
* The shared polling stream — how poll-based backends (sqlite,
|
|
@@ -82,3 +82,5 @@ function pollingStream(readAfter, options) {
|
|
|
82
82
|
}
|
|
83
83
|
//#endregion
|
|
84
84
|
export { pollingStream as n, defaultSleep as t };
|
|
85
|
+
|
|
86
|
+
//# sourceMappingURL=store-polling-dSeLxzfb.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store-polling-dSeLxzfb.js","names":[],"sources":["../src/store-polling.ts"],"sourcesContent":["/**\n * The shared polling stream — how poll-based backends (sqlite,\n * postgres) implement `A2Store.stream`.\n *\n * The cadence is activity-adaptive: while events are flowing, polls\n * run at a tight floor (25ms) so streams feel pushed — a token\n * generation reads smoothly instead of clumping at the idle interval;\n * every empty poll doubles the gap until the idle ceiling. The cadence\n * is owned (POLL_TIMINGS in internal.ts), not a public option: the\n * floor already keeps active streams smooth, so tuning it is a cost\n * decision a2 makes, not one it delegates. Correctness never depends\n * on the cadence — each poll is a catch-up read (`index > last`, in\n * order), so nothing can be missed, only arrive a beat later.\n */\n\nimport { POLL_TIMINGS } from './internal.ts'\nimport type { StoredEvent } from './store.ts'\n\n/** One cancellable sleep — the injection seam for cadence tests. */\nexport type Sleeper = { promise: Promise<void>; cancel: () => void }\n\n/** Unref'd cancellable timer — shared with the redis safety read. */\nexport const defaultSleep = (ms: number): Sleeper => {\n let timer: ReturnType<typeof setTimeout> | undefined\n let wake: (() => void) | undefined\n const promise = new Promise<void>((resolve) => {\n wake = resolve // assigned synchronously by the executor\n timer = setTimeout(resolve, ms)\n // Don't hold the process open for an idle poll.\n ;(timer as { unref?: () => void }).unref?.()\n })\n return {\n promise,\n cancel: () => {\n clearTimeout(timer)\n wake?.()\n },\n }\n}\n\n// Read the seam lazily — tests shrink POLL_TIMINGS in their setup.\nconst ceiling = (): number => POLL_TIMINGS.idleCeilingMs\nconst floor = (): number =>\n Math.min(POLL_TIMINGS.activeFloorMs, POLL_TIMINGS.idleCeilingMs)\n\nexport type PollingStreamOptions = {\n /** Deliver events strictly after this index (exclusive). */\n startAfter?: number\n /** Injectable sleep — tests assert the cadence without wall-clock waits. */\n sleep?: (ms: number) => Sleeper\n}\n\n/**\n * Build the `stream()` iterable from a backend's catch-up read. The\n * iterable ends when the consumer calls `return()` (a disconnecting\n * SSE client), waking any pending sleep immediately.\n */\nexport function pollingStream(\n readAfter: (afterIndex: number) => StoredEvent[] | Promise<StoredEvent[]>,\n options: PollingStreamOptions,\n): AsyncIterable<StoredEvent> {\n const startAfter = options.startAfter ?? 0\n const sleep = options.sleep ?? defaultSleep\n\n return {\n [Symbol.asyncIterator](): AsyncIterator<StoredEvent> {\n let last = startAfter\n let buffer: StoredEvent[] = []\n let closed = false\n let pending: Sleeper | null = null\n let interval = floor()\n\n return {\n async next(): Promise<IteratorResult<StoredEvent>> {\n for (;;) {\n if (closed) return { value: undefined, done: true }\n // oxlint-disable-next-line no-await-in-loop -- poll loop\n if (buffer.length === 0) buffer = await readAfter(last)\n const row = buffer.shift()\n if (row) {\n // Activity: tighten back to the floor.\n interval = floor()\n last = row.index\n return { value: row, done: false }\n }\n // Empty poll: wait, then back off toward the idle ceiling.\n pending = sleep(interval)\n // oxlint-disable-next-line no-await-in-loop -- poll loop\n await pending.promise\n pending = null\n interval = Math.min(interval * 2, ceiling())\n }\n },\n async return(): Promise<IteratorResult<StoredEvent>> {\n closed = true\n pending?.cancel()\n return { value: undefined, done: true }\n },\n }\n },\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAsBA,MAAa,gBAAgB,OAAwB;CACnD,IAAI;CACJ,IAAI;CAOJ,OAAO;EACL,SAAA,IAPkB,SAAe,YAAY;GAC7C,OAAO;GACP,QAAQ,WAAW,SAAS,EAAE;GAE7B,MAAkC,QAAQ;EAC7C,CAEQ;EACN,cAAc;GACZ,aAAa,KAAK;GAClB,OAAO;EACT;CACF;AACF;AAGA,MAAM,gBAAwB,aAAa;AAC3C,MAAM,cACJ,KAAK,IAAI,aAAa,eAAe,aAAa,aAAa;;;;;;AAcjE,SAAgB,cACd,WACA,SAC4B;CAC5B,MAAM,aAAa,QAAQ,cAAc;CACzC,MAAM,QAAQ,QAAQ,SAAS;CAE/B,OAAO,EACL,CAAC,OAAO,iBAA6C;EACnD,IAAI,OAAO;EACX,IAAI,SAAwB,CAAC;EAC7B,IAAI,SAAS;EACb,IAAI,UAA0B;EAC9B,IAAI,WAAW,MAAM;EAErB,OAAO;GACL,MAAM,OAA6C;IACjD,SAAS;KACP,IAAI,QAAQ,OAAO;MAAE,OAAO,KAAA;MAAW,MAAM;KAAK;KAElD,IAAI,OAAO,WAAW,GAAG,SAAS,MAAM,UAAU,IAAI;KACtD,MAAM,MAAM,OAAO,MAAM;KACzB,IAAI,KAAK;MAEP,WAAW,MAAM;MACjB,OAAO,IAAI;MACX,OAAO;OAAE,OAAO;OAAK,MAAM;MAAM;KACnC;KAEA,UAAU,MAAM,QAAQ;KAExB,MAAM,QAAQ;KACd,UAAU;KACV,WAAW,KAAK,IAAI,WAAW,GAAG,QAAQ,CAAC;IAC7C;GACF;GACA,MAAM,SAA+C;IACnD,SAAS;IACT,SAAS,OAAO;IAChB,OAAO;KAAE,OAAO,KAAA;KAAW,MAAM;IAAK;GACxC;EACF;CACF,EACF;AACF"}
|
package/dist/store-postgres.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as IdSource, i as Clock, t as A2Store } from "./store-
|
|
1
|
+
import { c as IdSource, i as Clock, t as A2Store } from "./store-DysUkTH3.js";
|
|
2
2
|
//#region src/store-postgres.d.ts
|
|
3
3
|
/** The result shape this backend reads: just rows. */
|
|
4
4
|
type PostgresQueryResult = {
|
|
@@ -37,4 +37,5 @@ type PostgresStore = A2Store & {
|
|
|
37
37
|
};
|
|
38
38
|
declare function postgres(options?: PostgresStoreOptions): PostgresStore;
|
|
39
39
|
//#endregion
|
|
40
|
-
export { PostgresClient, PostgresConnection, PostgresQueryResult, PostgresStore, PostgresStoreOptions, postgres };
|
|
40
|
+
export { PostgresClient, PostgresConnection, PostgresQueryResult, PostgresStore, PostgresStoreOptions, postgres };
|
|
41
|
+
//# sourceMappingURL=store-postgres.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store-postgres.d.ts","names":[],"sources":["../src/store-postgres.ts"],"mappings":";;;KAqCY;EAAwB,MAAM;;;KAG9B;EACV,MAAM,cAAc,qBAAqB,QAAQ;EACjD;;;;;;;;KASU;EACV,MAAM,cAAc,qBAAqB,QAAQ;EACjD,YAAY,QAAQ;EACpB,QAAQ;;KAGE;;EAEV;;EAEA,SAAS;;EAET,QAAQ;;EAER,MAAM;;KAGI,gBAAgB;;;EAG1B,SAAS;;iBAoLK,SAAS,UAAS,uBAA4B"}
|
package/dist/store-postgres.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as A2Error } from "./errors-BQuJpe82.js";
|
|
2
|
-
import { n as pollingStream } from "./store-polling-
|
|
2
|
+
import { n as pollingStream } from "./store-polling-dSeLxzfb.js";
|
|
3
3
|
import { t as retryableLazy } from "./retryable-lazy-DZWmHpii.js";
|
|
4
4
|
import { t as idempotentReplay } from "./idempotent-replay-DuqEkYA7.js";
|
|
5
5
|
import { n as SYSTEM_CLOCK, t as RANDOM_IDS } from "./store-N8PXxDAS.js";
|
|
@@ -633,6 +633,60 @@ function postgres(options = {}) {
|
|
|
633
633
|
};
|
|
634
634
|
});
|
|
635
635
|
},
|
|
636
|
+
async readStates(sessionIds, reducerName) {
|
|
637
|
+
if (sessionIds.length === 0) return [];
|
|
638
|
+
return wrap(async () => {
|
|
639
|
+
const { rows } = await (await client()).query(`with snapshot as materialized (
|
|
640
|
+
select session_id, up_to_index, state
|
|
641
|
+
from a2_snapshots
|
|
642
|
+
where session_id = any($1::text[]) and reducer_name = $2
|
|
643
|
+
)
|
|
644
|
+
select 0 as row_order, 'snapshot' as row_kind,
|
|
645
|
+
snapshot.session_id,
|
|
646
|
+
snapshot.up_to_index as snapshot_index,
|
|
647
|
+
snapshot.state as snapshot_state,
|
|
648
|
+
null::bigint as idx, null::text as event_type,
|
|
649
|
+
null::jsonb as payload, null::text as event_id,
|
|
650
|
+
null::bigint as created_at
|
|
651
|
+
from snapshot
|
|
652
|
+
union all
|
|
653
|
+
select 1 as row_order, 'event' as row_kind,
|
|
654
|
+
event.session_id,
|
|
655
|
+
null::bigint as snapshot_index,
|
|
656
|
+
null::jsonb as snapshot_state,
|
|
657
|
+
event.idx, event.event_type, event.payload,
|
|
658
|
+
event.event_id, event.created_at
|
|
659
|
+
from a2_events event
|
|
660
|
+
left join snapshot on snapshot.session_id = event.session_id
|
|
661
|
+
where event.session_id = any($1::text[])
|
|
662
|
+
and event.idx > coalesce(snapshot.up_to_index, 0)
|
|
663
|
+
order by row_order, session_id, idx`, [sessionIds, reducerName]);
|
|
664
|
+
const bySession = /* @__PURE__ */ new Map();
|
|
665
|
+
const readOf = (sessionId) => {
|
|
666
|
+
let read = bySession.get(sessionId);
|
|
667
|
+
if (!read) {
|
|
668
|
+
read = {
|
|
669
|
+
snapshot: null,
|
|
670
|
+
events: []
|
|
671
|
+
};
|
|
672
|
+
bySession.set(sessionId, read);
|
|
673
|
+
}
|
|
674
|
+
return read;
|
|
675
|
+
};
|
|
676
|
+
for (const row of rows) {
|
|
677
|
+
const read = readOf(row["session_id"]);
|
|
678
|
+
if (row["row_kind"] === "snapshot") read.snapshot = {
|
|
679
|
+
index: asNumber(row["snapshot_index"]),
|
|
680
|
+
state: row["snapshot_state"]
|
|
681
|
+
};
|
|
682
|
+
else read.events.push(toEvent(row));
|
|
683
|
+
}
|
|
684
|
+
return sessionIds.map((sessionId) => bySession.get(sessionId) ?? {
|
|
685
|
+
snapshot: null,
|
|
686
|
+
events: []
|
|
687
|
+
});
|
|
688
|
+
});
|
|
689
|
+
},
|
|
636
690
|
async putSnapshot(sessionId, reducerName, index, state) {
|
|
637
691
|
await wrap(async () => {
|
|
638
692
|
await (await client()).query(`insert into a2_snapshots (session_id, reducer_name, up_to_index, state, updated_at)
|
|
@@ -803,3 +857,5 @@ function postgres(options = {}) {
|
|
|
803
857
|
}
|
|
804
858
|
//#endregion
|
|
805
859
|
export { postgres };
|
|
860
|
+
|
|
861
|
+
//# sourceMappingURL=store-postgres.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store-postgres.js","names":[],"sources":["../src/store-postgres.ts"],"sourcesContent":["/**\n * experimental-a2/store-postgres — postgres store backend (the production path).\n *\n * Implements the A2Store interface over any client that speaks the tiny\n * `PostgresClient` shape below — a `pg` Pool matches it structurally\n * (and is what `connectionString` creates, lazily; `pg` is an optional\n * peer dependency). The conformance suite in test/conformance is the\n * executable contract, run against PGlite (real Postgres, in-process)\n * and against a real server when `TEST_DATABASE_URL` is set.\n *\n * Semantics mirror the sqlite backend: `index` is stored as `idx`,\n * timestamps are epoch milliseconds written from the injected clock —\n * never SQL `now()` — and `stream()` is a poll loop. (A LISTEN/NOTIFY\n * upgrade can land later without any API change; callers never branch\n * on the transport.) Appends serialize per session via\n * `pg_advisory_xact_lock`, so concurrent writers can't race the index\n * assignment.\n */\n\nimport { A2Error } from './errors.ts'\nimport { idempotentReplay } from './idempotent-replay.ts'\nimport { decodeReturnedEventIds } from './store-codec.ts'\nimport { pollingStream } from './store-polling.ts'\nimport { retryableLazy } from './retryable-lazy.ts'\nimport {\n RANDOM_IDS,\n SYSTEM_CLOCK,\n type A2Store,\n type Clock,\n type Event,\n type EventCause,\n type IdSource,\n type StoredEvent,\n type StoreStateRead,\n} from './store.ts'\n\n/** The result shape this backend reads: just rows. */\nexport type PostgresQueryResult = { rows: Record<string, unknown>[] }\n\n/** One checked-out connection — what transactions run on. */\nexport type PostgresConnection = {\n query(text: string, values?: unknown[]): Promise<PostgresQueryResult>\n release(): void\n}\n\n/**\n * The minimal client this backend needs. `pg.Pool` matches it\n * structurally; so does PGlite (which has no `connect` — transactions\n * then serialize on an internal mutex, fine for a single-connection\n * client).\n */\nexport type PostgresClient = {\n query(text: string, values?: unknown[]): Promise<PostgresQueryResult>\n connect?(): Promise<PostgresConnection>\n end?(): Promise<void>\n}\n\nexport type PostgresStoreOptions = {\n /** Creates a `pg` Pool lazily (requires the optional peer dep `pg`). */\n connectionString?: string | undefined\n /** Bring your own client — a `pg` Pool, PGlite, a pooler wrapper. */\n client?: PostgresClient\n /** Injectable clock — every stored timestamp comes from here. */\n clock?: Clock\n /** Injectable id source for generated event ids. */\n ids?: IdSource\n}\n\nexport type PostgresStore = A2Store & {\n /** End the underlying pool/client (when this backend created it, or\n * the injected client has `end`). */\n close(): Promise<void>\n}\n\nconst SCHEMA = `\ncreate table if not exists a2_events (\n session_id text not null,\n idx bigint not null,\n event_type text not null,\n payload jsonb not null,\n event_id text not null,\n created_at bigint not null,\n cause jsonb,\n lane text,\n lane_ready boolean not null default false,\n processed_at bigint,\n processed_by_attempt integer,\n returned_event_ids jsonb,\n first_claimed_at bigint,\n last_claimed_at bigint,\n attempt_count integer not null default 0,\n failure_count integer not null default 0,\n last_failed_at bigint,\n last_failed_attempt integer,\n last_error text,\n failed_at bigint,\n claim_holder text,\n claim_expires_at bigint,\n primary key (session_id, idx)\n);\n\ncreate unique index if not exists a2_events_event_id on a2_events (event_id);\ncreate index if not exists a2_events_unprocessed\n on a2_events (session_id, idx) where processed_at is null;\ncreate index if not exists a2_events_lane_pending\n on a2_events (session_id, lane, idx) where processed_at is null;\ncreate index if not exists a2_events_dispatch_ready\n on a2_events (session_id, idx)\n where lane_ready and processed_at is null and failed_at is null;\ncreate index if not exists a2_events_claim_expiry\n on a2_events (session_id, claim_expires_at)\n where lane_ready and processed_at is null and failed_at is null;\n\ncreate table if not exists a2_snapshots (\n session_id text not null,\n reducer_name text not null,\n up_to_index bigint not null,\n state jsonb not null,\n updated_at bigint not null,\n primary key (session_id, reducer_name)\n);\n\ncreate table if not exists a2_presence (\n ns text not null,\n participant text not null,\n field text not null,\n value jsonb not null,\n seen bigint not null,\n at bigint not null,\n expires_at bigint not null,\n primary key (ns, participant, field)\n);\n`\n\nconst asNumber = (value: unknown): number => Number(value) // bigint columns arrive as strings\n\nconst toDate = (ms: unknown): Date | null =>\n ms === null || ms === undefined ? null : new Date(asNumber(ms))\n\nconst toCause = (raw: unknown): EventCause | null => {\n if (raw === null || raw === undefined) return null\n const value = (typeof raw === 'string' ? JSON.parse(raw) : 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 toStored = (row: Record<string, unknown>): StoredEvent => ({\n id: row['event_id'] as string,\n type: row['event_type'] as string,\n payload: row['payload'],\n index: asNumber(row['idx']),\n sessionId: row['session_id'] as string,\n createdAt: new Date(asNumber(row['created_at'])),\n cause: toCause(row['cause']),\n lane: (row['lane'] as string | null) ?? null,\n processedAt: toDate(row['processed_at']),\n processedByAttempt:\n row['processed_by_attempt'] === null ||\n row['processed_by_attempt'] === undefined\n ? null\n : asNumber(row['processed_by_attempt']),\n returnedEventIds:\n row['returned_event_ids'] === null ||\n row['returned_event_ids'] === undefined\n ? null\n : decodeReturnedEventIds(row['returned_event_ids']),\n firstClaimedAt: toDate(row['first_claimed_at']),\n lastClaimedAt: toDate(row['last_claimed_at']),\n attemptCount: asNumber(row['attempt_count']),\n failureCount: asNumber(row['failure_count']),\n lastFailedAt: toDate(row['last_failed_at']),\n lastFailedAttempt:\n row['last_failed_attempt'] === null ||\n row['last_failed_attempt'] === undefined\n ? null\n : asNumber(row['last_failed_attempt']),\n lastError: (row['last_error'] as string | null) ?? null,\n failedAt: toDate(row['failed_at']),\n claimHolder: (row['claim_holder'] as string | null) ?? null,\n claimExpiresAt: toDate(row['claim_expires_at']),\n})\n\nconst toEvent = (row: Record<string, unknown>): Event => ({\n id: row['event_id'] as string,\n type: row['event_type'] as string,\n payload: row['payload'],\n index: asNumber(row['idx']),\n sessionId: row['session_id'] as string,\n createdAt: new Date(asNumber(row['created_at'])),\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', 'postgres store operation failed', {\n cause: err,\n })\n }\n}\n\nconst jsonb = (value: unknown): string => JSON.stringify(value) ?? 'null'\n\nconst initializeSchema = async (client: PostgresClient): Promise<void> => {\n if (!client.connect) {\n await client.query(SCHEMA)\n return\n }\n\n const conn = await client.connect()\n try {\n await conn.query('begin')\n // `if not exists` does not serialize concurrent catalog writes. Every\n // serverless isolate takes the same transaction-scoped lock before DDL.\n await conn.query(\n \"select pg_advisory_xact_lock(hashtext('a2:postgres:schema'))\",\n )\n await conn.query(SCHEMA)\n await conn.query('commit')\n } catch (err) {\n await conn.query('rollback').catch(() => {})\n throw err\n } finally {\n conn.release()\n }\n}\n\nexport function postgres(options: PostgresStoreOptions = {}): PostgresStore {\n const clock = options.clock ?? SYSTEM_CLOCK\n const generateId = options.ids ?? RANDOM_IDS\n\n if (!options.client && !options.connectionString) {\n throw new TypeError(\n 'postgres() needs a connectionString or an injected client',\n )\n }\n\n // Lazy init: the first operation creates the client (dynamic import\n // of the optional peer dep `pg`) and the schema. Constructing the\n // backend does no I/O — same contract as every other adapter.\n const connection = retryableLazy(async () => {\n let owned: PostgresClient | null = null\n try {\n let c = options.client\n if (!c) {\n const pg = await import('pg').catch(() => {\n throw new A2Error(\n 'STORE_NOT_CONFIGURED',\n \"experimental-a2/store-postgres with a connectionString needs the 'pg' package (optional peer dependency) — install it, or inject a client\",\n )\n })\n c = new pg.default.Pool({\n connectionString: options.connectionString,\n }) as unknown as PostgresClient\n owned = c\n }\n await initializeSchema(c)\n return c\n } catch (err) {\n if (owned?.end) await owned.end().catch(() => {})\n throw err\n }\n })\n const client = connection.get\n\n // Transactions run on one connection. Pools check one out; a\n // single-connection client (PGlite) serializes on this mutex instead.\n let mutex: Promise<unknown> = Promise.resolve()\n const withTx = async <T>(\n fn: (query: PostgresConnection['query']) => Promise<T>,\n ): Promise<T> => {\n const c = await client()\n if (c.connect) {\n const conn = await c.connect()\n try {\n await conn.query('begin')\n const result = await fn((text, values) => conn.query(text, values))\n await conn.query('commit')\n return result\n } catch (err) {\n await conn.query('rollback').catch(() => {})\n throw err\n } finally {\n conn.release()\n }\n }\n const run = mutex.then(async () => {\n await c.query('begin')\n try {\n const result = await fn((text, values) => c.query(text, values))\n await c.query('commit')\n return result\n } catch (err) {\n await c.query('rollback').catch(() => {})\n throw err\n }\n })\n mutex = run.catch(() => {})\n return run\n }\n\n return {\n async append(sessionId, events) {\n if (events.length === 0) {\n return wrap(async () => {\n const c = await client()\n const { rows } = await c.query(\n `select exists(\n select 1 from a2_events\n where session_id = $1 and processed_at is null\n ) as has_pending`,\n [sessionId],\n )\n return {\n events: [],\n hasPending: rows[0]?.['has_pending'] === true,\n }\n })\n }\n return wrap(() =>\n withTx(async (query) => {\n // Serialize appends per session — index assignment must not\n // race across writers. Transaction-scoped, self-releasing.\n await query('select pg_advisory_xact_lock(hashtext($1))', [sessionId])\n\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 if (suppliedIds.length > 0) {\n const { rows: existing } = await query(\n `select stored.*,\n exists(\n select 1 from a2_events pending\n where pending.session_id = $2\n and pending.processed_at is null\n ) as session_has_pending\n from a2_events stored\n where stored.event_id = any($1)\n order by stored.idx`,\n [suppliedIds, sessionId],\n )\n if (existing.length > 0) {\n const foreign = existing.find(\n (row) => row['session_id'] !== sessionId,\n )\n if (foreign) {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n `event id '${String(foreign['event_id'])}' already exists in another session`,\n )\n }\n if (existing.length === events.length) {\n return {\n events: idempotentReplay(events, existing.map(toStored)),\n hasPending: existing[0]?.['session_has_pending'] === true,\n }\n }\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n `batch mixes ${existing.length} already-appended and ${events.length - existing.length} fresh events`,\n )\n }\n }\n\n // Attempt-currency fence: a fresh handler append commits only\n // while its causal attempt is still the parent's latest\n // (store.ts `append`). `for update` serializes this read against\n // a concurrent `claimAvailable` — an unlocked read admits write\n // skew.\n const causeIndexes = [\n ...new Set(\n events\n .filter((e) => e.cause !== undefined)\n .map((e) => e.cause!.index),\n ),\n ]\n if (causeIndexes.length > 0) {\n const { rows: parents } = await query(\n `select idx, attempt_count, failed_at from a2_events\n where session_id = $1 and idx = any($2::bigint[])\n for update`,\n [sessionId, causeIndexes],\n )\n const byIndex = new Map(\n parents.map((row) => [asNumber(row['idx']), row]),\n )\n for (const e of events) {\n if (!e.cause) continue\n const parent = byIndex.get(e.cause.index)\n if (!parent) {\n throw new TypeError(\n `no event at index ${e.cause.index} in session '${sessionId}'`,\n )\n }\n if (\n asNumber(parent['attempt_count']) !== e.cause.attempt ||\n parent['failed_at'] !== null\n ) {\n throw new A2Error(\n 'SUPERSEDED_ATTEMPT',\n `attempt ${e.cause.attempt} no longer owns event ${e.cause.index} in session '${sessionId}'`,\n )\n }\n }\n }\n\n const { rows: maxRows } = await query(\n `select coalesce(max(idx), 0) as max,\n exists(\n select 1 from a2_events pending\n where pending.session_id = $1\n and pending.processed_at is null\n ) as session_has_pending\n from a2_events\n where session_id = $1`,\n [sessionId],\n )\n const base = asNumber(maxRows[0]?.['max'])\n const now = clock.now().getTime()\n const inserted: StoredEvent[] = []\n for (let i = 0; i < events.length; i += 1) {\n const e = events[i]!\n const id = e.id ?? generateId()\n const index = base + 1 + i\n // oxlint-disable-next-line no-await-in-loop -- one tx, ordered inserts\n await query(\n `insert into a2_events\n (session_id, idx, event_type, payload, event_id, created_at,\n cause, lane, lane_ready, processed_at)\n values (\n $1, $2, $3, $4::jsonb, $5, $6, $7::jsonb, $8,\n $8::text is null or not exists (\n select 1 from a2_events\n where session_id = $1 and lane = $8 and processed_at is null\n ),\n $9\n )`,\n [\n sessionId,\n index,\n e.type,\n jsonb(e.payload),\n id,\n now,\n e.cause ? jsonb(e.cause) : null,\n e.lane ?? null,\n e.settled ? now : null,\n ],\n )\n inserted.push({\n id,\n type: e.type,\n payload: structuredClone(e.payload),\n index,\n sessionId,\n createdAt: new Date(now),\n cause: e.cause ? { ...e.cause } : null,\n lane: e.lane ?? null,\n processedAt: e.settled ? new Date(now) : null,\n processedByAttempt: null,\n returnedEventIds: null,\n firstClaimedAt: null,\n lastClaimedAt: null,\n attemptCount: 0,\n failureCount: 0,\n lastFailedAt: null,\n lastFailedAttempt: null,\n lastError: null,\n failedAt: null,\n claimHolder: null,\n claimExpiresAt: null,\n })\n }\n return {\n events: inserted,\n hasPending:\n maxRows[0]?.['session_has_pending'] === true ||\n events.some((event) => event.settled !== true),\n }\n }),\n )\n },\n\n async read(sessionId, opts) {\n return wrap(async () => {\n const c = await client()\n const conditions = ['session_id = $1']\n const params: unknown[] = [sessionId]\n if (opts?.afterIndex !== undefined) {\n params.push(opts.afterIndex)\n conditions.push(`idx > $${params.length}`)\n }\n if (opts?.throughIndex !== undefined) {\n params.push(opts.throughIndex)\n conditions.push(`idx <= $${params.length}`)\n }\n const { rows } = await c.query(\n `select * from a2_events where ${conditions.join(' and ')} order by idx`,\n params,\n )\n return rows.map(toStored)\n })\n },\n\n async claimAvailable({\n sessionId,\n holder,\n ttlMs,\n expiresAtMs,\n excludeIndexes = [],\n }) {\n return wrap(() =>\n withTx(async (query) => {\n const now = clock.now().getTime()\n const expiresAt = expiresAtMs ?? now + ttlMs\n const { rows } = await query(\n `with eligible as materialized (\n select event.idx\n from a2_events event\n where event.session_id = $1\n and event.processed_at is null\n and event.failed_at is null\n and event.lane_ready\n and (event.claim_expires_at is null or event.claim_expires_at <= $2)\n and not (event.idx = any($5::bigint[]))\n for update of event\n )\n update a2_events claimed set\n attempt_count = claimed.attempt_count + 1,\n first_claimed_at = coalesce(claimed.first_claimed_at, $2),\n last_claimed_at = $2,\n claim_holder = $3,\n claim_expires_at = $4\n from eligible\n where claimed.session_id = $1 and claimed.idx = eligible.idx\n returning claimed.*`,\n [sessionId, now, holder, expiresAt, [...excludeIndexes]],\n )\n if (rows.length > 0) {\n const events = rows\n .map(toStored)\n .toSorted((a, b) => a.index - b.index)\n return { outcome: 'claimed', events }\n }\n const { rows: activeRows } = await query(\n `select min(claim_expires_at) as retry_at\n from a2_events\n where session_id = $1\n and processed_at is null\n and failed_at is null\n and lane_ready\n and claim_expires_at > $2`,\n [sessionId, now],\n )\n const retryAt = activeRows[0]?.['retry_at']\n return retryAt === null || retryAt === undefined\n ? { outcome: 'settled' }\n : { outcome: 'busy', retryAt: new Date(asNumber(retryAt)) }\n }),\n )\n },\n\n async renewClaims({ sessionId, holder, claims, ttlMs, expiresAtMs }) {\n if (claims.length === 0) return { renewed: [], superseded: [] }\n return wrap(async () => {\n const c = await client()\n const now = clock.now().getTime()\n const expiresAt = expiresAtMs ?? now + ttlMs\n const indexes = claims.map((claim) => claim.index)\n const attempts = new Map(\n claims.map((claim) => [claim.index, claim.attempt]),\n )\n const { rows } = await c.query(\n `with listed as (\n select idx, attempt\n from unnest($3::bigint[], $6::int[]) as listed(idx, attempt)\n ),\n renewed as (\n update a2_events event set claim_expires_at = $1\n from listed\n where event.session_id = $2\n and event.idx = listed.idx\n and event.attempt_count = listed.attempt\n and event.processed_at is null\n and event.failed_at is null\n and event.claim_holder = $4\n and event.claim_expires_at > $5\n returning event.idx\n )\n select event.idx, event.attempt_count,\n exists(select 1 from renewed where renewed.idx = event.idx)\n as renewed\n from a2_events event\n where event.session_id = $2 and event.idx = any($3::bigint[])`,\n [\n expiresAt,\n sessionId,\n indexes,\n holder,\n now,\n claims.map((claim) => claim.attempt),\n ],\n )\n const renewed: number[] = []\n const superseded: number[] = []\n for (const row of rows) {\n const index = asNumber(row['idx'])\n if (row['renewed'] === true) renewed.push(index)\n else if (\n asNumber(row['attempt_count']) > (attempts.get(index) ?? 0)\n ) {\n superseded.push(index)\n }\n }\n return {\n renewed: renewed.toSorted((a, b) => a - b),\n superseded: superseded.toSorted((a, b) => a - b),\n }\n })\n },\n\n async completeAttempt({ sessionId, index, attempt, events }) {\n return wrap(() =>\n withTx(async (query) => {\n const { rows: parentRows } = await query(\n `select * from a2_events\n where session_id = $1 and idx = $2\n for update`,\n [sessionId, index],\n )\n const parent = parentRows[0]\n if (!parent) {\n throw new TypeError(\n `no event at index ${index} in session '${sessionId}'`,\n )\n }\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 'returned event batch contains the same event id more than once',\n )\n }\n if (parent['processed_at'] !== null) {\n if (asNumber(parent['processed_by_attempt']) !== attempt) {\n return { outcome: 'superseded' }\n }\n const returnedIds =\n parent['returned_event_ids'] === null ||\n parent['returned_event_ids'] === undefined\n ? null\n : decodeReturnedEventIds(parent['returned_event_ids'])\n if (\n returnedIds === null ||\n returnedIds.length !== ids.length ||\n returnedIds.some((id, offset) => id !== ids[offset])\n ) {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n 'completed attempt does not match the returned event batch',\n )\n }\n if (events.length === 0) {\n return { outcome: 'completed', events: [] }\n }\n const { rows: existing } = await query(\n `select * from a2_events\n where event_id = any($1::text[])`,\n [ids],\n )\n const byId = new Map(\n existing.map((row) => [row['event_id'] as string, row]),\n )\n const ordered = ids.map((id) => byId.get(id))\n if (\n ordered.some((row) => {\n const cause = row ? toCause(row['cause']) : null\n return (\n !row ||\n row['session_id'] !== sessionId ||\n cause?.index !== index ||\n cause.attempt !== attempt\n )\n })\n ) {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n 'returned event retry does not match the committed batch',\n )\n }\n return {\n outcome: 'completed',\n events: idempotentReplay(\n events,\n (ordered as Record<string, unknown>[]).map(toStored),\n ),\n }\n }\n if (\n asNumber(parent['attempt_count']) !== attempt ||\n parent['claim_holder'] === null ||\n parent['claim_holder'] === undefined ||\n parent['failed_at'] !== null\n ) {\n return { outcome: 'superseded' }\n }\n\n await query('select pg_advisory_xact_lock(hashtext($1))', [sessionId])\n if (ids.length > 0) {\n const { rows: existing } = await query(\n 'select event_id from a2_events where event_id = any($1::text[])',\n [ids],\n )\n if (existing.length > 0) {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n 'returned event batch contains an already-appended event id',\n )\n }\n }\n\n const { rows: maxRows } = await query(\n 'select coalesce(max(idx), 0) as max from a2_events where session_id = $1',\n [sessionId],\n )\n const base = asNumber(maxRows[0]?.['max'])\n const now = clock.now().getTime()\n const inserted: StoredEvent[] = []\n for (let offset = 0; offset < events.length; offset += 1) {\n const event = events[offset]!\n const childIndex = base + offset + 1\n // oxlint-disable-next-line no-await-in-loop -- one tx, ordered inserts\n const { rows } = await query(\n `insert into a2_events\n (session_id, idx, event_type, payload, event_id, created_at,\n cause, lane, lane_ready, processed_at)\n values (\n $1, $2, $3, $4::jsonb, $5, $6, $7::jsonb, $8,\n $8::text is null or not exists (\n select 1 from a2_events\n where session_id = $1 and lane = $8 and processed_at is null\n ),\n $9\n )\n returning *`,\n [\n sessionId,\n childIndex,\n event.type,\n jsonb(event.payload),\n event.id,\n now,\n jsonb({ index, attempt }),\n event.lane ?? null,\n event.settled ? now : null,\n ],\n )\n inserted.push(toStored(rows[0]!))\n }\n await query(\n `update a2_events set\n processed_at = $1,\n processed_by_attempt = $2,\n returned_event_ids = $5::jsonb,\n lane_ready = false,\n claim_holder = null,\n claim_expires_at = null\n where session_id = $3 and idx = $4`,\n [now, attempt, sessionId, index, jsonb(ids)],\n )\n const parentLane = (parent['lane'] as string | null) ?? null\n if (parentLane !== null) {\n await query(\n `update a2_events set lane_ready = true\n where session_id = $1\n and idx = (\n select idx from a2_events\n where session_id = $1 and lane = $2 and processed_at is null\n order by idx limit 1\n )`,\n [sessionId, parentLane],\n )\n }\n return { outcome: 'completed', events: inserted }\n }),\n )\n },\n\n async failAttempt({ sessionId, index, attempt, error, maxFailures }) {\n return wrap(async () => {\n const c = await client()\n const { rows } = await c.query(\n `with current as materialized (\n select e.* from a2_events e\n where e.session_id = $1 and e.idx = $2\n for update of e\n ), updated as (\n update a2_events e set\n failure_count = e.failure_count + 1,\n last_error = $4,\n last_failed_at = $6,\n last_failed_attempt = $3,\n failed_at = case\n when e.failure_count + 1 >= $5 then $6\n else e.failed_at\n end,\n claim_holder = null,\n claim_expires_at = null\n from current\n where e.session_id = $1\n and e.idx = $2\n and current.processed_at is null\n and current.failed_at is null\n and current.attempt_count = $3\n and current.claim_holder is not null\n returning e.failure_count, e.failed_at\n )\n select case\n when not exists (select 1 from current) then 'missing'\n when exists (\n select 1 from updated where failed_at is not null\n ) then 'dead_lettered'\n when exists (select 1 from updated) then 'failed'\n when exists (\n select 1 from current\n where processed_at is not null or attempt_count <> $3\n ) then 'superseded'\n when exists (\n select 1 from current where failed_at is not null\n ) then 'dead_lettered'\n when exists (\n select 1 from current\n where claim_holder is null and last_failed_attempt = $3\n ) then 'failed'\n else 'superseded'\n end as outcome,\n coalesce(\n (select failure_count from updated),\n (select failure_count from current)\n ) as failure_count`,\n [\n sessionId,\n index,\n attempt,\n error,\n maxFailures,\n clock.now().getTime(),\n ],\n )\n const row = rows[0]\n if (!row || row['outcome'] === 'missing') {\n throw new TypeError(\n `no event at index ${index} in session '${sessionId}'`,\n )\n }\n return {\n outcome: row['outcome'] as 'failed' | 'dead_lettered' | 'superseded',\n failureCount: asNumber(row['failure_count']),\n }\n })\n },\n\n async readState(sessionId, reducerName) {\n return wrap(async () => {\n const c = await client()\n const { rows } = await c.query(\n `with snapshot as materialized (\n select up_to_index, state\n from a2_snapshots\n where session_id = $1 and reducer_name = $2\n )\n select 0 as row_order, 'snapshot' as row_kind,\n snapshot.up_to_index as snapshot_index,\n snapshot.state as snapshot_state,\n null::text as session_id, null::bigint as idx,\n null::text as event_type, null::jsonb as payload,\n null::text as event_id, null::bigint as created_at\n from snapshot\n union all\n select 1 as row_order, 'event' as row_kind,\n null::bigint as snapshot_index,\n null::jsonb as snapshot_state,\n event.session_id, event.idx, event.event_type,\n event.payload, event.event_id, event.created_at\n from a2_events event\n where event.session_id = $1\n and event.idx > coalesce((select up_to_index from snapshot), 0)\n order by row_order, idx`,\n [sessionId, reducerName],\n )\n const snapshot = rows.find((row) => row['row_kind'] === 'snapshot')\n return {\n snapshot: snapshot\n ? {\n index: asNumber(snapshot['snapshot_index']),\n state: snapshot['snapshot_state'],\n }\n : null,\n events: rows\n .filter((row) => row['row_kind'] === 'event')\n .map(toEvent),\n }\n })\n },\n\n async readStates(sessionIds, reducerName) {\n if (sessionIds.length === 0) return []\n return wrap(async () => {\n const c = await client()\n // One statement, one MVCC snapshot — each session's tail is\n // bounded by the same snapshot row the read returns.\n const { rows } = await c.query(\n `with snapshot as materialized (\n select session_id, up_to_index, state\n from a2_snapshots\n where session_id = any($1::text[]) and reducer_name = $2\n )\n select 0 as row_order, 'snapshot' as row_kind,\n snapshot.session_id,\n snapshot.up_to_index as snapshot_index,\n snapshot.state as snapshot_state,\n null::bigint as idx, null::text as event_type,\n null::jsonb as payload, null::text as event_id,\n null::bigint as created_at\n from snapshot\n union all\n select 1 as row_order, 'event' as row_kind,\n event.session_id,\n null::bigint as snapshot_index,\n null::jsonb as snapshot_state,\n event.idx, event.event_type, event.payload,\n event.event_id, event.created_at\n from a2_events event\n left join snapshot on snapshot.session_id = event.session_id\n where event.session_id = any($1::text[])\n and event.idx > coalesce(snapshot.up_to_index, 0)\n order by row_order, session_id, idx`,\n [sessionIds, reducerName],\n )\n const bySession = new Map<string, StoreStateRead>()\n const readOf = (sessionId: string): StoreStateRead => {\n let read = bySession.get(sessionId)\n if (!read) {\n read = { snapshot: null, events: [] }\n bySession.set(sessionId, read)\n }\n return read\n }\n for (const row of rows) {\n const read = readOf(row['session_id'] as string)\n if (row['row_kind'] === 'snapshot') {\n read.snapshot = {\n index: asNumber(row['snapshot_index']),\n state: row['snapshot_state'],\n }\n } else {\n read.events.push(toEvent(row))\n }\n }\n return sessionIds.map(\n (sessionId) =>\n bySession.get(sessionId) ?? { snapshot: null, events: [] },\n )\n })\n },\n\n async putSnapshot(sessionId, reducerName, index, state) {\n await wrap(async () => {\n const c = await client()\n // Guarded upsert: a slower concurrent writer must never clobber\n // a further-along snapshot (spec §3).\n await c.query(\n `insert into a2_snapshots (session_id, reducer_name, up_to_index, state, updated_at)\n values ($1, $2, $3, $4::jsonb, $5)\n on conflict (session_id, reducer_name) do update set\n up_to_index = excluded.up_to_index,\n state = excluded.state,\n updated_at = excluded.updated_at\n where excluded.up_to_index > a2_snapshots.up_to_index`,\n [sessionId, reducerName, index, jsonb(state), clock.now().getTime()],\n )\n })\n },\n\n presence: {\n async set(ns, participant, values, meta) {\n const entries = Object.entries(values)\n if (entries.length === 0) return\n await wrap(async () => {\n const c = await client()\n const atMs = meta.at.getTime()\n // Expiry anchors on the storage clock — the sender's `at`\n // orders writes but never anchors their lifetime.\n const expiresAtMs = clock.now().getTime() + meta.ttlMs\n for (const [field, value] of entries) {\n // Field-wise LWW: a stored row with a strictly newer `at`\n // survives; ties go to the incoming write (memory parity).\n // oxlint-disable-next-line no-await-in-loop -- independent per-field registers\n await (value === null\n ? c.query(\n `delete from a2_presence\n where ns = $1 and participant = $2 and field = $3\n and at <= $4`,\n [ns, participant, field, atMs],\n )\n : c.query(\n `insert into a2_presence\n (ns, participant, field, value, seen, at, expires_at)\n values ($1, $2, $3, $4::jsonb, $5, $6, $7)\n on conflict (ns, participant, field) do update set\n value = excluded.value,\n seen = excluded.seen,\n at = excluded.at,\n expires_at = excluded.expires_at\n where a2_presence.at <= excluded.at`,\n [\n ns,\n participant,\n field,\n jsonb(value),\n meta.seen,\n atMs,\n expiresAtMs,\n ],\n ))\n }\n })\n },\n\n async read(ns) {\n return wrap(async () => {\n const c = await client()\n const now = clock.now().getTime()\n // Sweep and read share one statement (one snapshot): the\n // delete is opportunistic cleanup; the filter alone keeps\n // expired rows out of this result.\n const { rows } = await c.query(\n `with swept as (\n delete from a2_presence\n where ns = $1 and expires_at <= $2\n )\n select participant, field, value, seen, at, expires_at\n from a2_presence\n where ns = $1 and expires_at > $2\n order by participant, field`,\n [ns, now],\n )\n return rows.map((row) => ({\n participant: row['participant'] as string,\n field: row['field'] as string,\n value: row['value'],\n seen: asNumber(row['seen']),\n at: new Date(asNumber(row['at'])),\n expiresAt: new Date(asNumber(row['expires_at'])),\n }))\n })\n },\n },\n\n inspect: {\n async listSessions(inspectionOptions) {\n return wrap(async () => {\n const c = await client()\n const conditions = ['left(session_id, length($1)) = $1']\n const params: unknown[] = [inspectionOptions.prefix]\n if (inspectionOptions.cursor !== undefined) {\n params.push(inspectionOptions.cursor)\n conditions.push(`session_id > $${params.length}`)\n }\n params.push(inspectionOptions.limit + 1)\n const { rows } = await c.query(\n `select\n session_id,\n count(*) as event_count,\n count(*) filter (where processed_at is null and failed_at is null) as pending_count,\n count(*) filter (where failed_at is not null) as failed_count,\n sum(attempt_count) as attempt_count,\n sum(failure_count) as failure_count,\n min(created_at) as first_event_at,\n max(greatest(\n created_at,\n coalesce(first_claimed_at, 0),\n coalesce(last_claimed_at, 0),\n coalesce(last_failed_at, 0),\n coalesce(processed_at, 0),\n coalesce(failed_at, 0)\n )) as updated_at\n from a2_events\n where ${conditions.join(' and ')}\n group by session_id\n order by session_id\n limit $${params.length}`,\n params,\n )\n const hasMore = rows.length > inspectionOptions.limit\n const visible = rows.slice(0, inspectionOptions.limit)\n return {\n sessions: visible.map((row) => ({\n sessionId: row['session_id'] as string,\n eventCount: asNumber(row['event_count']),\n pendingCount: asNumber(row['pending_count']),\n failedCount: asNumber(row['failed_count']),\n attemptCount: asNumber(row['attempt_count']),\n failureCount: asNumber(row['failure_count']),\n firstEventAt: new Date(asNumber(row['first_event_at'])),\n updatedAt: new Date(asNumber(row['updated_at'])),\n })),\n cursor: hasMore\n ? ((visible.at(-1)?.['session_id'] as string | undefined) ?? null)\n : null,\n }\n })\n },\n async readEvents(sessionId, pageOptions) {\n return wrap(async () => {\n const c = await client()\n const currentFrontier = asNumber(\n (\n await c.query(\n 'select coalesce(max(idx), 0) as frontier from a2_events where session_id = $1',\n [sessionId],\n )\n ).rows[0]?.['frontier'] ?? 0,\n )\n const throughIndex = Math.min(\n pageOptions.throughIndex ?? currentFrontier,\n currentFrontier,\n )\n const { rows } = await c.query(\n `select * from a2_events\n where session_id = $1 and idx > $2 and idx <= $3\n order by idx limit $4`,\n [\n sessionId,\n pageOptions.afterIndex,\n throughIndex,\n pageOptions.limit,\n ],\n )\n return { events: rows.map(toStored), throughIndex }\n })\n },\n async listSnapshots(sessionId) {\n return wrap(async () => {\n const c = await client()\n const { rows } = await c.query(\n `select reducer_name, up_to_index, updated_at\n from a2_snapshots where session_id = $1 order by reducer_name`,\n [sessionId],\n )\n return rows.map((row) => ({\n reducerName: row['reducer_name'] as string,\n index: asNumber(row['up_to_index']),\n updatedAt: new Date(asNumber(row['updated_at'])),\n }))\n })\n },\n },\n\n stream(sessionId, opts) {\n const readAfter = (afterIndex: number): Promise<StoredEvent[]> =>\n wrap(async () => {\n const c = await client()\n const { rows } = await c.query(\n 'select * from a2_events where session_id = $1 and idx > $2 order by idx',\n [sessionId, afterIndex],\n )\n return rows.map(toStored)\n })\n return pollingStream(\n readAfter,\n opts?.startAfter !== undefined ? { startAfter: opts.startAfter } : {},\n )\n },\n\n async close() {\n const current = connection.peek()\n if (!current) return\n const c = await current\n await c.end?.()\n },\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as A2Error } from "./errors-BQuJpe82.js";
|
|
2
|
-
import { g as nullProtoRecord } from "./internal-
|
|
2
|
+
import { g as nullProtoRecord } from "./internal-DstsI6Re.js";
|
|
3
3
|
import { t as idempotentReplay } from "./idempotent-replay-DuqEkYA7.js";
|
|
4
4
|
import { t as decodeReturnedEventIds } from "./store-codec-DTG0Ftek.js";
|
|
5
5
|
//#region src/store-redis-core.ts
|
|
@@ -1037,3 +1037,5 @@ function createRedisStoreCore(options) {
|
|
|
1037
1037
|
}
|
|
1038
1038
|
//#endregion
|
|
1039
1039
|
export { createRedisStoreCore as t };
|
|
1040
|
+
|
|
1041
|
+
//# sourceMappingURL=store-redis-core-BFLwz0Wj.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store-redis-core-BFLwz0Wj.js","names":[],"sources":["../src/store-redis-core.ts"],"sourcesContent":["/**\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 A2StoreInspection,\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 /**\n * Disposable wake-up fired after a write appends new events to a\n * watched session — `count` is the session's new event count. The\n * watched check runs inside the write script, so unwatched sessions\n * cost no wake-up command at all. Losing one is harmless: live\n * feeds re-read on a safety cadence, so notifications only affect\n * latency, never delivery.\n */\n notify?: (sessionId: string, count: number) => void\n}\n\nexport type RedisStoreCore = Omit<\n A2Store,\n 'stream' | 'inspect' | 'presence'\n> & {\n inspect: A2StoreInspection\n /**\n * The storage half of the presence capability, shared by both\n * transports. `set` returns the applied patch — null when every\n * field lost the LWW race — so a push-capable transport broadcasts\n * exactly what `read` now reflects; delivery is its business.\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 /** XRANGE only — the light catch-up read live feeds use. */\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\n\n// ── Lua scripts (each runs atomically) ───────────────────────────────\n\n/**\n * KEYS: store stream, counter, global id hash, session index, metadata,\n * pending, ready, watched.\n * ARGV: sessionId, nowMs, n, then per event:\n * id, type, payloadJson, causeJson, lane, settled.\n * Returns ['empty', hasPending] | ['ok', firstIndex, hasPending, watched] |\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[6], idx, field)\n if lane == '' then\n redis.call('ZADD', KEYS[7], idx, field)\n return\n end\n local tail_key = lane_field(lane, 'tail')\n local tail = redis.call('HGET', KEYS[5], tail_key)\n if tail then\n redis.call('HSET', KEYS[5], tail .. ':next', field)\n else\n redis.call('HSET', KEYS[5], lane_field(lane, 'head'), field)\n redis.call('ZADD', KEYS[7], idx, field)\n end\n redis.call('HSET', KEYS[5], 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[6]) > 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[5], field .. ':d') or '0')\n if dispatches ~= tonumber(parsed.attempt) or\n redis.call('HEXISTS', KEYS[5], 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[5], tostring(idx) .. ':l', lane)\n if settled == '1' then\n redis.call('HSET', KEYS[5], tostring(idx) .. ':p', ARGV[2])\n else\n enqueue(tostring(idx), idx, lane)\n end\nend\nredis.call('SET', KEYS[2], tostring(base + n))\nredis.call('ZADD', KEYS[4], 0, ARGV[1])\nhas_pending = redis.call('ZCARD', KEYS[6]) > 0 and '1' or '0'\nreturn { 'ok', tostring(base + 1), has_pending,\n tostring(redis.call('EXISTS', KEYS[8])) }\n`\n\n/** KEYS: counter, meta, pending, ready. ARGV: idx, attempt, error, maxFailures, nowMs. */\nconst FAIL_ATTEMPT_LUA = `\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 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, session index, meta, pending, ready, watched.\n * ARGV: sessionId, nowMs, parent index, attempt, n, then per child:\n * id, type, payloadJson, lane, settled.\n */\nconst COMPLETE_ATTEMPT_LUA = `\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[6], field)\n redis.call('ZREM', KEYS[7], field)\n local lane = redis.call('HGET', KEYS[5], field .. ':l') or ''\n if lane == '' then return end\n local head_key = lane_field(lane, 'head')\n if redis.call('HGET', KEYS[5], head_key) ~= field then return end\n local next_field = redis.call('HGET', KEYS[5], field .. ':next')\n while next_field and redis.call('HEXISTS', KEYS[5], next_field .. ':p') == 1 do\n next_field = redis.call('HGET', KEYS[5], next_field .. ':next')\n end\n if next_field then\n redis.call('HSET', KEYS[5], head_key, next_field)\n if redis.call('HEXISTS', KEYS[5], next_field .. ':f') == 0 then\n redis.call('ZADD', KEYS[7], tonumber(next_field), next_field)\n end\n else\n redis.call('HDEL', KEYS[5], head_key, lane_field(lane, 'tail'))\n end\nend\nlocal function enqueue(field, idx, lane)\n redis.call('ZADD', KEYS[6], idx, field)\n if lane == '' then\n redis.call('ZADD', KEYS[7], idx, field)\n return\n end\n local tail_key = lane_field(lane, 'tail')\n local tail = redis.call('HGET', KEYS[5], tail_key)\n if tail then\n redis.call('HSET', KEYS[5], tail .. ':next', field)\n else\n redis.call('HSET', KEYS[5], lane_field(lane, 'head'), field)\n redis.call('ZADD', KEYS[7], idx, field)\n end\n redis.call('HSET', KEYS[5], 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[5], parent .. ':d') or '0')\nlocal processed = redis.call('HGET', KEYS[5], parent .. ':p')\nlocal processed_attempt = tonumber(redis.call('HGET', KEYS[5], 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[5], 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[5], parent .. ':ch') == 0 or\n redis.call('HEXISTS', KEYS[5], parent .. ':f') == 1 or\n tonumber(redis.call('HGET', KEYS[5], 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[5],\n parent .. ':p', ARGV[2],\n parent .. ':pa', ARGV[4],\n parent .. ':ri', returned_json)\nredis.call('HDEL', KEYS[5], parent .. ':ch', parent .. ':ce')\nsettle(parent)\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[5], tostring(idx) .. ':l', lane)\n if settled == '1' then\n redis.call('HSET', KEYS[5], tostring(idx) .. ':p', ARGV[2])\n else\n enqueue(tostring(idx), idx, lane)\n end\nend\nredis.call('SET', KEYS[2], tostring(total + n))\nredis.call('ZADD', KEYS[4], 0, ARGV[1])\nreturn { 'ok', tostring(total + 1),\n tostring(redis.call('EXISTS', KEYS[8])) }\n`\n\n/** KEYS: snapshot. ARGV: upToIndex, json. Guarded: never move backward. */\nconst PUT_SNAPSHOT_LUA = `\nlocal cur = redis.call('GET', KEYS[1])\nif cur and tonumber(cjson.decode(cur)['index']) >= tonumber(ARGV[1]) then\n return 0\nend\nredis.call('SET', KEYS[1], ARGV[2])\nreturn 1\n`\n\n/**\n * KEYS: presence hash. ARGV: atMs, ttlMs, n, then per entry:\n * hashField, valueJson ('' deletes). 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 = {}\nfor i = 1, tonumber(ARGV[3]) do\n local field = ARGV[3 + (i - 1) * 2 + 1]\n local value = ARGV[3 + (i - 1) * 2 + 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 end\nend\nif #applied > 0 then redis.call('PEXPIRE', KEYS[1], ARGV[2]) end\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/** KEYS: snapshot, event counter. Returns one atomic cache-plus-frontier read. */\nconst READ_STATE_LUA = `\nlocal snapshot = redis.call('GET', KEYS[1])\nlocal frontier = redis.call('GET', KEYS[2]) or '0'\nreturn { 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 escapeGlob = (value: string): string =>\n value\n .replaceAll('\\\\', '\\\\\\\\')\n .replaceAll('*', '\\\\*')\n .replaceAll('?', '\\\\?')\n .replaceAll('[', '\\\\[')\n .replaceAll(']', '\\\\]')\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, notify } = 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 watchedKey = (s: string): string => `${prefix}:${s}:watched`\n const presenceKey = (s: string): string => `${prefix}:${s}:presence`\n const idsKey = `${prefix}:ids`\n const sessionsKey = `${prefix}:sessions`\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 readEventRange = 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 /** 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 ]\n const reply = (await evalScript(\n APPEND_LUA,\n [\n storeKey(sessionId),\n countKey(sessionId),\n idsKey,\n sessionsKey,\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 if (reply[3] === '1') notify?.(sessionId, base + withIds.length)\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 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 sessionsKey,\n metaKey(sessionId),\n pendingKey(sessionId),\n readyKey(sessionId),\n watchedKey(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 ],\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 if (events.length > 0 && reply[2] === '1') {\n notify?.(sessionId, base + events.length)\n }\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 ],\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) {\n return wrap(async () => {\n const reply = (await evalScript(\n READ_STATE_LUA,\n [snapKey(sessionId, reducerName), countKey(sessionId)],\n [],\n )) as [string, string]\n const raw = reply[0]\n const parsed =\n raw === ''\n ? null\n : (JSON.parse(raw) as { index: number; state: unknown })\n const frontier = Number(reply[1])\n if (\n !Number.isSafeInteger(frontier) ||\n frontier < 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 events = await readEventRange(\n sessionId,\n parsed?.index ?? 0,\n frontier,\n )\n return {\n snapshot: parsed\n ? { index: parsed.index, state: parsed.state }\n : null,\n events,\n }\n })\n },\n\n async putSnapshot(sessionId, reducerName, index, state) {\n await wrap(async () => {\n await evalScript(\n PUT_SNAPSHOT_LUA,\n [snapKey(sessionId, reducerName)],\n [index, json({ index, state, updatedAt: clock.now().getTime() })],\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 ]),\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 inspect: {\n async listSessions(inspectionOptions) {\n return wrap(async () => {\n const minimum =\n inspectionOptions.cursor === undefined\n ? `[${inspectionOptions.prefix}`\n : `(${inspectionOptions.cursor}`\n const candidates = (await call(\n 'ZRANGEBYLEX',\n sessionsKey,\n minimum,\n '+',\n 'LIMIT',\n 0,\n inspectionOptions.limit + 1,\n )) as string[]\n const sessionIds = candidates\n .filter((id) => id.startsWith(inspectionOptions.prefix))\n .slice(0, inspectionOptions.limit)\n\n const sessions = await Promise.all(\n sessionIds.map(async (sessionId) => {\n const rows = await readRange(sessionId, 0)\n const first = rows[0]!\n let updatedAt = first.createdAt\n let pendingCount = 0\n let failedCount = 0\n let attemptCount = 0\n let failureCount = 0\n for (const row of rows) {\n attemptCount += row.attemptCount\n failureCount += row.failureCount\n if (row.processedAt === null && row.failedAt === null) {\n pendingCount += 1\n }\n if (row.failedAt !== null) failedCount += 1\n for (const timestamp of [\n row.createdAt,\n row.firstClaimedAt,\n row.lastClaimedAt,\n row.lastFailedAt,\n row.processedAt,\n row.failedAt,\n ]) {\n if (timestamp && timestamp > updatedAt) updatedAt = timestamp\n }\n }\n return {\n sessionId,\n eventCount: rows.length,\n pendingCount,\n failedCount,\n attemptCount,\n failureCount,\n firstEventAt: first.createdAt,\n updatedAt,\n }\n }),\n )\n return {\n sessions: sessions.toSorted((a, b) =>\n a.sessionId.localeCompare(b.sessionId),\n ),\n cursor:\n candidates.length > inspectionOptions.limit &&\n candidates[inspectionOptions.limit]?.startsWith(\n inspectionOptions.prefix,\n )\n ? (sessionIds.at(-1) ?? null)\n : null,\n }\n })\n },\n async readEvents(sessionId, pageOptions) {\n return wrap(async () => {\n const currentFrontier = await readFrontier(sessionId)\n const throughIndex = Math.min(\n pageOptions.throughIndex ?? currentFrontier,\n currentFrontier,\n )\n const events = await readRangePage(\n sessionId,\n pageOptions.afterIndex,\n throughIndex,\n pageOptions.limit,\n )\n if (throughIndex > pageOptions.afterIndex && events.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 < events.length; position += 1) {\n if (\n events[position]!.index !==\n pageOptions.afterIndex + position + 1\n ) {\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n `redis event range for session '${sessionId}' is not contiguous`,\n )\n }\n }\n return { events, throughIndex }\n })\n },\n async listSnapshots(sessionId) {\n return wrap(async () => {\n const start = `${prefix}:${sessionId}:snap:`\n let cursor = '0'\n const keys: string[] = []\n do {\n // oxlint-disable-next-line no-await-in-loop -- each SCAN cursor comes from the prior reply\n const reply = (await call(\n 'SCAN',\n cursor,\n 'MATCH',\n `${escapeGlob(start)}*`,\n 'COUNT',\n 100,\n )) as [string, string[]]\n cursor = reply[0]\n keys.push(...reply[1].filter((key) => key.startsWith(start)))\n } while (cursor !== '0')\n const snapshots = await Promise.all(\n keys.map(async (key) => {\n const raw = (await call('GET', key)) as string | null\n if (raw === null) return null\n const parsed = JSON.parse(raw) as {\n index: number\n updatedAt?: number\n }\n return {\n reducerName: key.slice(start.length),\n index: parsed.index,\n updatedAt: new Date(parsed.updatedAt ?? 0),\n }\n }),\n )\n return snapshots\n .filter((snapshot) => snapshot !== null)\n .toSorted((a, b) => a.reducerName.localeCompare(b.reducerName))\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 )) 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"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as IdSource, i as Clock, t as A2Store } from "./store-
|
|
1
|
+
import { c as IdSource, i as Clock, t as A2Store } from "./store-DysUkTH3.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`. */
|
|
@@ -18,4 +18,5 @@ type RedisHttpStore = A2Store & {
|
|
|
18
18
|
};
|
|
19
19
|
declare function redisHttp(options: RedisHttpStoreOptions): RedisHttpStore;
|
|
20
20
|
//#endregion
|
|
21
|
-
export { RedisHttpStore, RedisHttpStoreOptions, redisHttp };
|
|
21
|
+
export { RedisHttpStore, RedisHttpStoreOptions, redisHttp };
|
|
22
|
+
//# sourceMappingURL=store-redis-http.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store-redis-http.d.ts","names":[],"sources":["../src/store-redis-http.ts"],"mappings":";;KA0BY;;EAEV;;EAEA;;EAEA;;EAEA,QAAQ;;EAER,MAAM;;KAGI,iBAAiB;;EAE3B,SAAS;;iBAGK,UAAU,SAAS,wBAAwB"}
|
package/dist/store-redis-http.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { t as A2Error } from "./errors-BQuJpe82.js";
|
|
2
|
-
import { n as pollingStream } from "./store-polling-
|
|
2
|
+
import { n as pollingStream } from "./store-polling-dSeLxzfb.js";
|
|
3
3
|
import { n as SYSTEM_CLOCK, t as RANDOM_IDS } from "./store-N8PXxDAS.js";
|
|
4
|
-
import { t as createRedisStoreCore } from "./store-redis-core-
|
|
4
|
+
import { t as createRedisStoreCore } from "./store-redis-core-BFLwz0Wj.js";
|
|
5
5
|
//#region src/store-redis-http.ts
|
|
6
6
|
/**
|
|
7
7
|
* experimental-a2/store-redis-http — the same Redis Streams store over a
|
|
@@ -68,3 +68,5 @@ function redisHttp(options) {
|
|
|
68
68
|
}
|
|
69
69
|
//#endregion
|
|
70
70
|
export { redisHttp };
|
|
71
|
+
|
|
72
|
+
//# sourceMappingURL=store-redis-http.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store-redis-http.js","names":[],"sources":["../src/store-redis-http.ts"],"sourcesContent":["/**\n * experimental-a2/store-redis-http — the same Redis Streams store over a\n * provider REST API (Upstash-shaped: POST one command as a JSON array,\n * receive `{ result }` or `{ error }`). The storage semantics live in\n * store-redis-core.ts, shared with experimental-a2/store-redis.\n *\n * Fully connectionless: every command is one `fetch`, so it works\n * where a Redis-protocol connection cannot exist or cannot be spared.\n * Without a connection there is no push and no pub/sub, so `stream()`\n * is the shared activity-adaptive poll loop (25ms while events flow,\n * backing off to a 250ms idle ceiling), and presence is the degraded\n * tier: `set`/`read` from the shared core, no `subscribe`. No peer\n * dependencies.\n */\n\nimport { A2Error } from './errors.ts'\nimport { pollingStream } from './store-polling.ts'\nimport { createRedisStoreCore } from './store-redis-core.ts'\nimport {\n RANDOM_IDS,\n SYSTEM_CLOCK,\n type A2Store,\n type Clock,\n type IdSource,\n} from './store.ts'\n\nexport type RedisHttpStoreOptions = {\n /** REST endpoint, e.g. `process.env.UPSTASH_REDIS_REST_URL`. */\n url: string\n /** Bearer token, e.g. `process.env.UPSTASH_REDIS_REST_TOKEN`. */\n token: string\n /** Key prefix — isolates multiple apps on one Redis. Default `'a2'`. */\n keyPrefix?: string\n /** Injectable clock — every stored timestamp comes from here. */\n clock?: Clock\n /** Injectable id source for generated event ids. */\n ids?: IdSource\n}\n\nexport type RedisHttpStore = A2Store & {\n /** Nothing to disconnect; present so callers can treat stores uniformly. */\n close(): Promise<void>\n}\n\nexport function redisHttp(options: RedisHttpStoreOptions): RedisHttpStore {\n if (!options.url || !options.token) {\n throw new TypeError('redisHttp() needs a url and a token')\n }\n const { url, token } = options\n\n const call = async (\n command: string,\n ...args: Array<string | number>\n ): Promise<unknown> => {\n let response: Response\n try {\n response = await fetch(url, {\n method: 'POST',\n headers: {\n authorization: `Bearer ${token}`,\n 'content-type': 'application/json',\n },\n body: JSON.stringify([command, ...args.map(String)]),\n })\n } catch (err) {\n throw new A2Error('STORE_UNAVAILABLE', 'redis rest request failed', {\n cause: err,\n })\n }\n const body = (await response.json().catch(() => null)) as {\n result?: unknown\n error?: string\n } | null\n if (body === null || !response.ok || body.error !== undefined) {\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n `redis rest command failed: ${body?.error ?? `status ${response.status}`}`,\n )\n }\n return body.result\n }\n\n const core = createRedisStoreCore({\n call,\n clock: options.clock ?? SYSTEM_CLOCK,\n ids: options.ids ?? RANDOM_IDS,\n keyPrefix: options.keyPrefix ?? 'a2',\n })\n\n return {\n append: core.append,\n read: core.read,\n claimAvailable: core.claimAvailable,\n renewClaims: core.renewClaims,\n completeAttempt: core.completeAttempt,\n failAttempt: core.failAttempt,\n readState: core.readState,\n putSnapshot: core.putSnapshot,\n presence: {\n async set(ns, participant, values, meta) {\n await core.presence.set(ns, participant, values, meta)\n },\n read: core.presence.read,\n },\n inspect: core.inspect,\n\n stream(sessionId, opts) {\n return pollingStream(\n (afterIndex) => core.readRange(sessionId, afterIndex),\n opts?.startAfter !== undefined ? { startAfter: opts.startAfter } : {},\n )\n },\n\n async close() {},\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AA4CA,SAAgB,UAAU,SAAgD;CACxE,IAAI,CAAC,QAAQ,OAAO,CAAC,QAAQ,OAC3B,MAAM,IAAI,UAAU,qCAAqC;CAE3D,MAAM,EAAE,KAAK,UAAU;CAEvB,MAAM,OAAO,OACX,SACA,GAAG,SACkB;EACrB,IAAI;EACJ,IAAI;GACF,WAAW,MAAM,MAAM,KAAK;IAC1B,QAAQ;IACR,SAAS;KACP,eAAe,UAAU;KACzB,gBAAgB;IAClB;IACA,MAAM,KAAK,UAAU,CAAC,SAAS,GAAG,KAAK,IAAI,MAAM,CAAC,CAAC;GACrD,CAAC;EACH,SAAS,KAAK;GACZ,MAAM,IAAI,QAAQ,qBAAqB,6BAA6B,EAClE,OAAO,IACT,CAAC;EACH;EACA,MAAM,OAAQ,MAAM,SAAS,KAAK,CAAC,CAAC,YAAY,IAAI;EAIpD,IAAI,SAAS,QAAQ,CAAC,SAAS,MAAM,KAAK,UAAU,KAAA,GAClD,MAAM,IAAI,QACR,qBACA,8BAA8B,MAAM,SAAS,UAAU,SAAS,UAClE;EAEF,OAAO,KAAK;CACd;CAEA,MAAM,OAAO,qBAAqB;EAChC;EACA,OAAO,QAAQ,SAAS;EACxB,KAAK,QAAQ,OAAO;EACpB,WAAW,QAAQ,aAAa;CAClC,CAAC;CAED,OAAO;EACL,QAAQ,KAAK;EACb,MAAM,KAAK;EACX,gBAAgB,KAAK;EACrB,aAAa,KAAK;EAClB,iBAAiB,KAAK;EACtB,aAAa,KAAK;EAClB,WAAW,KAAK;EAChB,aAAa,KAAK;EAClB,UAAU;GACR,MAAM,IAAI,IAAI,aAAa,QAAQ,MAAM;IACvC,MAAM,KAAK,SAAS,IAAI,IAAI,aAAa,QAAQ,IAAI;GACvD;GACA,MAAM,KAAK,SAAS;EACtB;EACA,SAAS,KAAK;EAEd,OAAO,WAAW,MAAM;GACtB,OAAO,eACJ,eAAe,KAAK,UAAU,WAAW,UAAU,GACpD,MAAM,eAAe,KAAA,IAAY,EAAE,YAAY,KAAK,WAAW,IAAI,CAAC,CACtE;EACF;EAEA,MAAM,QAAQ,CAAC;CACjB;AACF"}
|
package/dist/store-redis.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as IdSource, i as Clock, t as A2Store } from "./store-
|
|
1
|
+
import { c as IdSource, i as Clock, t as A2Store } from "./store-DysUkTH3.js";
|
|
2
2
|
//#region src/store-redis.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* The minimal client this backend needs — `ioredis` matches it
|
|
@@ -34,4 +34,5 @@ type RedisStore = A2Store & {
|
|
|
34
34
|
};
|
|
35
35
|
declare function redis(options?: RedisStoreOptions): RedisStore;
|
|
36
36
|
//#endregion
|
|
37
|
-
export { RedisConnection, RedisStore, RedisStoreOptions, redis };
|
|
37
|
+
export { RedisConnection, RedisStore, RedisStoreOptions, redis };
|
|
38
|
+
//# sourceMappingURL=store-redis.d.ts.map
|