experimental-a2 0.3.0 → 0.5.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 (205) hide show
  1. package/CHANGELOG.md +207 -0
  2. package/dist/ai-CFNeCrRl.d.ts +334 -0
  3. package/dist/ai-CFNeCrRl.d.ts.map +1 -0
  4. package/dist/ai-server.browser.js +2 -0
  5. package/dist/ai-server.browser.js.map +1 -0
  6. package/dist/ai-server.d.ts +49 -9
  7. package/dist/ai-server.d.ts.map +1 -0
  8. package/dist/ai-server.js +123 -49
  9. package/dist/ai-server.js.map +1 -0
  10. package/dist/ai.d.ts +2 -303
  11. package/dist/ai.js +233 -86
  12. package/dist/ai.js.map +1 -0
  13. package/dist/cli-B3VuxoDe.js +599 -0
  14. package/dist/cli-B3VuxoDe.js.map +1 -0
  15. package/dist/cli-bin.d.ts +1 -0
  16. package/dist/cli-bin.js +7 -0
  17. package/dist/cli-bin.js.map +1 -0
  18. package/dist/cli.d.ts +20 -0
  19. package/dist/cli.d.ts.map +1 -0
  20. package/dist/cli.js +2 -0
  21. package/dist/client-BKlyLiOU.js +1008 -0
  22. package/dist/client-BKlyLiOU.js.map +1 -0
  23. package/dist/client-D7mvIXrF.d.ts +191 -0
  24. package/dist/client-D7mvIXrF.d.ts.map +1 -0
  25. package/dist/client.d.ts +2 -90
  26. package/dist/client.js +1 -409
  27. package/dist/{contract-CG_adnu_.js → contract-48bUMgcL.js} +12 -2
  28. package/dist/contract-48bUMgcL.js.map +1 -0
  29. package/dist/{contract-C_3dIIEU.d.ts → contract-jIfaR085.d.ts} +63 -8
  30. package/dist/contract-jIfaR085.d.ts.map +1 -0
  31. package/dist/devtools-J_jZ2vQf.d.ts +152 -0
  32. package/dist/devtools-J_jZ2vQf.d.ts.map +1 -0
  33. package/dist/devtools-kJJaORn-.js +340 -0
  34. package/dist/devtools-kJJaORn-.js.map +1 -0
  35. package/dist/devtools-server.browser.js +3 -1
  36. package/dist/devtools-server.browser.js.map +1 -0
  37. package/dist/devtools-server.d.ts +4 -3
  38. package/dist/devtools-server.d.ts.map +1 -0
  39. package/dist/devtools-server.js +226 -43
  40. package/dist/devtools-server.js.map +1 -0
  41. package/dist/devtools.d.ts +2 -0
  42. package/dist/devtools.js +2 -0
  43. package/dist/{errors-BJRMd-h6.js → errors-BQuJpe82.js} +6 -4
  44. package/dist/errors-BQuJpe82.js.map +1 -0
  45. package/dist/{errors-xL_JTXsY.d.ts → errors-W6nwJ-fm.d.ts} +3 -2
  46. package/dist/errors-W6nwJ-fm.d.ts.map +1 -0
  47. package/dist/http.d.ts +135 -28
  48. package/dist/http.d.ts.map +1 -0
  49. package/dist/http.js +647 -61
  50. package/dist/http.js.map +1 -0
  51. package/dist/{idempotent-replay-BMyHrP0L.js → idempotent-replay-DuqEkYA7.js} +4 -2
  52. package/dist/idempotent-replay-DuqEkYA7.js.map +1 -0
  53. package/dist/index.d.ts +5 -5
  54. package/dist/index.js +2 -2
  55. package/dist/{inspection-E7qbD0Xj.js → inspection-DaxB5jM2.js} +4 -1
  56. package/dist/inspection-DaxB5jM2.js.map +1 -0
  57. package/dist/internal-DstsI6Re.js +211 -0
  58. package/dist/internal-DstsI6Re.js.map +1 -0
  59. package/dist/otel.d.ts +3 -2
  60. package/dist/otel.d.ts.map +1 -0
  61. package/dist/otel.js +2 -0
  62. package/dist/otel.js.map +1 -0
  63. package/dist/platform-B4TnJtWu.js +34 -0
  64. package/dist/platform-B4TnJtWu.js.map +1 -0
  65. package/dist/react.d.ts +47 -15
  66. package/dist/react.d.ts.map +1 -0
  67. package/dist/react.js +30 -15
  68. package/dist/react.js.map +1 -0
  69. package/dist/retryable-lazy-DZWmHpii.js +2 -0
  70. package/dist/retryable-lazy-DZWmHpii.js.map +1 -0
  71. package/dist/scheduler-qstash.d.ts +79 -0
  72. package/dist/scheduler-qstash.d.ts.map +1 -0
  73. package/dist/scheduler-qstash.js +501 -0
  74. package/dist/scheduler-qstash.js.map +1 -0
  75. package/dist/scheduler-task-BpzhPnRS.js +56 -0
  76. package/dist/scheduler-task-BpzhPnRS.js.map +1 -0
  77. package/dist/{recovery-vercel.d.ts → scheduler-vercel.d.ts} +18 -24
  78. package/dist/scheduler-vercel.d.ts.map +1 -0
  79. package/dist/scheduler-vercel.js +228 -0
  80. package/dist/scheduler-vercel.js.map +1 -0
  81. package/dist/server-C72KOw51.js +1457 -0
  82. package/dist/server-C72KOw51.js.map +1 -0
  83. package/dist/server-DUF9pjsx.d.ts +261 -0
  84. package/dist/server-DUF9pjsx.d.ts.map +1 -0
  85. package/dist/server.browser.js +2 -0
  86. package/dist/server.browser.js.map +1 -0
  87. package/dist/server.d.ts +4 -155
  88. package/dist/server.js +2 -2
  89. package/dist/{log-ldf5g8Cx.d.ts → store-DysUkTH3.d.ts} +120 -35
  90. package/dist/store-DysUkTH3.d.ts.map +1 -0
  91. package/dist/{log-yJbXUf72.js → store-N8PXxDAS.js} +3 -1
  92. package/dist/store-N8PXxDAS.js.map +1 -0
  93. package/dist/store-codec-DTG0Ftek.js +10 -0
  94. package/dist/store-codec-DTG0Ftek.js.map +1 -0
  95. package/dist/store-memory.d.ts +12 -0
  96. package/dist/store-memory.d.ts.map +1 -0
  97. package/dist/{log-memory.js → store-memory.js} +145 -34
  98. package/dist/store-memory.js.map +1 -0
  99. package/dist/{log-polling-DZ1MiKLg.js → store-polling-dSeLxzfb.js} +7 -5
  100. package/dist/store-polling-dSeLxzfb.js.map +1 -0
  101. package/dist/{log-postgres.d.ts → store-postgres.d.ts} +7 -6
  102. package/dist/store-postgres.d.ts.map +1 -0
  103. package/dist/{log-postgres.js → store-postgres.js} +214 -24
  104. package/dist/store-postgres.js.map +1 -0
  105. package/dist/{log-redis-core-CyJ5L8yR.js → store-redis-core-BFLwz0Wj.js} +271 -66
  106. package/dist/store-redis-core-BFLwz0Wj.js.map +1 -0
  107. package/dist/{log-redis-http.d.ts → store-redis-http.d.ts} +8 -7
  108. package/dist/store-redis-http.d.ts.map +1 -0
  109. package/dist/{log-redis-http.js → store-redis-http.js} +22 -12
  110. package/dist/store-redis-http.js.map +1 -0
  111. package/dist/{log-redis.d.ts → store-redis.d.ts} +7 -6
  112. package/dist/store-redis.d.ts.map +1 -0
  113. package/dist/{log-redis.js → store-redis.js} +101 -39
  114. package/dist/store-redis.js.map +1 -0
  115. package/dist/{log-sqlite.d.ts → store-sqlite.d.ts} +7 -6
  116. package/dist/store-sqlite.d.ts.map +1 -0
  117. package/dist/{log-sqlite.js → store-sqlite.js} +118 -22
  118. package/dist/store-sqlite.js.map +1 -0
  119. package/dist/{telemetry-Cso0qyHQ.d.ts → telemetry-BjYHTfh2.d.ts} +3 -2
  120. package/dist/telemetry-BjYHTfh2.d.ts.map +1 -0
  121. package/dist/testing.browser.d.ts +1 -0
  122. package/dist/testing.browser.js +6 -0
  123. package/dist/testing.browser.js.map +1 -0
  124. package/dist/testing.d.ts +32 -0
  125. package/dist/testing.d.ts.map +1 -0
  126. package/dist/testing.js +103 -0
  127. package/dist/testing.js.map +1 -0
  128. package/dist/validate-XKT4FSNn.js +2 -0
  129. package/dist/validate-XKT4FSNn.js.map +1 -0
  130. package/dist/wire-BFQmSJ-9.js +284 -0
  131. package/dist/wire-BFQmSJ-9.js.map +1 -0
  132. package/docs/01-quickstart.mdx +3 -3
  133. package/docs/concepts/01-contracts.mdx +18 -13
  134. package/docs/concepts/02-handlers.mdx +7 -7
  135. package/docs/concepts/03-durability.mdx +21 -21
  136. package/docs/concepts/04-state.mdx +18 -21
  137. package/docs/guides/01-timers.mdx +154 -49
  138. package/docs/guides/02-cancellation.mdx +30 -4
  139. package/docs/guides/03-react.mdx +76 -57
  140. package/docs/guides/04-local-first.mdx +1 -1
  141. package/docs/guides/05-production.mdx +310 -61
  142. package/docs/guides/06-ai-agents.mdx +252 -74
  143. package/docs/guides/07-devtools.mdx +137 -12
  144. package/docs/guides/08-application-data.mdx +1 -1
  145. package/docs/guides/09-presence.mdx +263 -0
  146. package/docs/guides/10-transports.mdx +140 -0
  147. package/docs/index.mdx +18 -14
  148. package/docs/reference/01-api.mdx +839 -110
  149. package/docs/reference/02-errors.mdx +13 -6
  150. package/package.json +24 -7
  151. package/src/ai-coordinator.ts +358 -0
  152. package/src/ai-projector.ts +524 -0
  153. package/src/ai-sdk-step.ts +261 -0
  154. package/src/ai-server.browser.ts +5 -0
  155. package/src/ai-server.ts +1719 -0
  156. package/src/ai.ts +2155 -0
  157. package/src/cache-indexeddb.ts +10 -0
  158. package/src/cli-bin.ts +5 -0
  159. package/src/cli.ts +1046 -0
  160. package/src/client.ts +1826 -0
  161. package/src/contract.ts +206 -0
  162. package/src/deterministic-id.ts +72 -0
  163. package/src/devtools-app.ts +989 -0
  164. package/src/devtools-server.browser.ts +5 -0
  165. package/src/devtools-server.ts +604 -0
  166. package/src/devtools.ts +716 -0
  167. package/src/errors.ts +50 -0
  168. package/src/http.ts +394 -0
  169. package/src/idempotent-replay.ts +53 -0
  170. package/src/index.ts +37 -0
  171. package/src/inspection.ts +39 -0
  172. package/src/internal.ts +426 -0
  173. package/src/otel.ts +59 -0
  174. package/src/platform.ts +60 -0
  175. package/src/push-envelope.ts +137 -0
  176. package/src/react.ts +284 -0
  177. package/src/reducer.ts +108 -0
  178. package/src/retryable-lazy.ts +27 -0
  179. package/src/scheduler-qstash.ts +915 -0
  180. package/src/scheduler-task.ts +106 -0
  181. package/src/scheduler-vercel.ts +437 -0
  182. package/src/server.browser.ts +12 -0
  183. package/src/server.ts +2700 -0
  184. package/src/session-socket.ts +548 -0
  185. package/src/sse.ts +141 -0
  186. package/src/standard-schema.ts +77 -0
  187. package/src/store-codec.ts +10 -0
  188. package/src/store-memory.ts +788 -0
  189. package/src/store-polling.ts +102 -0
  190. package/src/store-postgres.ts +1212 -0
  191. package/src/store-redis-core.ts +1494 -0
  192. package/src/store-redis-http.ts +116 -0
  193. package/src/store-redis.ts +458 -0
  194. package/src/store-sqlite.ts +1108 -0
  195. package/src/store.ts +385 -0
  196. package/src/telemetry.ts +47 -0
  197. package/src/testing.browser.ts +5 -0
  198. package/src/testing.ts +185 -0
  199. package/src/validate.ts +39 -0
  200. package/src/wire.ts +454 -0
  201. package/dist/internal-gCd5qMry.js +0 -44
  202. package/dist/log-memory.d.ts +0 -11
  203. package/dist/recovery-vercel.js +0 -119
  204. package/dist/server-BcLa4RFL.js +0 -877
  205. package/dist/wire-BVsgR8o9.js +0 -62
@@ -1,13 +1,14 @@
1
- import { t as A2Error } from "./errors-BJRMd-h6.js";
1
+ import { t as A2Error } from "./errors-BQuJpe82.js";
2
+ import { n as pollingStream } from "./store-polling-dSeLxzfb.js";
2
3
  import { t as retryableLazy } from "./retryable-lazy-DZWmHpii.js";
3
- import { t as idempotentReplay } from "./idempotent-replay-BMyHrP0L.js";
4
- import { n as SYSTEM_CLOCK, t as RANDOM_IDS } from "./log-yJbXUf72.js";
5
- import { n as pollingStream } from "./log-polling-DZ1MiKLg.js";
6
- //#region src/log-postgres.ts
4
+ import { t as idempotentReplay } from "./idempotent-replay-DuqEkYA7.js";
5
+ import { n as SYSTEM_CLOCK, t as RANDOM_IDS } from "./store-N8PXxDAS.js";
6
+ import { t as decodeReturnedEventIds } from "./store-codec-DTG0Ftek.js";
7
+ //#region src/store-postgres.ts
7
8
  /**
8
- * experimental-a2/log-postgres — postgres log backend (the production path).
9
+ * experimental-a2/store-postgres — postgres store backend (the production path).
9
10
  *
10
- * Implements the A2Log interface over any client that speaks the tiny
11
+ * Implements the A2Store interface over any client that speaks the tiny
11
12
  * `PostgresClient` shape below — a `pg` Pool matches it structurally
12
13
  * (and is what `connectionString` creates, lazily; `pg` is an optional
13
14
  * peer dependency). The conformance suite in test/conformance is the
@@ -69,6 +70,17 @@ create table if not exists a2_snapshots (
69
70
  updated_at bigint not null,
70
71
  primary key (session_id, reducer_name)
71
72
  );
73
+
74
+ create table if not exists a2_presence (
75
+ ns text not null,
76
+ participant text not null,
77
+ field text not null,
78
+ value jsonb not null,
79
+ seen bigint not null,
80
+ at bigint not null,
81
+ expires_at bigint not null,
82
+ primary key (ns, participant, field)
83
+ );
72
84
  `;
73
85
  const asNumber = (value) => Number(value);
74
86
  const toDate = (ms) => ms === null || ms === void 0 ? null : new Date(asNumber(ms));
@@ -94,7 +106,7 @@ const toStored = (row) => ({
94
106
  lane: row["lane"] ?? null,
95
107
  processedAt: toDate(row["processed_at"]),
96
108
  processedByAttempt: row["processed_by_attempt"] === null || row["processed_by_attempt"] === void 0 ? null : asNumber(row["processed_by_attempt"]),
97
- returnedEventIds: row["returned_event_ids"] === null || row["returned_event_ids"] === void 0 ? null : typeof row["returned_event_ids"] === "string" ? JSON.parse(row["returned_event_ids"]) : row["returned_event_ids"],
109
+ returnedEventIds: row["returned_event_ids"] === null || row["returned_event_ids"] === void 0 ? null : decodeReturnedEventIds(row["returned_event_ids"]),
98
110
  firstClaimedAt: toDate(row["first_claimed_at"]),
99
111
  lastClaimedAt: toDate(row["last_claimed_at"]),
100
112
  attemptCount: asNumber(row["attempt_count"]),
@@ -119,7 +131,7 @@ const wrap = async (fn) => {
119
131
  return await fn();
120
132
  } catch (err) {
121
133
  if (err instanceof A2Error || err instanceof TypeError) throw err;
122
- throw new A2Error("LOG_UNAVAILABLE", "postgres log operation failed", { cause: err });
134
+ throw new A2Error("STORE_UNAVAILABLE", "postgres store operation failed", { cause: err });
123
135
  }
124
136
  };
125
137
  const jsonb = (value) => JSON.stringify(value) ?? "null";
@@ -151,7 +163,7 @@ function postgres(options = {}) {
151
163
  let c = options.client;
152
164
  if (!c) {
153
165
  c = new (await (import("pg").catch(() => {
154
- throw new A2Error("LOG_NOT_CONFIGURED", "experimental-a2/log-postgres with a connectionString needs the 'pg' package (optional peer dependency) — install it, or inject a client");
166
+ throw new A2Error("STORE_NOT_CONFIGURED", "experimental-a2/store-postgres with a connectionString needs the 'pg' package (optional peer dependency) — install it, or inject a client");
155
167
  }))).default.Pool({ connectionString: options.connectionString });
156
168
  owned = c;
157
169
  }
@@ -230,6 +242,19 @@ function postgres(options = {}) {
230
242
  throw new A2Error("PARTIAL_DUPLICATE_BATCH", `batch mixes ${existing.length} already-appended and ${events.length - existing.length} fresh events`);
231
243
  }
232
244
  }
245
+ const causeIndexes = [...new Set(events.filter((e) => e.cause !== void 0).map((e) => e.cause.index))];
246
+ if (causeIndexes.length > 0) {
247
+ const { rows: parents } = await query(`select idx, attempt_count, failed_at from a2_events
248
+ where session_id = $1 and idx = any($2::bigint[])
249
+ for update`, [sessionId, causeIndexes]);
250
+ const byIndex = new Map(parents.map((row) => [asNumber(row["idx"]), row]));
251
+ for (const e of events) {
252
+ if (!e.cause) continue;
253
+ const parent = byIndex.get(e.cause.index);
254
+ if (!parent) throw new TypeError(`no event at index ${e.cause.index} in session '${sessionId}'`);
255
+ if (asNumber(parent["attempt_count"]) !== e.cause.attempt || parent["failed_at"] !== null) throw new A2Error("SUPERSEDED_ATTEMPT", `attempt ${e.cause.attempt} no longer owns event ${e.cause.index} in session '${sessionId}'`);
256
+ }
257
+ }
233
258
  const { rows: maxRows } = await query(`select coalesce(max(idx), 0) as max,
234
259
  exists(
235
260
  select 1 from a2_events pending
@@ -305,6 +330,10 @@ function postgres(options = {}) {
305
330
  params.push(opts.afterIndex);
306
331
  conditions.push(`idx > $${params.length}`);
307
332
  }
333
+ if (opts?.throughIndex !== void 0) {
334
+ params.push(opts.throughIndex);
335
+ conditions.push(`idx <= $${params.length}`);
336
+ }
308
337
  const { rows } = await c.query(`select * from a2_events where ${conditions.join(" and ")} order by idx`, params);
309
338
  return rows.map(toStored);
310
339
  });
@@ -356,27 +385,56 @@ function postgres(options = {}) {
356
385
  };
357
386
  }));
358
387
  },
359
- async renewClaims({ sessionId, holder, indexes, ttlMs, expiresAtMs }) {
360
- if (indexes.length === 0) return [];
388
+ async renewClaims({ sessionId, holder, claims, ttlMs, expiresAtMs }) {
389
+ if (claims.length === 0) return {
390
+ renewed: [],
391
+ superseded: []
392
+ };
361
393
  return wrap(async () => {
362
394
  const c = await client();
363
395
  const now = clock.now().getTime();
364
396
  const expiresAt = expiresAtMs ?? now + ttlMs;
365
- const { rows } = await c.query(`update a2_events set claim_expires_at = $1
366
- where session_id = $2
367
- and idx = any($3::bigint[])
368
- and processed_at is null
369
- and failed_at is null
370
- and claim_holder = $4
371
- and claim_expires_at > $5
372
- returning idx`, [
397
+ const indexes = claims.map((claim) => claim.index);
398
+ const attempts = new Map(claims.map((claim) => [claim.index, claim.attempt]));
399
+ const { rows } = await c.query(`with listed as (
400
+ select idx, attempt
401
+ from unnest($3::bigint[], $6::int[]) as listed(idx, attempt)
402
+ ),
403
+ renewed as (
404
+ update a2_events event set claim_expires_at = $1
405
+ from listed
406
+ where event.session_id = $2
407
+ and event.idx = listed.idx
408
+ and event.attempt_count = listed.attempt
409
+ and event.processed_at is null
410
+ and event.failed_at is null
411
+ and event.claim_holder = $4
412
+ and event.claim_expires_at > $5
413
+ returning event.idx
414
+ )
415
+ select event.idx, event.attempt_count,
416
+ exists(select 1 from renewed where renewed.idx = event.idx)
417
+ as renewed
418
+ from a2_events event
419
+ where event.session_id = $2 and event.idx = any($3::bigint[])`, [
373
420
  expiresAt,
374
421
  sessionId,
375
422
  indexes,
376
423
  holder,
377
- now
424
+ now,
425
+ claims.map((claim) => claim.attempt)
378
426
  ]);
379
- return rows.map((row) => asNumber(row["idx"])).toSorted((a, b) => a - b);
427
+ const renewed = [];
428
+ const superseded = [];
429
+ for (const row of rows) {
430
+ const index = asNumber(row["idx"]);
431
+ if (row["renewed"] === true) renewed.push(index);
432
+ else if (asNumber(row["attempt_count"]) > (attempts.get(index) ?? 0)) superseded.push(index);
433
+ }
434
+ return {
435
+ renewed: renewed.toSorted((a, b) => a - b),
436
+ superseded: superseded.toSorted((a, b) => a - b)
437
+ };
380
438
  });
381
439
  },
382
440
  async completeAttempt({ sessionId, index, attempt, events }) {
@@ -390,7 +448,7 @@ function postgres(options = {}) {
390
448
  if (new Set(ids).size !== ids.length) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "returned event batch contains the same event id more than once");
391
449
  if (parent["processed_at"] !== null) {
392
450
  if (asNumber(parent["processed_by_attempt"]) !== attempt) return { outcome: "superseded" };
393
- const returnedIds = parent["returned_event_ids"] === null || parent["returned_event_ids"] === void 0 ? null : typeof parent["returned_event_ids"] === "string" ? JSON.parse(parent["returned_event_ids"]) : parent["returned_event_ids"];
451
+ const returnedIds = parent["returned_event_ids"] === null || parent["returned_event_ids"] === void 0 ? null : decodeReturnedEventIds(parent["returned_event_ids"]);
394
452
  if (returnedIds === null || returnedIds.length !== ids.length || returnedIds.some((id, offset) => id !== ids[offset])) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "completed attempt does not match the returned event batch");
395
453
  if (events.length === 0) return {
396
454
  outcome: "completed",
@@ -575,6 +633,60 @@ function postgres(options = {}) {
575
633
  };
576
634
  });
577
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
+ },
578
690
  async putSnapshot(sessionId, reducerName, index, state) {
579
691
  await wrap(async () => {
580
692
  await (await client()).query(`insert into a2_snapshots (session_id, reducer_name, up_to_index, state, updated_at)
@@ -592,6 +704,63 @@ function postgres(options = {}) {
592
704
  ]);
593
705
  });
594
706
  },
707
+ presence: {
708
+ async set(ns, participant, values, meta) {
709
+ const entries = Object.entries(values);
710
+ if (entries.length === 0) return;
711
+ await wrap(async () => {
712
+ const c = await client();
713
+ const atMs = meta.at.getTime();
714
+ const expiresAtMs = clock.now().getTime() + meta.ttlMs;
715
+ for (const [field, value] of entries) await (value === null ? c.query(`delete from a2_presence
716
+ where ns = $1 and participant = $2 and field = $3
717
+ and at <= $4`, [
718
+ ns,
719
+ participant,
720
+ field,
721
+ atMs
722
+ ]) : c.query(`insert into a2_presence
723
+ (ns, participant, field, value, seen, at, expires_at)
724
+ values ($1, $2, $3, $4::jsonb, $5, $6, $7)
725
+ on conflict (ns, participant, field) do update set
726
+ value = excluded.value,
727
+ seen = excluded.seen,
728
+ at = excluded.at,
729
+ expires_at = excluded.expires_at
730
+ where a2_presence.at <= excluded.at`, [
731
+ ns,
732
+ participant,
733
+ field,
734
+ jsonb(value),
735
+ meta.seen,
736
+ atMs,
737
+ expiresAtMs
738
+ ]));
739
+ });
740
+ },
741
+ async read(ns) {
742
+ return wrap(async () => {
743
+ const c = await client();
744
+ const now = clock.now().getTime();
745
+ const { rows } = await c.query(`with swept as (
746
+ delete from a2_presence
747
+ where ns = $1 and expires_at <= $2
748
+ )
749
+ select participant, field, value, seen, at, expires_at
750
+ from a2_presence
751
+ where ns = $1 and expires_at > $2
752
+ order by participant, field`, [ns, now]);
753
+ return rows.map((row) => ({
754
+ participant: row["participant"],
755
+ field: row["field"],
756
+ value: row["value"],
757
+ seen: asNumber(row["seen"]),
758
+ at: new Date(asNumber(row["at"])),
759
+ expiresAt: new Date(asNumber(row["expires_at"]))
760
+ }));
761
+ });
762
+ }
763
+ },
595
764
  inspect: {
596
765
  async listSessions(inspectionOptions) {
597
766
  return wrap(async () => {
@@ -641,6 +810,25 @@ function postgres(options = {}) {
641
810
  };
642
811
  });
643
812
  },
813
+ async readEvents(sessionId, pageOptions) {
814
+ return wrap(async () => {
815
+ const c = await client();
816
+ const currentFrontier = asNumber((await c.query("select coalesce(max(idx), 0) as frontier from a2_events where session_id = $1", [sessionId])).rows[0]?.["frontier"] ?? 0);
817
+ const throughIndex = Math.min(pageOptions.throughIndex ?? currentFrontier, currentFrontier);
818
+ const { rows } = await c.query(`select * from a2_events
819
+ where session_id = $1 and idx > $2 and idx <= $3
820
+ order by idx limit $4`, [
821
+ sessionId,
822
+ pageOptions.afterIndex,
823
+ throughIndex,
824
+ pageOptions.limit
825
+ ]);
826
+ return {
827
+ events: rows.map(toStored),
828
+ throughIndex
829
+ };
830
+ });
831
+ },
644
832
  async listSnapshots(sessionId) {
645
833
  return wrap(async () => {
646
834
  const { rows } = await (await client()).query(`select reducer_name, up_to_index, updated_at
@@ -658,7 +846,7 @@ function postgres(options = {}) {
658
846
  const { rows } = await (await client()).query("select * from a2_events where session_id = $1 and idx > $2 order by idx", [sessionId, afterIndex]);
659
847
  return rows.map(toStored);
660
848
  });
661
- return pollingStream(readAfter, opts?.startAt !== void 0 ? { startAt: opts.startAt } : {});
849
+ return pollingStream(readAfter, opts?.startAfter !== void 0 ? { startAfter: opts.startAfter } : {});
662
850
  },
663
851
  async close() {
664
852
  const current = connection.peek();
@@ -669,3 +857,5 @@ function postgres(options = {}) {
669
857
  }
670
858
  //#endregion
671
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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"}