experimental-a2 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/ai-server.d.ts +4 -5
  3. package/dist/ai-server.d.ts.map +1 -1
  4. package/dist/ai-server.js +8 -7
  5. package/dist/ai-server.js.map +1 -1
  6. package/dist/ai.d.ts +334 -2
  7. package/dist/ai.d.ts.map +1 -0
  8. package/dist/ai.js +1 -1
  9. package/dist/client.d.ts +202 -2
  10. package/dist/client.d.ts.map +1 -0
  11. package/dist/client.js +1025 -1
  12. package/dist/client.js.map +1 -0
  13. package/dist/errors-BQuJpe82.js.map +1 -1
  14. package/dist/index.d.ts +22 -3
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/{internal-DstsI6Re.js → internal-DRXJ56EI.js} +5 -28
  17. package/dist/internal-DRXJ56EI.js.map +1 -0
  18. package/dist/react.d.ts +1 -1
  19. package/dist/react.js +1 -1
  20. package/dist/scheduler-qstash.d.ts +3 -3
  21. package/dist/scheduler-qstash.js +4 -5
  22. package/dist/scheduler-qstash.js.map +1 -1
  23. package/dist/scheduler-vercel.d.ts +2 -2
  24. package/dist/scheduler-vercel.js +4 -4
  25. package/dist/scheduler-vercel.js.map +1 -1
  26. package/dist/{server-Duw6MVlB.js → server-286j79Mt.js} +708 -79
  27. package/dist/server-286j79Mt.js.map +1 -0
  28. package/dist/{server-DpvjhdoE.d.ts → server-DgXmORIq.d.ts} +67 -49
  29. package/dist/server-DgXmORIq.d.ts.map +1 -0
  30. package/dist/server.d.ts +3 -3
  31. package/dist/server.js +1 -1
  32. package/dist/store-N8PXxDAS.js.map +1 -1
  33. package/dist/{store-DysUkTH3.d.ts → store-flRz1OWh.d.ts} +2 -57
  34. package/dist/store-flRz1OWh.d.ts.map +1 -0
  35. package/dist/store-memory.d.ts +1 -1
  36. package/dist/store-memory.d.ts.map +1 -1
  37. package/dist/store-memory.js +1 -59
  38. package/dist/store-memory.js.map +1 -1
  39. package/dist/{store-polling-dSeLxzfb.js → store-polling-6DW7F1DT.js} +2 -2
  40. package/dist/{store-polling-dSeLxzfb.js.map → store-polling-6DW7F1DT.js.map} +1 -1
  41. package/dist/store-postgres.d.ts +1 -1
  42. package/dist/store-postgres.js +1 -81
  43. package/dist/store-postgres.js.map +1 -1
  44. package/dist/{store-redis-core-BFLwz0Wj.js → store-redis-core-DEYO8Ryv.js} +48 -134
  45. package/dist/store-redis-core-DEYO8Ryv.js.map +1 -0
  46. package/dist/store-redis-http.d.ts +1 -1
  47. package/dist/store-redis-http.js +2 -3
  48. package/dist/store-redis-http.js.map +1 -1
  49. package/dist/store-redis.d.ts +1 -1
  50. package/dist/store-redis.js +3 -4
  51. package/dist/store-redis.js.map +1 -1
  52. package/dist/store-sqlite.d.ts +1 -1
  53. package/dist/store-sqlite.d.ts.map +1 -1
  54. package/dist/store-sqlite.js +1 -72
  55. package/dist/store-sqlite.js.map +1 -1
  56. package/dist/{wire-BFQmSJ-9.js → wire-B6te_wns.js} +4 -3
  57. package/dist/wire-B6te_wns.js.map +1 -0
  58. package/docs/guides/03-react.mdx +20 -28
  59. package/docs/guides/05-production.mdx +9 -11
  60. package/docs/guides/06-ai-agents.mdx +8 -14
  61. package/docs/guides/09-presence.mdx +14 -19
  62. package/docs/guides/10-transports.mdx +104 -86
  63. package/docs/reference/01-api.mdx +143 -277
  64. package/docs/reference/02-errors.mdx +5 -7
  65. package/package.json +1 -14
  66. package/src/ai-server.ts +9 -5
  67. package/src/client.ts +2 -2
  68. package/src/errors.ts +1 -0
  69. package/src/internal.ts +3 -62
  70. package/src/push-envelope.ts +24 -21
  71. package/src/scheduler-qstash.ts +3 -3
  72. package/src/scheduler-vercel.ts +2 -2
  73. package/src/server-fetch.ts +344 -0
  74. package/src/server.ts +73 -225
  75. package/src/session-socket.ts +36 -20
  76. package/src/sse.ts +2 -2
  77. package/src/store-memory.ts +0 -81
  78. package/src/store-postgres.ts +0 -100
  79. package/src/store-redis-core.ts +47 -211
  80. package/src/store-redis-http.ts +0 -1
  81. package/src/store-redis.ts +0 -1
  82. package/src/store-sqlite.ts +0 -119
  83. package/src/store.ts +0 -60
  84. package/src/wire.ts +2 -1
  85. package/dist/ai-D_PGS-JR.d.ts +0 -334
  86. package/dist/ai-D_PGS-JR.d.ts.map +0 -1
  87. package/dist/cli-B3VuxoDe.js +0 -599
  88. package/dist/cli-B3VuxoDe.js.map +0 -1
  89. package/dist/cli-bin.d.ts +0 -1
  90. package/dist/cli-bin.js +0 -7
  91. package/dist/cli-bin.js.map +0 -1
  92. package/dist/cli.d.ts +0 -20
  93. package/dist/cli.d.ts.map +0 -1
  94. package/dist/cli.js +0 -2
  95. package/dist/client-CdMqi7mC.d.ts +0 -202
  96. package/dist/client-CdMqi7mC.d.ts.map +0 -1
  97. package/dist/client-Dj5d3SP_.js +0 -1026
  98. package/dist/client-Dj5d3SP_.js.map +0 -1
  99. package/dist/devtools-J_jZ2vQf.d.ts +0 -152
  100. package/dist/devtools-J_jZ2vQf.d.ts.map +0 -1
  101. package/dist/devtools-kJJaORn-.js +0 -340
  102. package/dist/devtools-kJJaORn-.js.map +0 -1
  103. package/dist/devtools-server.browser.d.ts +0 -1
  104. package/dist/devtools-server.browser.js +0 -6
  105. package/dist/devtools-server.browser.js.map +0 -1
  106. package/dist/devtools-server.d.ts +0 -23
  107. package/dist/devtools-server.d.ts.map +0 -1
  108. package/dist/devtools-server.js +0 -1270
  109. package/dist/devtools-server.js.map +0 -1
  110. package/dist/devtools.d.ts +0 -2
  111. package/dist/devtools.js +0 -2
  112. package/dist/errors-W6nwJ-fm.d.ts +0 -21
  113. package/dist/errors-W6nwJ-fm.d.ts.map +0 -1
  114. package/dist/http.d.ts +0 -151
  115. package/dist/http.d.ts.map +0 -1
  116. package/dist/http.js +0 -706
  117. package/dist/http.js.map +0 -1
  118. package/dist/inspection-DaxB5jM2.js +0 -13
  119. package/dist/inspection-DaxB5jM2.js.map +0 -1
  120. package/dist/internal-DstsI6Re.js.map +0 -1
  121. package/dist/platform-B4TnJtWu.js +0 -34
  122. package/dist/platform-B4TnJtWu.js.map +0 -1
  123. package/dist/server-DpvjhdoE.d.ts.map +0 -1
  124. package/dist/server-Duw6MVlB.js.map +0 -1
  125. package/dist/store-DysUkTH3.d.ts.map +0 -1
  126. package/dist/store-redis-core-BFLwz0Wj.js.map +0 -1
  127. package/dist/testing.browser.d.ts +0 -1
  128. package/dist/testing.browser.js +0 -6
  129. package/dist/testing.browser.js.map +0 -1
  130. package/dist/testing.d.ts +0 -32
  131. package/dist/testing.d.ts.map +0 -1
  132. package/dist/testing.js +0 -103
  133. package/dist/testing.js.map +0 -1
  134. package/dist/wire-BFQmSJ-9.js.map +0 -1
  135. package/docs/guides/07-devtools.mdx +0 -229
  136. package/src/cli-bin.ts +0 -5
  137. package/src/cli.ts +0 -1046
  138. package/src/devtools-app.ts +0 -989
  139. package/src/devtools-server.browser.ts +0 -5
  140. package/src/devtools-server.ts +0 -604
  141. package/src/devtools.ts +0 -716
  142. package/src/http.ts +0 -394
  143. package/src/inspection.ts +0 -39
  144. package/src/testing.browser.ts +0 -5
  145. package/src/testing.ts +0 -185
@@ -1 +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
+ {"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 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,12 +1,12 @@
1
1
  import { t as A2Error } from "./errors-BQuJpe82.js";
2
- import { g as nullProtoRecord } from "./internal-DstsI6Re.js";
2
+ import { h as nullProtoRecord } from "./internal-DRXJ56EI.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
6
6
  const READ_PAGE_SIZE = 256;
7
7
  /**
8
- * KEYS: store stream, counter, global id hash, session index, metadata,
9
- * pending, ready, watched.
8
+ * KEYS: store stream, counter, global id hash, metadata, pending, ready,
9
+ * watched.
10
10
  * ARGV: sessionId, nowMs, n, then per event:
11
11
  * id, type, payloadJson, causeJson, lane, settled.
12
12
  * Returns ['empty', hasPending] | ['ok', firstIndex, hasPending, watched] |
@@ -18,24 +18,24 @@ local function lane_field(lane, suffix)
18
18
  return '__lane:' .. tostring(string.len(lane)) .. ':' .. lane .. ':' .. suffix
19
19
  end
20
20
  local function enqueue(field, idx, lane)
21
- redis.call('ZADD', KEYS[6], idx, field)
21
+ redis.call('ZADD', KEYS[5], idx, field)
22
22
  if lane == '' then
23
- redis.call('ZADD', KEYS[7], idx, field)
23
+ redis.call('ZADD', KEYS[6], idx, field)
24
24
  return
25
25
  end
26
26
  local tail_key = lane_field(lane, 'tail')
27
- local tail = redis.call('HGET', KEYS[5], tail_key)
27
+ local tail = redis.call('HGET', KEYS[4], tail_key)
28
28
  if tail then
29
- redis.call('HSET', KEYS[5], tail .. ':next', field)
29
+ redis.call('HSET', KEYS[4], tail .. ':next', field)
30
30
  else
31
- redis.call('HSET', KEYS[5], lane_field(lane, 'head'), field)
32
- redis.call('ZADD', KEYS[7], idx, field)
31
+ redis.call('HSET', KEYS[4], lane_field(lane, 'head'), field)
32
+ redis.call('ZADD', KEYS[6], idx, field)
33
33
  end
34
- redis.call('HSET', KEYS[5], tail_key, field)
34
+ redis.call('HSET', KEYS[4], tail_key, field)
35
35
  end
36
36
  local n = tonumber(ARGV[3])
37
37
  local base = tonumber(redis.call('GET', KEYS[2]) or '0')
38
- local has_pending = redis.call('ZCARD', KEYS[6]) > 0 and '1' or '0'
38
+ local has_pending = redis.call('ZCARD', KEYS[5]) > 0 and '1' or '0'
39
39
  if n == 0 then return { 'empty', has_pending } end
40
40
  local dups = {}
41
41
  for i = 1, n do
@@ -62,9 +62,9 @@ for i = 1, n do
62
62
  return { 'no_parent', tostring(parsed.index) }
63
63
  end
64
64
  local field = tostring(pidx)
65
- local dispatches = tonumber(redis.call('HGET', KEYS[5], field .. ':d') or '0')
65
+ local dispatches = tonumber(redis.call('HGET', KEYS[4], field .. ':d') or '0')
66
66
  if dispatches ~= tonumber(parsed.attempt) or
67
- redis.call('HEXISTS', KEYS[5], field .. ':f') == 1 then
67
+ redis.call('HEXISTS', KEYS[4], field .. ':f') == 1 then
68
68
  return { 'superseded', field }
69
69
  end
70
70
  end
@@ -81,18 +81,17 @@ for i = 1, n do
81
81
  'id', id, 'type', typ, 'payload', payload, 'created_at', ARGV[2],
82
82
  'cause', cause, 'lane', lane)
83
83
  redis.call('HSET', KEYS[3], id, cjson.encode({ ARGV[1], idx }))
84
- redis.call('HSET', KEYS[5], tostring(idx) .. ':l', lane)
84
+ redis.call('HSET', KEYS[4], tostring(idx) .. ':l', lane)
85
85
  if settled == '1' then
86
- redis.call('HSET', KEYS[5], tostring(idx) .. ':p', ARGV[2])
86
+ redis.call('HSET', KEYS[4], tostring(idx) .. ':p', ARGV[2])
87
87
  else
88
88
  enqueue(tostring(idx), idx, lane)
89
89
  end
90
90
  end
91
91
  redis.call('SET', KEYS[2], tostring(base + n))
92
- redis.call('ZADD', KEYS[4], 0, ARGV[1])
93
- has_pending = redis.call('ZCARD', KEYS[6]) > 0 and '1' or '0'
92
+ has_pending = redis.call('ZCARD', KEYS[5]) > 0 and '1' or '0'
94
93
  return { 'ok', tostring(base + 1), has_pending,
95
- tostring(redis.call('EXISTS', KEYS[8])) }
94
+ tostring(redis.call('EXISTS', KEYS[7])) }
96
95
  `;
97
96
  /** KEYS: counter, meta, pending, ready. ARGV: idx, attempt, error, maxFailures, nowMs. */
98
97
  const FAIL_ATTEMPT_LUA = `
@@ -239,7 +238,7 @@ for i = 1, #superseded do result[#result + 1] = superseded[i] end
239
238
  return result
240
239
  `;
241
240
  /**
242
- * KEYS: store, counter, ids, session index, meta, pending, ready, watched.
241
+ * KEYS: store, counter, ids, meta, pending, ready, watched.
243
242
  * ARGV: sessionId, nowMs, parent index, attempt, n, then per child:
244
243
  * id, type, payloadJson, lane, settled.
245
244
  */
@@ -248,52 +247,52 @@ local function lane_field(lane, suffix)
248
247
  return '__lane:' .. tostring(string.len(lane)) .. ':' .. lane .. ':' .. suffix
249
248
  end
250
249
  local function settle(field)
250
+ redis.call('ZREM', KEYS[5], field)
251
251
  redis.call('ZREM', KEYS[6], field)
252
- redis.call('ZREM', KEYS[7], field)
253
- local lane = redis.call('HGET', KEYS[5], field .. ':l') or ''
252
+ local lane = redis.call('HGET', KEYS[4], field .. ':l') or ''
254
253
  if lane == '' then return end
255
254
  local head_key = lane_field(lane, 'head')
256
- if redis.call('HGET', KEYS[5], head_key) ~= field then return end
257
- local next_field = redis.call('HGET', KEYS[5], field .. ':next')
258
- while next_field and redis.call('HEXISTS', KEYS[5], next_field .. ':p') == 1 do
259
- next_field = redis.call('HGET', KEYS[5], next_field .. ':next')
255
+ if redis.call('HGET', KEYS[4], head_key) ~= field then return end
256
+ local next_field = redis.call('HGET', KEYS[4], field .. ':next')
257
+ while next_field and redis.call('HEXISTS', KEYS[4], next_field .. ':p') == 1 do
258
+ next_field = redis.call('HGET', KEYS[4], next_field .. ':next')
260
259
  end
261
260
  if next_field then
262
- redis.call('HSET', KEYS[5], head_key, next_field)
263
- if redis.call('HEXISTS', KEYS[5], next_field .. ':f') == 0 then
264
- redis.call('ZADD', KEYS[7], tonumber(next_field), next_field)
261
+ redis.call('HSET', KEYS[4], head_key, next_field)
262
+ if redis.call('HEXISTS', KEYS[4], next_field .. ':f') == 0 then
263
+ redis.call('ZADD', KEYS[6], tonumber(next_field), next_field)
265
264
  end
266
265
  else
267
- redis.call('HDEL', KEYS[5], head_key, lane_field(lane, 'tail'))
266
+ redis.call('HDEL', KEYS[4], head_key, lane_field(lane, 'tail'))
268
267
  end
269
268
  end
270
269
  local function enqueue(field, idx, lane)
271
- redis.call('ZADD', KEYS[6], idx, field)
270
+ redis.call('ZADD', KEYS[5], idx, field)
272
271
  if lane == '' then
273
- redis.call('ZADD', KEYS[7], idx, field)
272
+ redis.call('ZADD', KEYS[6], idx, field)
274
273
  return
275
274
  end
276
275
  local tail_key = lane_field(lane, 'tail')
277
- local tail = redis.call('HGET', KEYS[5], tail_key)
276
+ local tail = redis.call('HGET', KEYS[4], tail_key)
278
277
  if tail then
279
- redis.call('HSET', KEYS[5], tail .. ':next', field)
278
+ redis.call('HSET', KEYS[4], tail .. ':next', field)
280
279
  else
281
- redis.call('HSET', KEYS[5], lane_field(lane, 'head'), field)
282
- redis.call('ZADD', KEYS[7], idx, field)
280
+ redis.call('HSET', KEYS[4], lane_field(lane, 'head'), field)
281
+ redis.call('ZADD', KEYS[6], idx, field)
283
282
  end
284
- redis.call('HSET', KEYS[5], tail_key, field)
283
+ redis.call('HSET', KEYS[4], tail_key, field)
285
284
  end
286
285
  local parent = ARGV[3]
287
286
  local attempt = tonumber(ARGV[4])
288
287
  local n = tonumber(ARGV[5])
289
288
  local total = tonumber(redis.call('GET', KEYS[2]) or '0')
290
289
  if tonumber(parent) < 1 or tonumber(parent) > total then return { 'missing' } end
291
- local dispatches = tonumber(redis.call('HGET', KEYS[5], parent .. ':d') or '0')
292
- local processed = redis.call('HGET', KEYS[5], parent .. ':p')
293
- local processed_attempt = tonumber(redis.call('HGET', KEYS[5], parent .. ':pa') or '0')
290
+ local dispatches = tonumber(redis.call('HGET', KEYS[4], parent .. ':d') or '0')
291
+ local processed = redis.call('HGET', KEYS[4], parent .. ':p')
292
+ local processed_attempt = tonumber(redis.call('HGET', KEYS[4], parent .. ':pa') or '0')
294
293
  if processed then
295
294
  if processed_attempt ~= attempt then return { 'superseded' } end
296
- local raw_recorded_ids = redis.call('HGET', KEYS[5], parent .. ':ri')
295
+ local raw_recorded_ids = redis.call('HGET', KEYS[4], parent .. ':ri')
297
296
  if not raw_recorded_ids then return { 'partial' } end
298
297
  local recorded_ids = cjson.decode(raw_recorded_ids)
299
298
  if #recorded_ids ~= n then return { 'partial' } end
@@ -323,9 +322,9 @@ if processed then
323
322
  return result
324
323
  end
325
324
  if dispatches ~= attempt then return { 'superseded' } end
326
- if redis.call('HEXISTS', KEYS[5], parent .. ':ch') == 0 or
327
- redis.call('HEXISTS', KEYS[5], parent .. ':f') == 1 or
328
- tonumber(redis.call('HGET', KEYS[5], parent .. ':lfa') or '0') == attempt then
325
+ if redis.call('HEXISTS', KEYS[4], parent .. ':ch') == 0 or
326
+ redis.call('HEXISTS', KEYS[4], parent .. ':f') == 1 or
327
+ tonumber(redis.call('HGET', KEYS[4], parent .. ':lfa') or '0') == attempt then
329
328
  return { 'superseded' }
330
329
  end
331
330
  for i = 1, n do
@@ -336,11 +335,11 @@ local returned_ids = {}
336
335
  for i = 1, n do returned_ids[i] = ARGV[5 + (i - 1) * 5 + 1] end
337
336
  local returned_json = '[]'
338
337
  if n > 0 then returned_json = cjson.encode(returned_ids) end
339
- redis.call('HSET', KEYS[5],
338
+ redis.call('HSET', KEYS[4],
340
339
  parent .. ':p', ARGV[2],
341
340
  parent .. ':pa', ARGV[4],
342
341
  parent .. ':ri', returned_json)
343
- redis.call('HDEL', KEYS[5], parent .. ':ch', parent .. ':ce')
342
+ redis.call('HDEL', KEYS[4], parent .. ':ch', parent .. ':ce')
344
343
  settle(parent)
345
344
  local cause = cjson.encode({ index = tonumber(parent), attempt = attempt })
346
345
  for i = 1, n do
@@ -354,17 +353,16 @@ for i = 1, n do
354
353
  'id', id, 'type', typ, 'payload', payload, 'created_at', ARGV[2],
355
354
  'cause', cause, 'lane', lane)
356
355
  redis.call('HSET', KEYS[3], id, cjson.encode({ ARGV[1], idx }))
357
- redis.call('HSET', KEYS[5], tostring(idx) .. ':l', lane)
356
+ redis.call('HSET', KEYS[4], tostring(idx) .. ':l', lane)
358
357
  if settled == '1' then
359
- redis.call('HSET', KEYS[5], tostring(idx) .. ':p', ARGV[2])
358
+ redis.call('HSET', KEYS[4], tostring(idx) .. ':p', ARGV[2])
360
359
  else
361
360
  enqueue(tostring(idx), idx, lane)
362
361
  end
363
362
  end
364
363
  redis.call('SET', KEYS[2], tostring(total + n))
365
- redis.call('ZADD', KEYS[4], 0, ARGV[1])
366
364
  return { 'ok', tostring(total + 1),
367
- tostring(redis.call('EXISTS', KEYS[8])) }
365
+ tostring(redis.call('EXISTS', KEYS[7])) }
368
366
  `;
369
367
  /** KEYS: snapshot. ARGV: upToIndex, json. Guarded: never move backward. */
370
368
  const PUT_SNAPSHOT_LUA = `
@@ -465,7 +463,6 @@ const toEvent = (sessionId, entry) => {
465
463
  };
466
464
  };
467
465
  const json = (value) => JSON.stringify(value) ?? "null";
468
- const escapeGlob = (value) => value.replaceAll("\\", "\\\\").replaceAll("*", "\\*").replaceAll("?", "\\?").replaceAll("[", "\\[").replaceAll("]", "\\]");
469
466
  const toCause = (raw) => {
470
467
  if (raw === "" || raw === void 0) return null;
471
468
  const value = JSON.parse(raw);
@@ -538,7 +535,6 @@ function createRedisStoreCore(options) {
538
535
  const watchedKey = (s) => `${prefix}:${s}:watched`;
539
536
  const presenceKey = (s) => `${prefix}:${s}:presence`;
540
537
  const idsKey = `${prefix}:ids`;
541
- const sessionsKey = `${prefix}:sessions`;
542
538
  const evalScript = (script, keys, args) => call("EVAL", script, keys.length, ...keys, ...args);
543
539
  const readFrontier = async (sessionId) => {
544
540
  const frontier = Number(await call("GET", countKey(sessionId)) ?? 0);
@@ -658,7 +654,6 @@ function createRedisStoreCore(options) {
658
654
  storeKey(sessionId),
659
655
  countKey(sessionId),
660
656
  idsKey,
661
- sessionsKey,
662
657
  metaKey(sessionId),
663
658
  pendingKey(sessionId),
664
659
  readyKey(sessionId),
@@ -767,7 +762,6 @@ function createRedisStoreCore(options) {
767
762
  storeKey(sessionId),
768
763
  countKey(sessionId),
769
764
  idsKey,
770
- sessionsKey,
771
765
  metaKey(sessionId),
772
766
  pendingKey(sessionId),
773
767
  readyKey(sessionId),
@@ -942,86 +936,6 @@ function createRedisStoreCore(options) {
942
936
  });
943
937
  }
944
938
  },
945
- inspect: {
946
- async listSessions(inspectionOptions) {
947
- return wrap(async () => {
948
- const minimum = inspectionOptions.cursor === void 0 ? `[${inspectionOptions.prefix}` : `(${inspectionOptions.cursor}`;
949
- const candidates = await call("ZRANGEBYLEX", sessionsKey, minimum, "+", "LIMIT", 0, inspectionOptions.limit + 1);
950
- const sessionIds = candidates.filter((id) => id.startsWith(inspectionOptions.prefix)).slice(0, inspectionOptions.limit);
951
- return {
952
- sessions: (await Promise.all(sessionIds.map(async (sessionId) => {
953
- const rows = await readRange(sessionId, 0);
954
- const first = rows[0];
955
- let updatedAt = first.createdAt;
956
- let pendingCount = 0;
957
- let failedCount = 0;
958
- let attemptCount = 0;
959
- let failureCount = 0;
960
- for (const row of rows) {
961
- attemptCount += row.attemptCount;
962
- failureCount += row.failureCount;
963
- if (row.processedAt === null && row.failedAt === null) pendingCount += 1;
964
- if (row.failedAt !== null) failedCount += 1;
965
- for (const timestamp of [
966
- row.createdAt,
967
- row.firstClaimedAt,
968
- row.lastClaimedAt,
969
- row.lastFailedAt,
970
- row.processedAt,
971
- row.failedAt
972
- ]) if (timestamp && timestamp > updatedAt) updatedAt = timestamp;
973
- }
974
- return {
975
- sessionId,
976
- eventCount: rows.length,
977
- pendingCount,
978
- failedCount,
979
- attemptCount,
980
- failureCount,
981
- firstEventAt: first.createdAt,
982
- updatedAt
983
- };
984
- }))).toSorted((a, b) => a.sessionId.localeCompare(b.sessionId)),
985
- cursor: candidates.length > inspectionOptions.limit && candidates[inspectionOptions.limit]?.startsWith(inspectionOptions.prefix) ? sessionIds.at(-1) ?? null : null
986
- };
987
- });
988
- },
989
- async readEvents(sessionId, pageOptions) {
990
- return wrap(async () => {
991
- const currentFrontier = await readFrontier(sessionId);
992
- const throughIndex = Math.min(pageOptions.throughIndex ?? currentFrontier, currentFrontier);
993
- const events = await readRangePage(sessionId, pageOptions.afterIndex, throughIndex, pageOptions.limit);
994
- if (throughIndex > pageOptions.afterIndex && events.length === 0) throw new A2Error("STORE_UNAVAILABLE", `redis event range for session '${sessionId}' ended before index ${throughIndex}`);
995
- for (let position = 0; position < events.length; position += 1) if (events[position].index !== pageOptions.afterIndex + position + 1) throw new A2Error("STORE_UNAVAILABLE", `redis event range for session '${sessionId}' is not contiguous`);
996
- return {
997
- events,
998
- throughIndex
999
- };
1000
- });
1001
- },
1002
- async listSnapshots(sessionId) {
1003
- return wrap(async () => {
1004
- const start = `${prefix}:${sessionId}:snap:`;
1005
- let cursor = "0";
1006
- const keys = [];
1007
- do {
1008
- const reply = await call("SCAN", cursor, "MATCH", `${escapeGlob(start)}*`, "COUNT", 100);
1009
- cursor = reply[0];
1010
- keys.push(...reply[1].filter((key) => key.startsWith(start)));
1011
- } while (cursor !== "0");
1012
- return (await Promise.all(keys.map(async (key) => {
1013
- const raw = await call("GET", key);
1014
- if (raw === null) return null;
1015
- const parsed = JSON.parse(raw);
1016
- return {
1017
- reducerName: key.slice(start.length),
1018
- index: parsed.index,
1019
- updatedAt: new Date(parsed.updatedAt ?? 0)
1020
- };
1021
- }))).filter((snapshot) => snapshot !== null).toSorted((a, b) => a.reducerName.localeCompare(b.reducerName));
1022
- });
1023
- }
1024
- },
1025
939
  async readRange(sessionId, afterIndex, throughIndex) {
1026
940
  return wrap(() => readRange(sessionId, afterIndex, throughIndex));
1027
941
  },
@@ -1038,4 +952,4 @@ function createRedisStoreCore(options) {
1038
952
  //#endregion
1039
953
  export { createRedisStoreCore as t };
1040
954
 
1041
- //# sourceMappingURL=store-redis-core-BFLwz0Wj.js.map
955
+ //# sourceMappingURL=store-redis-core-DEYO8Ryv.js.map