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,16 +1,17 @@
1
- import { t as A2Error } from "./errors-BJRMd-h6.js";
2
- import { t as idempotentReplay } from "./idempotent-replay-BMyHrP0L.js";
3
- import { n as SYSTEM_CLOCK, t as RANDOM_IDS } from "./log-yJbXUf72.js";
4
- import { n as pollingStream } from "./log-polling-DZ1MiKLg.js";
1
+ import { t as A2Error } from "./errors-BQuJpe82.js";
2
+ import { n as pollingStream } from "./store-polling-dSeLxzfb.js";
3
+ import { t as idempotentReplay } from "./idempotent-replay-DuqEkYA7.js";
4
+ import { n as SYSTEM_CLOCK, t as RANDOM_IDS } from "./store-N8PXxDAS.js";
5
+ import { t as decodeReturnedEventIds } from "./store-codec-DTG0Ftek.js";
5
6
  import { mkdirSync } from "node:fs";
6
7
  import { dirname, resolve } from "node:path";
7
8
  import { DatabaseSync } from "node:sqlite";
8
- //#region src/log-sqlite.ts
9
+ //#region src/store-sqlite.ts
9
10
  /**
10
- * experimental-a2/log-sqlite — sqlite log backend (the dev default, `.a2/dev.db`).
11
+ * experimental-a2/store-sqlite — sqlite store backend (the dev default, `.a2/dev.db`).
11
12
  *
12
13
  * Built on `node:sqlite` (Node ≥ 22.13) so it ships with zero
13
- * dependencies. Implements the A2Log interface; the conformance suite
14
+ * dependencies. Implements the A2Store interface; the conformance suite
14
15
  * in test/conformance is the executable contract.
15
16
  *
16
17
  * The spec's `index` column is stored as `idx` (a2-implementation.md
@@ -66,6 +67,17 @@ create table if not exists a2_snapshots (
66
67
  updated_at integer not null,
67
68
  primary key (session_id, reducer_name)
68
69
  ) strict;
70
+
71
+ create table if not exists a2_presence (
72
+ ns text not null,
73
+ participant text not null,
74
+ field text not null,
75
+ value text not null,
76
+ seen integer not null,
77
+ at integer not null,
78
+ expires_at integer not null,
79
+ primary key (ns, participant, field)
80
+ ) strict;
69
81
  `;
70
82
  /**
71
83
  * Switching journal modes takes an exclusive lock, and — unlike normal
@@ -88,7 +100,7 @@ const wrap = (fn) => {
88
100
  return fn();
89
101
  } catch (err) {
90
102
  if (err instanceof A2Error || err instanceof TypeError) throw err;
91
- throw new A2Error("LOG_UNAVAILABLE", "sqlite log operation failed", { cause: err });
103
+ throw new A2Error("STORE_UNAVAILABLE", "sqlite store operation failed", { cause: err });
92
104
  }
93
105
  };
94
106
  const toDate = (ms) => ms === null ? null : new Date(Number(ms));
@@ -114,7 +126,7 @@ const toStored = (row) => ({
114
126
  lane: row.lane,
115
127
  processedAt: toDate(row.processed_at),
116
128
  processedByAttempt: row.processed_by_attempt === null ? null : Number(row.processed_by_attempt),
117
- returnedEventIds: row.returned_event_ids === null ? null : JSON.parse(row.returned_event_ids),
129
+ returnedEventIds: row.returned_event_ids === null ? null : decodeReturnedEventIds(row.returned_event_ids),
118
130
  firstClaimedAt: toDate(row.first_claimed_at),
119
131
  lastClaimedAt: toDate(row.last_claimed_at),
120
132
  attemptCount: Number(row.attempt_count),
@@ -175,6 +187,21 @@ function sqlite(options = {}) {
175
187
  select 1 from a2_events
176
188
  where session_id = ? and processed_at is null
177
189
  ) as has_pending`);
190
+ const upsertPresence = db.prepare(`insert into a2_presence (ns, participant, field, value, seen, at, expires_at)
191
+ values (?, ?, ?, ?, ?, ?, ?)
192
+ on conflict (ns, participant, field) do update set
193
+ value = excluded.value,
194
+ seen = excluded.seen,
195
+ at = excluded.at,
196
+ expires_at = excluded.expires_at
197
+ where a2_presence.at <= excluded.at`);
198
+ const deletePresence = db.prepare(`delete from a2_presence
199
+ where ns = ? and participant = ? and field = ? and at <= ?`);
200
+ const sweepPresence = db.prepare("delete from a2_presence where ns = ? and expires_at <= ?");
201
+ const selectPresence = db.prepare(`select participant, field, value, seen, at, expires_at
202
+ from a2_presence
203
+ where ns = ? and expires_at > ?
204
+ order by participant, field`);
178
205
  const selectByIds = (count) => db.prepare(`select stored.*,
179
206
  exists(
180
207
  select 1 from a2_events pending
@@ -221,6 +248,12 @@ function sqlite(options = {}) {
221
248
  throw new A2Error("PARTIAL_DUPLICATE_BATCH", `batch mixes ${existing.length} already-appended and ${events.length - existing.length} fresh events`);
222
249
  }
223
250
  }
251
+ for (const e of events) {
252
+ if (!e.cause) continue;
253
+ const parent = db.prepare("select attempt_count, failed_at from a2_events where session_id = ? and idx = ?").get(sessionId, e.cause.index);
254
+ if (!parent) throw new TypeError(`no event at index ${e.cause.index} in session '${sessionId}'`);
255
+ if (Number(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
+ }
224
257
  const status = appendStatus.get(sessionId, sessionId);
225
258
  const base = Number(status.max);
226
259
  const now = clock.now().getTime();
@@ -265,6 +298,10 @@ function sqlite(options = {}) {
265
298
  conditions.push("idx > ?");
266
299
  params.push(opts.afterIndex);
267
300
  }
301
+ if (opts?.throughIndex !== void 0) {
302
+ conditions.push("idx <= ?");
303
+ params.push(opts.throughIndex);
304
+ }
268
305
  return db.prepare(`select * from a2_events where ${conditions.join(" and ")} order by idx`).all(...params).map(toStored);
269
306
  });
270
307
  },
@@ -312,20 +349,39 @@ function sqlite(options = {}) {
312
349
  };
313
350
  }));
314
351
  },
315
- async renewClaims({ sessionId, holder, indexes, ttlMs, expiresAtMs }) {
316
- if (indexes.length === 0) return [];
352
+ async renewClaims({ sessionId, holder, claims, ttlMs, expiresAtMs }) {
353
+ if (claims.length === 0) return {
354
+ renewed: [],
355
+ superseded: []
356
+ };
317
357
  return wrap(() => tx(() => {
318
358
  const now = clock.now().getTime();
319
359
  const expiresAt = expiresAtMs ?? now + ttlMs;
320
- const placeholders = indexes.map(() => "?").join(", ");
321
- return db.prepare(`update a2_events set claim_expires_at = ?
322
- where session_id = ?
323
- and idx in (${placeholders})
324
- and processed_at is null
325
- and failed_at is null
326
- and claim_holder = ?
327
- and claim_expires_at > ?
328
- returning idx`).all(expiresAt, sessionId, ...indexes, holder, now).map((row) => Number(row.idx)).toSorted((a, b) => a - b);
360
+ const renewed = [];
361
+ const superseded = [];
362
+ const currentAttempt = db.prepare("select attempt_count from a2_events where session_id = ? and idx = ?");
363
+ const extend = db.prepare(`update a2_events set claim_expires_at = ?
364
+ where session_id = ?
365
+ and idx = ?
366
+ and attempt_count = ?
367
+ and processed_at is null
368
+ and failed_at is null
369
+ and claim_holder = ?
370
+ and claim_expires_at > ?`);
371
+ for (const claim of claims) {
372
+ const row = currentAttempt.get(sessionId, claim.index);
373
+ if (!row) continue;
374
+ if (Number(row.attempt_count) > claim.attempt) {
375
+ superseded.push(claim.index);
376
+ continue;
377
+ }
378
+ const updated = extend.run(expiresAt, sessionId, claim.index, claim.attempt, holder, now);
379
+ if (Number(updated.changes) === 1) renewed.push(claim.index);
380
+ }
381
+ return {
382
+ renewed: renewed.toSorted((a, b) => a - b),
383
+ superseded: superseded.toSorted((a, b) => a - b)
384
+ };
329
385
  }));
330
386
  },
331
387
  async completeAttempt({ sessionId, index, attempt, events }) {
@@ -336,7 +392,7 @@ function sqlite(options = {}) {
336
392
  if (new Set(ids).size !== ids.length) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "returned event batch contains the same event id more than once");
337
393
  if (parent.processed_at !== null) {
338
394
  if (Number(parent.processed_by_attempt) !== attempt) return { outcome: "superseded" };
339
- const returnedIds = parent.returned_event_ids === null ? null : JSON.parse(parent.returned_event_ids);
395
+ const returnedIds = parent.returned_event_ids === null ? null : decodeReturnedEventIds(parent.returned_event_ids);
340
396
  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");
341
397
  const existing = ids.length === 0 ? [] : selectByIds(ids.length).all(sessionId, ...ids);
342
398
  const byId = new Map(existing.map((row) => [row.event_id, row]));
@@ -465,6 +521,32 @@ function sqlite(options = {}) {
465
521
  where excluded.up_to_index > a2_snapshots.up_to_index`).run(sessionId, reducerName, index, JSON.stringify(state) ?? "null", clock.now().getTime());
466
522
  });
467
523
  },
524
+ presence: {
525
+ async set(ns, participant, values, meta) {
526
+ const entries = Object.entries(values);
527
+ if (entries.length === 0) return;
528
+ wrap(() => tx(() => {
529
+ const atMs = meta.at.getTime();
530
+ const expiresAtMs = clock.now().getTime() + meta.ttlMs;
531
+ for (const [field, value] of entries) if (value === null) deletePresence.run(ns, participant, field, atMs);
532
+ else upsertPresence.run(ns, participant, field, JSON.stringify(value) ?? "null", meta.seen, atMs, expiresAtMs);
533
+ }));
534
+ },
535
+ async read(ns) {
536
+ return wrap(() => {
537
+ const now = clock.now().getTime();
538
+ sweepPresence.run(ns, now);
539
+ return selectPresence.all(ns, now).map((row) => ({
540
+ participant: row.participant,
541
+ field: row.field,
542
+ value: JSON.parse(row.value),
543
+ seen: Number(row.seen),
544
+ at: new Date(Number(row.at)),
545
+ expiresAt: new Date(Number(row.expires_at))
546
+ }));
547
+ });
548
+ }
549
+ },
468
550
  inspect: {
469
551
  async listSessions(inspectionOptions) {
470
552
  return wrap(() => {
@@ -513,6 +595,18 @@ function sqlite(options = {}) {
513
595
  };
514
596
  });
515
597
  },
598
+ async readEvents(sessionId, pageOptions) {
599
+ return wrap(() => {
600
+ const currentFrontier = Number(db.prepare("select coalesce(max(idx), 0) as frontier from a2_events where session_id = ?").get(sessionId).frontier);
601
+ const throughIndex = Math.min(pageOptions.throughIndex ?? currentFrontier, currentFrontier);
602
+ return {
603
+ events: db.prepare(`select * from a2_events
604
+ where session_id = ? and idx > ? and idx <= ?
605
+ order by idx limit ?`).all(sessionId, pageOptions.afterIndex, throughIndex, pageOptions.limit).map(toStored),
606
+ throughIndex
607
+ };
608
+ });
609
+ },
516
610
  async listSnapshots(sessionId) {
517
611
  return wrap(() => {
518
612
  return db.prepare(`select reducer_name, up_to_index, updated_at
@@ -528,7 +622,7 @@ function sqlite(options = {}) {
528
622
  const readAfter = (afterIndex) => wrap(() => {
529
623
  return db.prepare("select * from a2_events where session_id = ? and idx > ? order by idx").all(sessionId, afterIndex).map(toStored);
530
624
  });
531
- return pollingStream(readAfter, opts?.startAt !== void 0 ? { startAt: opts.startAt } : {});
625
+ return pollingStream(readAfter, opts?.startAfter !== void 0 ? { startAfter: opts.startAfter } : {});
532
626
  },
533
627
  close() {
534
628
  db.close();
@@ -537,3 +631,5 @@ function sqlite(options = {}) {
537
631
  }
538
632
  //#endregion
539
633
  export { sqlite };
634
+
635
+ //# sourceMappingURL=store-sqlite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store-sqlite.js","names":[],"sources":["../src/store-sqlite.ts"],"sourcesContent":["/**\n * experimental-a2/store-sqlite — sqlite store backend (the dev default, `.a2/dev.db`).\n *\n * Built on `node:sqlite` (Node ≥ 22.13) so it ships with zero\n * dependencies. Implements the A2Store interface; the conformance suite\n * in test/conformance is the executable contract.\n *\n * The spec's `index` column is stored as `idx` (a2-implementation.md\n * §11 — `index` collides with a reserved word) and mapped back at the\n * API boundary. Timestamps are epoch milliseconds written from the\n * injected clock — never SQL `now()` — so tests can time-travel against\n * real storage.\n */\n\nimport { mkdirSync } from 'node:fs'\nimport { dirname, resolve } from 'node:path'\nimport { DatabaseSync } from 'node:sqlite'\nimport { A2Error } from './errors.ts'\nimport { idempotentReplay } from './idempotent-replay.ts'\nimport { decodeReturnedEventIds } from './store-codec.ts'\nimport { pollingStream } from './store-polling.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} from './store.ts'\n\nexport type SqliteStoreOptions = {\n /** Database file path. Defaults to `.a2/dev.db`; `:memory:` works. */\n path?: string\n /** Injectable clock — every stored timestamp comes from here. */\n clock?: Clock\n /** Injectable id source for generated event ids. */\n ids?: IdSource\n}\n\nexport type SqliteStore = A2Store & {\n /** Close the underlying database handle. */\n close(): void\n}\n\nconst SCHEMA = `\ncreate table if not exists a2_events (\n session_id text not null,\n idx integer not null,\n event_type text not null,\n payload text not null,\n event_id text not null,\n created_at integer not null,\n cause text,\n lane text,\n lane_ready integer not null default 0,\n processed_at integer,\n processed_by_attempt integer,\n returned_event_ids text,\n first_claimed_at integer,\n last_claimed_at integer,\n attempt_count integer not null default 0,\n failure_count integer not null default 0,\n last_failed_at integer,\n last_failed_attempt integer,\n last_error text,\n failed_at integer,\n claim_holder text,\n claim_expires_at integer,\n primary key (session_id, idx)\n) strict;\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 = 1 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 = 1 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 integer not null,\n state text not null,\n updated_at integer not null,\n primary key (session_id, reducer_name)\n) strict;\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 text not null,\n seen integer not null,\n at integer not null,\n expires_at integer not null,\n primary key (ns, participant, field)\n) strict;\n`\n\n/**\n * Switching journal modes takes an exclusive lock, and — unlike normal\n * statements — the switch can return SQLITE_BUSY without consulting the\n * busy handler while several connections race it (instances cold-booting\n * against one file). Bounded synchronous retry; the window is boot-only\n * and tiny. Found by the multi-process torture test.\n */\nconst setWalJournalMode = (db: DatabaseSync): void => {\n for (let attempt = 0; ; attempt += 1) {\n try {\n db.exec('pragma journal_mode = wal')\n return\n } catch (err) {\n const busy = (err as { errcode?: number }).errcode === 5\n if (!busy || attempt >= 100) throw err\n // Synchronous 5ms sleep — sqlite() is a sync constructor.\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5)\n }\n }\n}\n\nconst wrap = <T>(fn: () => T): T => {\n try {\n return fn()\n } catch (err) {\n if (err instanceof A2Error || err instanceof TypeError) throw err\n throw new A2Error('STORE_UNAVAILABLE', 'sqlite store operation failed', {\n cause: err,\n })\n }\n}\n\nconst toDate = (ms: number | bigint | null): Date | null =>\n ms === null ? null : new Date(Number(ms))\n\nconst toCause = (json: string | null): EventCause | null => {\n if (json === null) return null\n const value = JSON.parse(json) 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: EventRow): StoredEvent => ({\n id: row.event_id,\n type: row.event_type,\n payload: JSON.parse(row.payload),\n index: Number(row.idx),\n sessionId: row.session_id,\n createdAt: new Date(Number(row.created_at)),\n cause: toCause(row.cause),\n lane: row.lane,\n processedAt: toDate(row.processed_at),\n processedByAttempt:\n row.processed_by_attempt === null ? null : Number(row.processed_by_attempt),\n returnedEventIds:\n row.returned_event_ids === null\n ? null\n : decodeReturnedEventIds(row.returned_event_ids),\n firstClaimedAt: toDate(row.first_claimed_at),\n lastClaimedAt: toDate(row.last_claimed_at),\n attemptCount: Number(row.attempt_count),\n failureCount: Number(row.failure_count),\n lastFailedAt: toDate(row.last_failed_at),\n lastFailedAttempt:\n row.last_failed_attempt === null ? null : Number(row.last_failed_attempt),\n lastError: row.last_error,\n failedAt: toDate(row.failed_at),\n claimHolder: row.claim_holder,\n claimExpiresAt: toDate(row.claim_expires_at),\n})\n\nconst toEvent = (row: EventRow): Event => ({\n id: row.event_id,\n type: row.event_type,\n payload: JSON.parse(row.payload),\n index: Number(row.idx),\n sessionId: row.session_id,\n createdAt: new Date(Number(row.created_at)),\n})\n\ntype EventRow = {\n session_id: string\n idx: number | bigint\n event_type: string\n payload: string\n event_id: string\n created_at: number | bigint\n cause: string | null\n lane: string | null\n processed_at: number | bigint | null\n processed_by_attempt: number | bigint | null\n returned_event_ids: string | null\n first_claimed_at: number | bigint | null\n last_claimed_at: number | bigint | null\n attempt_count: number | bigint\n failure_count: number | bigint\n last_failed_at: number | bigint | null\n last_failed_attempt: number | bigint | null\n last_error: string | null\n failed_at: number | bigint | null\n claim_holder: string | null\n claim_expires_at: number | bigint | null\n}\n\ntype SessionSummaryRow = {\n session_id: string\n event_count: number | bigint\n pending_count: number | bigint\n failed_count: number | bigint\n attempt_count: number | bigint\n failure_count: number | bigint\n first_event_at: number | bigint\n updated_at: number | bigint\n}\n\ntype SnapshotRow = {\n reducer_name: string\n up_to_index: number | bigint\n updated_at: number | bigint\n}\n\ntype PresenceDbRow = {\n participant: string\n field: string\n value: string\n seen: number | bigint\n at: number | bigint\n expires_at: number | bigint\n}\n\ntype StateReadRow = Partial<EventRow> & {\n row_kind: 'snapshot' | 'event'\n snapshot_index: number | bigint | null\n snapshot_state: string | null\n}\n\ntype AppendStatusRow = {\n max: number | bigint\n has_pending: number | bigint\n}\n\ntype ExistingEventRow = EventRow & {\n session_has_pending: number | bigint\n}\n\nexport function sqlite(options: SqliteStoreOptions = {}): SqliteStore {\n const path = options.path ?? '.a2/dev.db'\n const clock = options.clock ?? SYSTEM_CLOCK\n const generateId = options.ids ?? RANDOM_IDS\n\n if (path !== ':memory:') {\n // The dev-default backend only ever runs outside production, but\n // bundlers can't know that: without the ignore, Turbopack sees a\n // dynamic fs path and traces the entire project into the output.\n mkdirSync(dirname(resolve(/* turbopackIgnore: true */ path)), {\n recursive: true,\n })\n }\n const db = new DatabaseSync(path)\n // busy_timeout before anything that takes locks: concurrent\n // cold-opens (several instances booting at once) race the WAL switch\n // and schema creation.\n db.exec('pragma busy_timeout = 5000')\n setWalJournalMode(db)\n db.exec('pragma synchronous = normal')\n db.exec(SCHEMA)\n\n const insertEvent = db.prepare(\n `insert into a2_events\n (session_id, idx, event_type, payload, event_id, created_at,\n cause, lane, lane_ready, processed_at)\n select ?, ?, ?, ?, ?, ?, ?, ?,\n case\n when ? is null then 1\n when not exists (\n select 1 from a2_events\n where session_id = ? and lane = ? and processed_at is null\n ) then 1\n else 0\n end,\n ?`,\n )\n const promoteLaneHead = db.prepare(\n `update a2_events set lane_ready = 1\n where session_id = ?\n and idx = (\n select idx from a2_events\n where session_id = ? and lane = ? and processed_at is null\n order by idx limit 1\n )`,\n )\n const appendStatus = db.prepare(\n `select coalesce(\n (select max(idx) from a2_events where session_id = ?),\n 0\n ) as max,\n exists(\n select 1 from a2_events\n where session_id = ? and processed_at is null\n ) as has_pending`,\n )\n const upsertPresence = db.prepare(\n `insert into a2_presence (ns, participant, field, value, seen, at, expires_at)\n values (?, ?, ?, ?, ?, ?, ?)\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 const deletePresence = db.prepare(\n `delete from a2_presence\n where ns = ? and participant = ? and field = ? and at <= ?`,\n )\n const sweepPresence = db.prepare(\n 'delete from a2_presence where ns = ? and expires_at <= ?',\n )\n const selectPresence = db.prepare(\n `select participant, field, value, seen, at, expires_at\n from a2_presence\n where ns = ? and expires_at > ?\n order by participant, field`,\n )\n const selectByIds = (count: number) =>\n db.prepare(\n `select stored.*,\n exists(\n select 1 from a2_events pending\n where pending.session_id = ?\n and pending.processed_at is null\n ) as session_has_pending\n from a2_events stored\n where stored.event_id in (${Array.from({ length: count }, () => '?').join(', ')})\n order by stored.idx`,\n )\n\n const tx = <T>(fn: () => T): T => {\n db.exec('begin immediate')\n try {\n const result = fn()\n db.exec('commit')\n return result\n } catch (err) {\n try {\n db.exec('rollback')\n } catch {\n // Rollback can fail if the transaction never started; the\n // original error is what matters.\n }\n throw err\n }\n }\n\n return {\n async append(sessionId, events) {\n if (events.length === 0) {\n return wrap(() => {\n const status = appendStatus.get(\n sessionId,\n sessionId,\n ) as AppendStatusRow\n return {\n events: [],\n hasPending: Number(status.has_pending) === 1,\n }\n })\n }\n return wrap(() =>\n tx(() => {\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 existing = selectByIds(suppliedIds.length).all(\n sessionId,\n ...suppliedIds,\n ) as unknown as ExistingEventRow[]\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 '${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: Number(existing[0]!.session_has_pending) === 1,\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`). `begin immediate` serializes this read\n // against a concurrent claim.\n for (const e of events) {\n if (!e.cause) continue\n const parent = db\n .prepare(\n 'select attempt_count, failed_at from a2_events where session_id = ? and idx = ?',\n )\n .get(sessionId, e.cause.index) as\n { attempt_count: number | bigint; failed_at: unknown } | undefined\n if (!parent) {\n throw new TypeError(\n `no event at index ${e.cause.index} in session '${sessionId}'`,\n )\n }\n if (\n Number(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 const status = appendStatus.get(\n sessionId,\n sessionId,\n ) as AppendStatusRow\n const base = Number(status.max)\n const now = clock.now().getTime()\n const inserted: StoredEvent[] = events.map((e, i) => {\n const id = e.id ?? generateId()\n const index = base + 1 + i\n insertEvent.run(\n sessionId,\n index,\n e.type,\n JSON.stringify(e.payload) ?? 'null',\n id,\n now,\n e.cause ? JSON.stringify(e.cause) : null,\n e.lane ?? null,\n e.lane ?? null,\n sessionId,\n e.lane ?? null,\n e.settled ? now : null,\n )\n return {\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 Number(status.has_pending) === 1 ||\n events.some((event) => event.settled !== true),\n }\n }),\n )\n },\n\n async read(sessionId, opts) {\n return wrap(() => {\n const conditions = ['session_id = ?']\n const params: (string | number)[] = [sessionId]\n if (opts?.afterIndex !== undefined) {\n conditions.push('idx > ?')\n params.push(opts.afterIndex)\n }\n if (opts?.throughIndex !== undefined) {\n conditions.push('idx <= ?')\n params.push(opts.throughIndex)\n }\n const rows = db\n .prepare(\n `select * from a2_events where ${conditions.join(' and ')} order by idx`,\n )\n .all(...params) as unknown as EventRow[]\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 tx(() => {\n const now = clock.now().getTime()\n const expiresAt = expiresAtMs ?? now + ttlMs\n const exclusion =\n excludeIndexes.length === 0\n ? ''\n : `and event.idx not in (${excludeIndexes.map(() => '?').join(', ')})`\n const eligible = db\n .prepare(\n `select event.idx\n from a2_events event\n where event.session_id = ?\n and event.processed_at is null\n and event.failed_at is null\n and event.lane_ready = 1\n and (event.claim_expires_at is null or event.claim_expires_at <= ?)\n ${exclusion}\n order by event.idx`,\n )\n .all(sessionId, now, ...excludeIndexes) as unknown as Array<{\n idx: number | bigint\n }>\n if (eligible.length > 0) {\n const update = db.prepare(\n `update a2_events set\n attempt_count = attempt_count + 1,\n first_claimed_at = coalesce(first_claimed_at, ?),\n last_claimed_at = ?,\n claim_holder = ?,\n claim_expires_at = ?\n where session_id = ? and idx = ?\n returning *`,\n )\n const events = eligible.map((row) => {\n const claimed = update.get(\n now,\n now,\n holder,\n expiresAt,\n sessionId,\n row.idx,\n ) as EventRow\n return toStored(claimed)\n })\n return { outcome: 'claimed', events }\n }\n\n const active = db\n .prepare(\n `select min(claim_expires_at) as retry_at\n from a2_events\n where session_id = ?\n and processed_at is null\n and failed_at is null\n and lane_ready = 1\n and claim_expires_at > ?`,\n )\n .get(sessionId, now) as {\n retry_at: number | bigint | null\n }\n return active.retry_at === null\n ? { outcome: 'settled' }\n : { outcome: 'busy', retryAt: new Date(Number(active.retry_at)) }\n }),\n )\n },\n\n async renewClaims({ sessionId, holder, claims, ttlMs, expiresAtMs }) {\n if (claims.length === 0) return { renewed: [], superseded: [] }\n return wrap(() =>\n tx(() => {\n const now = clock.now().getTime()\n const expiresAt = expiresAtMs ?? now + ttlMs\n const renewed: number[] = []\n const superseded: number[] = []\n const currentAttempt = db.prepare(\n 'select attempt_count from a2_events where session_id = ? and idx = ?',\n )\n const extend = db.prepare(\n `update a2_events set claim_expires_at = ?\n where session_id = ?\n and idx = ?\n and attempt_count = ?\n and processed_at is null\n and failed_at is null\n and claim_holder = ?\n and claim_expires_at > ?`,\n )\n for (const claim of claims) {\n const row = currentAttempt.get(sessionId, claim.index) as\n { attempt_count: number | bigint } | undefined\n if (!row) continue\n if (Number(row.attempt_count) > claim.attempt) {\n superseded.push(claim.index)\n continue\n }\n const updated = extend.run(\n expiresAt,\n sessionId,\n claim.index,\n claim.attempt,\n holder,\n now,\n )\n if (Number(updated.changes) === 1) renewed.push(claim.index)\n }\n return {\n renewed: renewed.toSorted((a, b) => a - b),\n superseded: superseded.toSorted((a, b) => a - b),\n }\n }),\n )\n },\n\n async completeAttempt({ sessionId, index, attempt, events }) {\n return wrap(() =>\n tx(() => {\n const parent = db\n .prepare('select * from a2_events where session_id = ? and idx = ?')\n .get(sessionId, index) as EventRow | undefined\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\n if (parent.processed_at !== null) {\n if (Number(parent.processed_by_attempt) !== attempt) {\n return { outcome: 'superseded' }\n }\n const returnedIds =\n parent.returned_event_ids === null\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 const existing =\n ids.length === 0\n ? []\n : (selectByIds(ids.length).all(\n sessionId,\n ...ids,\n ) as unknown as ExistingEventRow[])\n const byId = new Map(existing.map((row) => [row.event_id, row]))\n const ordered = ids.map((id) => byId.get(id))\n if (\n ordered.some(\n (row) =>\n !row ||\n row.session_id !== sessionId ||\n row.cause === null ||\n toCause(row.cause)?.index !== index ||\n toCause(row.cause)?.attempt !== attempt,\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 ExistingEventRow[]).map(toStored),\n ),\n }\n }\n if (\n Number(parent.attempt_count) !== attempt ||\n parent.claim_holder === null ||\n parent.failed_at !== null\n ) {\n return { outcome: 'superseded' }\n }\n\n if (\n ids.length > 0 &&\n (selectByIds(ids.length).all(sessionId, ...ids) as unknown[])\n .length > 0\n ) {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n 'returned event batch contains an already-appended event id',\n )\n }\n\n const status = appendStatus.get(\n sessionId,\n sessionId,\n ) as AppendStatusRow\n const now = clock.now().getTime()\n db.prepare(\n `update a2_events set\n processed_at = ?,\n processed_by_attempt = ?,\n returned_event_ids = ?,\n lane_ready = 0,\n claim_holder = null,\n claim_expires_at = null\n where session_id = ? and idx = ?`,\n ).run(now, attempt, JSON.stringify(ids), sessionId, index)\n if (parent.lane !== null) {\n promoteLaneHead.run(sessionId, sessionId, parent.lane)\n }\n const inserted = events.map((event, offset) => {\n const id = event.id\n const childIndex = Number(status.max) + offset + 1\n const cause = { index, attempt }\n insertEvent.run(\n sessionId,\n childIndex,\n event.type,\n JSON.stringify(event.payload) ?? 'null',\n id,\n now,\n JSON.stringify(cause),\n event.lane ?? null,\n event.lane ?? null,\n sessionId,\n event.lane ?? null,\n event.settled ? now : null,\n )\n return toStored(\n db\n .prepare(\n 'select * from a2_events where session_id = ? and idx = ?',\n )\n .get(sessionId, childIndex) as EventRow,\n )\n })\n return { outcome: 'completed', events: inserted }\n }),\n )\n },\n\n async failAttempt({ sessionId, index, attempt, error, maxFailures }) {\n return wrap(() =>\n tx(() => {\n const current = db\n .prepare('select * from a2_events where session_id = ? and idx = ?')\n .get(sessionId, index) as EventRow | undefined\n if (!current) {\n throw new TypeError(\n `no event at index ${index} in session '${sessionId}'`,\n )\n }\n const failureCount = Number(current.failure_count)\n if (\n current.processed_at !== null ||\n Number(current.attempt_count) !== attempt\n ) {\n return { outcome: 'superseded', failureCount }\n }\n if (current.failed_at !== null) {\n return { outcome: 'dead_lettered', failureCount }\n }\n if (\n current.claim_holder === null &&\n current.last_failed_attempt !== null &&\n Number(current.last_failed_attempt) === attempt\n ) {\n return { outcome: 'failed', failureCount }\n }\n if (current.claim_holder === null) {\n return { outcome: 'superseded', failureCount }\n }\n\n const now = clock.now().getTime()\n const nextFailureCount = failureCount + 1\n const deadLettered = nextFailureCount >= maxFailures\n db.prepare(\n `update a2_events set\n failure_count = ?,\n last_error = ?,\n last_failed_at = ?,\n last_failed_attempt = ?,\n failed_at = ?,\n claim_holder = null,\n claim_expires_at = null\n where session_id = ? and idx = ?`,\n ).run(\n nextFailureCount,\n error,\n now,\n attempt,\n deadLettered ? now : null,\n sessionId,\n index,\n )\n return {\n outcome: deadLettered ? 'dead_lettered' : 'failed',\n failureCount: nextFailureCount,\n }\n }),\n )\n },\n\n async readState(sessionId, reducerName) {\n return wrap(() => {\n const rows = db\n .prepare(\n `with snapshot as materialized (\n select up_to_index, state\n from a2_snapshots\n where session_id = ? and reducer_name = ?\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 as session_id, null as idx, null as event_type,\n null as payload, null as event_id, null as created_at,\n null as cause, null as lane, null as lane_ready,\n null as processed_at,\n null as processed_by_attempt, null as returned_event_ids,\n null as first_claimed_at,\n null as last_claimed_at, null as attempt_count,\n null as failure_count, null as last_failed_at,\n null as last_failed_attempt, null as last_error,\n null as failed_at, null as claim_holder,\n null as claim_expires_at\n from snapshot\n union all\n select 1 as row_order, 'event' as row_kind,\n null as snapshot_index, null as snapshot_state,\n event.*\n from a2_events as event\n where event.session_id = ?\n and event.idx > coalesce((select up_to_index from snapshot), 0)\n order by row_order, idx`,\n )\n .all(sessionId, reducerName, sessionId) as unknown as StateReadRow[]\n const snapshot = rows.find((row) => row.row_kind === 'snapshot')\n return {\n snapshot: snapshot\n ? {\n index: Number(snapshot.snapshot_index),\n state: JSON.parse(snapshot.snapshot_state!),\n }\n : null,\n events: rows\n .filter(\n (row): row is StateReadRow & EventRow => row.row_kind === 'event',\n )\n .map(toEvent),\n }\n })\n },\n\n async putSnapshot(sessionId, reducerName, index, state) {\n wrap(() => {\n // Guarded upsert: a slower concurrent writer must never clobber\n // a further-along snapshot (spec §3).\n db.prepare(\n `insert into a2_snapshots (session_id, reducer_name, up_to_index, state, updated_at)\n values (?, ?, ?, ?, ?)\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 ).run(\n sessionId,\n reducerName,\n index,\n JSON.stringify(state) ?? 'null',\n 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 wrap(() =>\n tx(() => {\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 if (value === null) {\n deletePresence.run(ns, participant, field, atMs)\n } else {\n upsertPresence.run(\n ns,\n participant,\n field,\n JSON.stringify(value) ?? 'null',\n meta.seen,\n atMs,\n expiresAtMs,\n )\n }\n }\n }),\n )\n },\n\n async read(ns) {\n return wrap(() => {\n const now = clock.now().getTime()\n // The sweep is opportunistic cleanup; the select's filter\n // alone keeps expired rows out of this result.\n sweepPresence.run(ns, now)\n const rows = selectPresence.all(ns, now) as unknown as PresenceDbRow[]\n return rows.map((row) => ({\n participant: row.participant,\n field: row.field,\n value: JSON.parse(row.value) as unknown,\n seen: Number(row.seen),\n at: new Date(Number(row.at)),\n expiresAt: new Date(Number(row.expires_at)),\n }))\n })\n },\n },\n\n inspect: {\n async listSessions(inspectionOptions) {\n return wrap(() => {\n const conditions = ['substr(session_id, 1, ?) = ?']\n const params: (string | number)[] = [\n inspectionOptions.prefix.length,\n inspectionOptions.prefix,\n ]\n if (inspectionOptions.cursor !== undefined) {\n conditions.push('session_id > ?')\n params.push(inspectionOptions.cursor)\n }\n params.push(inspectionOptions.limit + 1)\n const rows = db\n .prepare(\n `select\n session_id,\n count(*) as event_count,\n sum(case when processed_at is null and failed_at is null then 1 else 0 end) as pending_count,\n sum(case when failed_at is not null then 1 else 0 end) 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(max(\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 ?`,\n )\n .all(...params) as unknown as SessionSummaryRow[]\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,\n eventCount: Number(row.event_count),\n pendingCount: Number(row.pending_count),\n failedCount: Number(row.failed_count),\n attemptCount: Number(row.attempt_count),\n failureCount: Number(row.failure_count),\n firstEventAt: new Date(Number(row.first_event_at)),\n updatedAt: new Date(Number(row.updated_at)),\n })),\n cursor: hasMore ? (visible.at(-1)?.session_id ?? null) : null,\n }\n })\n },\n async readEvents(sessionId, pageOptions) {\n return wrap(() => {\n const currentFrontier = Number(\n (\n db\n .prepare(\n 'select coalesce(max(idx), 0) as frontier from a2_events where session_id = ?',\n )\n .get(sessionId) as { frontier: number }\n ).frontier,\n )\n const throughIndex = Math.min(\n pageOptions.throughIndex ?? currentFrontier,\n currentFrontier,\n )\n const rows = db\n .prepare(\n `select * from a2_events\n where session_id = ? and idx > ? and idx <= ?\n order by idx limit ?`,\n )\n .all(\n sessionId,\n pageOptions.afterIndex,\n throughIndex,\n pageOptions.limit,\n ) as unknown as EventRow[]\n return { events: rows.map(toStored), throughIndex }\n })\n },\n async listSnapshots(sessionId) {\n return wrap(() => {\n const rows = db\n .prepare(\n `select reducer_name, up_to_index, updated_at\n from a2_snapshots where session_id = ? order by reducer_name`,\n )\n .all(sessionId) as unknown as SnapshotRow[]\n return rows.map((row) => ({\n reducerName: row.reducer_name,\n index: Number(row.up_to_index),\n updatedAt: new Date(Number(row.updated_at)),\n }))\n })\n },\n },\n\n stream(sessionId, opts) {\n const readAfter = (afterIndex: number): StoredEvent[] =>\n wrap(() => {\n const rows = db\n .prepare(\n 'select * from a2_events where session_id = ? and idx > ? order by idx',\n )\n .all(sessionId, afterIndex) as unknown as EventRow[]\n return rows.map(toStored)\n })\n return pollingStream(\n readAfter,\n opts?.startAfter !== undefined ? { startAfter: opts.startAfter } : {},\n )\n },\n\n close() {\n db.close()\n },\n }\n}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  //#region src/telemetry.d.ts
2
2
  /**
3
3
  * The A2Telemetry interface — the instrumentation seam. Same philosophy
4
- * as log backends: the interface lives in core, implementations ship as
4
+ * as store backends: the interface lives in core, implementations ship as
5
5
  * entry points (`experimental-a2/otel` adapts it to OpenTelemetry). Without one,
6
6
  * every operation runs through a no-op wrapper.
7
7
  */
@@ -29,4 +29,5 @@ type A2Telemetry = {
29
29
  span<T>(name: A2SpanName, attributes: Record<string, A2AttributeValue>, fn: (span: A2SpanHandle) => Promise<T>): Promise<T>;
30
30
  };
31
31
  //#endregion
32
- export { A2Telemetry as i, A2SpanHandle as n, A2SpanName as r, A2AttributeValue as t };
32
+ export { A2Telemetry as i, A2SpanHandle as n, A2SpanName as r, A2AttributeValue as t };
33
+ //# sourceMappingURL=telemetry-BjYHTfh2.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"telemetry-BjYHTfh2.d.ts","names":[],"sources":["../src/telemetry.ts"],"mappings":";;;;;;;;KAQY;;KAGA;KAEA;;EAEV,aAAa,aAAa,OAAO;;;;;;EAMjC,YAAY;;KAGF;;;;;;;EAOV,KAAK,GACH,MAAM,YACN,YAAY,eAAe,mBAC3B,KAAK,MAAM,iBAAiB,QAAQ,KACnC,QAAQ"}
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,6 @@
1
+ //#region src/testing.browser.ts
2
+ /** experimental-a2/testing loads local capture files and is Node-only. */
3
+ throw new Error("experimental-a2/testing is Node-only. Load captures in a local test or script.");
4
+ //#endregion
5
+
6
+ //# sourceMappingURL=testing.browser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testing.browser.js","names":[],"sources":["../src/testing.browser.ts"],"sourcesContent":["/** experimental-a2/testing loads local capture files and is Node-only. */\nthrow new Error(\n 'experimental-a2/testing is Node-only. Load captures in a local test or script.',\n)\nexport {}\n"],"mappings":";;AACA,MAAM,IAAI,MACR,gFACF"}
@@ -0,0 +1,32 @@
1
+ import { a as EventDefs, n as Contract, p as Reducer, r as ContractEvent } from "./contract-jIfaR085.js";
2
+ import { c as DevtoolsCapture, l as DevtoolsCaptureEvent } from "./devtools-J_jZ2vQf.js";
3
+ //#region src/testing.d.ts
4
+ type CaptureSource = string | URL;
5
+ type LoadedCapture<D extends EventDefs> = {
6
+ /** The verified capture, including its footer and operational records. */
7
+ readonly capture: DevtoolsCapture;
8
+ /** The contract used to validate and type the captured event history. */
9
+ readonly contract: Contract<D>;
10
+ /** Contract-typed public events with `createdAt` revived to `Date`. */
11
+ readonly events: readonly ContractEvent<D>[];
12
+ /** Unmodified wire events, including dispatch and failure metadata. */
13
+ readonly rawEvents: readonly DevtoolsCaptureEvent[];
14
+ };
15
+ type ReplayCaptureOptions = {
16
+ /** Fold events through this log index, inclusive. Zero means no events. */
17
+ throughIndex?: number;
18
+ };
19
+ type CaptureReplay<S> = {
20
+ readonly state: S;
21
+ /** The last event index actually folded, or zero for the initial state. */
22
+ readonly index: number;
23
+ };
24
+ /** Validate an already-parsed capture against an application's contract. */
25
+ declare function prepareCapture<D extends EventDefs>(capture: DevtoolsCapture, contract: Contract<D>): Promise<LoadedCapture<D>>;
26
+ /** Load, integrity-check, and contract-type a `.a2log` file or URL. */
27
+ declare function loadCapture<D extends EventDefs>(source: CaptureSource, contract: Contract<D>): Promise<LoadedCapture<D>>;
28
+ /** Purely fold captured events through a reducer, optionally to a cutpoint. */
29
+ declare function replayCapture<D extends EventDefs, S>(loaded: LoadedCapture<D>, reducer: Reducer<D, S>, options?: ReplayCaptureOptions): CaptureReplay<S>;
30
+ //#endregion
31
+ export { CaptureReplay, CaptureSource, LoadedCapture, ReplayCaptureOptions, loadCapture, prepareCapture, replayCapture };
32
+ //# sourceMappingURL=testing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testing.d.ts","names":[],"sources":["../src/testing.ts"],"mappings":";;;KAkBY,yBAAyB;KAEzB,cAAc,UAAU;;WAEzB,SAAS;;WAET,UAAU,SAAS;;WAEnB,iBAAiB,cAAc;;WAE/B,oBAAoB;;KAGnB;;EAEV;;KAGU,cAAc;WACf,OAAO;;WAEP;;;iBA6CW,eAAe,UAAU,WAC7C,SAAS,iBACT,UAAU,SAAS,KAClB,QAAQ,cAAc;;iBA0DH,YAAY,UAAU,WAC1C,QAAQ,eACR,UAAU,SAAS,KAClB,QAAQ,cAAc;;iBAeT,cAAc,UAAU,WAAW,GACjD,QAAQ,cAAc,IACtB,SAAS,QAAQ,GAAG,IACpB,UAAS,uBACR,cAAc"}
@@ -0,0 +1,103 @@
1
+ import { n as validateSync } from "./validate-XKT4FSNn.js";
2
+ import { l as parseDevtoolsCapture, u as verifyDevtoolsCapture } from "./devtools-kJJaORn-.js";
3
+ import { readFile } from "node:fs/promises";
4
+ import { fileURLToPath } from "node:url";
5
+ //#region src/testing.ts
6
+ /**
7
+ * Offline helpers for loading a captured A2 session and replaying its events
8
+ * through the application's current reducers. This module never constructs a
9
+ * server or dispatches handlers.
10
+ */
11
+ const captureError = (message) => /* @__PURE__ */ new TypeError(`invalid A2 capture: ${message}`);
12
+ const typedEvents = (capture, contract) => {
13
+ const events = [];
14
+ for (const event of capture.events) {
15
+ const schema = Object.hasOwn(contract.events, event.type) ? contract.events[event.type] : void 0;
16
+ if (schema === void 0) throw captureError(`contract '${contract.name}' has no event type '${event.type}' at index ${event.index}`);
17
+ const result = validateSync(schema, structuredClone(event.payload), `captured event '${event.type}'`);
18
+ if (result.issues !== void 0) throw captureError(`event #${event.index} '${event.type}' has an invalid payload: ${result.issues.map((issue) => issue.message).join("; ")}`);
19
+ events.push({
20
+ id: event.id,
21
+ type: event.type,
22
+ payload: result.value,
23
+ index: event.index,
24
+ sessionId: event.sessionId,
25
+ createdAt: new Date(event.createdAt)
26
+ });
27
+ }
28
+ return events;
29
+ };
30
+ /** Validate an already-parsed capture against an application's contract. */
31
+ async function prepareCapture(capture, contract) {
32
+ await verifyDevtoolsCapture(capture);
33
+ if (capture.contract !== contract.name) throw captureError(`expected contract '${contract.name}', received '${capture.contract}'`);
34
+ return {
35
+ capture,
36
+ contract,
37
+ events: typedEvents(capture, contract),
38
+ rawEvents: capture.events
39
+ };
40
+ }
41
+ const displayUrl = (url) => {
42
+ if (url.protocol === "data:") return "data: URL";
43
+ const safe = new URL(url);
44
+ safe.username = "";
45
+ safe.password = "";
46
+ safe.search = "";
47
+ safe.hash = "";
48
+ return safe.href;
49
+ };
50
+ const fetchCapture = async (url) => {
51
+ const location = displayUrl(url);
52
+ let response;
53
+ try {
54
+ response = await fetch(url);
55
+ } catch {
56
+ throw new Error(`failed to load A2 capture from '${location}'`);
57
+ }
58
+ if (!response.ok) throw new Error(`failed to load A2 capture from '${location}': ${response.status} ${response.statusText}`);
59
+ return new Uint8Array(await response.arrayBuffer());
60
+ };
61
+ const readCapture = async (source) => {
62
+ if (source instanceof URL) return source.protocol === "file:" ? readFile(fileURLToPath(source)) : fetchCapture(source);
63
+ if (/^[a-z]:[\\/]/i.test(source)) return readFile(source);
64
+ let url;
65
+ try {
66
+ url = new URL(source);
67
+ } catch {
68
+ return readFile(source);
69
+ }
70
+ return readCapture(url);
71
+ };
72
+ /** Load, integrity-check, and contract-type a `.a2log` file or URL. */
73
+ async function loadCapture(source, contract) {
74
+ const encoded = await readCapture(source);
75
+ return prepareCapture(parseDevtoolsCapture(encoded), contract);
76
+ }
77
+ const cloneInitial = (initialState) => {
78
+ try {
79
+ return structuredClone(initialState);
80
+ } catch {
81
+ return initialState;
82
+ }
83
+ };
84
+ /** Purely fold captured events through a reducer, optionally to a cutpoint. */
85
+ function replayCapture(loaded, reducer, options = {}) {
86
+ const { throughIndex } = options;
87
+ if (throughIndex !== void 0 && (!Number.isInteger(throughIndex) || throughIndex < 0)) throw new TypeError("throughIndex must be a non-negative integer");
88
+ let state = cloneInitial(reducer.initialState);
89
+ let index = 0;
90
+ for (const event of loaded.events) {
91
+ if (throughIndex !== void 0 && event.index > throughIndex) break;
92
+ state = reducer.fold(state, event);
93
+ index = event.index;
94
+ }
95
+ return {
96
+ state,
97
+ index
98
+ };
99
+ }
100
+ //#endregion
101
+ export { loadCapture, prepareCapture, replayCapture };
102
+
103
+ //# sourceMappingURL=testing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testing.js","names":[],"sources":["../src/testing.ts"],"sourcesContent":["/**\n * Offline helpers for loading a captured A2 session and replaying its events\n * through the application's current reducers. This module never constructs a\n * server or dispatches handlers.\n */\n\nimport { readFile } from 'node:fs/promises'\nimport { fileURLToPath } from 'node:url'\nimport type { Contract, ContractEvent, EventDefs } from './contract.ts'\nimport {\n parseDevtoolsCapture,\n verifyDevtoolsCapture,\n type DevtoolsCapture,\n type DevtoolsCaptureEvent,\n} from './devtools.ts'\nimport type { Reducer } from './reducer.ts'\nimport { validateSync } from './validate.ts'\n\nexport type CaptureSource = string | URL\n\nexport type LoadedCapture<D extends EventDefs> = {\n /** The verified capture, including its footer and operational records. */\n readonly capture: DevtoolsCapture\n /** The contract used to validate and type the captured event history. */\n readonly contract: Contract<D>\n /** Contract-typed public events with `createdAt` revived to `Date`. */\n readonly events: readonly ContractEvent<D>[]\n /** Unmodified wire events, including dispatch and failure metadata. */\n readonly rawEvents: readonly DevtoolsCaptureEvent[]\n}\n\nexport type ReplayCaptureOptions = {\n /** Fold events through this log index, inclusive. Zero means no events. */\n throughIndex?: number\n}\n\nexport type CaptureReplay<S> = {\n readonly state: S\n /** The last event index actually folded, or zero for the initial state. */\n readonly index: number\n}\n\nconst captureError = (message: string): TypeError =>\n new TypeError(`invalid A2 capture: ${message}`)\n\nconst typedEvents = <D extends EventDefs>(\n capture: DevtoolsCapture,\n contract: Contract<D>,\n): ContractEvent<D>[] => {\n const events: ContractEvent<D>[] = []\n for (const event of capture.events) {\n const schema = Object.hasOwn(contract.events, event.type)\n ? contract.events[event.type]\n : undefined\n if (schema === undefined) {\n throw captureError(\n `contract '${contract.name}' has no event type '${event.type}' at index ${event.index}`,\n )\n }\n const result = validateSync(\n schema,\n structuredClone(event.payload),\n `captured event '${event.type}'`,\n )\n if (result.issues !== undefined) {\n throw captureError(\n `event #${event.index} '${event.type}' has an invalid payload: ${result.issues\n .map((issue) => issue.message)\n .join('; ')}`,\n )\n }\n events.push({\n id: event.id,\n type: event.type,\n payload: result.value,\n index: event.index,\n sessionId: event.sessionId,\n createdAt: new Date(event.createdAt),\n } as ContractEvent<D>)\n }\n return events\n}\n\n/** Validate an already-parsed capture against an application's contract. */\nexport async function prepareCapture<D extends EventDefs>(\n capture: DevtoolsCapture,\n contract: Contract<D>,\n): Promise<LoadedCapture<D>> {\n await verifyDevtoolsCapture(capture)\n if (capture.contract !== contract.name) {\n throw captureError(\n `expected contract '${contract.name}', received '${capture.contract}'`,\n )\n }\n return {\n capture,\n contract,\n events: typedEvents(capture, contract),\n rawEvents: capture.events,\n }\n}\n\nconst displayUrl = (url: URL): string => {\n if (url.protocol === 'data:') return 'data: URL'\n const safe = new URL(url)\n safe.username = ''\n safe.password = ''\n safe.search = ''\n safe.hash = ''\n return safe.href\n}\n\nconst fetchCapture = async (url: URL): Promise<Uint8Array> => {\n const location = displayUrl(url)\n let response: Response\n try {\n response = await fetch(url)\n } catch {\n throw new Error(`failed to load A2 capture from '${location}'`)\n }\n if (!response.ok) {\n throw new Error(\n `failed to load A2 capture from '${location}': ${response.status} ${response.statusText}`,\n )\n }\n return new Uint8Array(await response.arrayBuffer())\n}\n\nconst readCapture = async (source: CaptureSource): Promise<Uint8Array> => {\n if (source instanceof URL) {\n return source.protocol === 'file:'\n ? readFile(fileURLToPath(source))\n : fetchCapture(source)\n }\n if (/^[a-z]:[\\\\/]/i.test(source)) return readFile(source)\n let url: URL | undefined\n try {\n url = new URL(source)\n } catch {\n return readFile(source)\n }\n return readCapture(url)\n}\n\n/** Load, integrity-check, and contract-type a `.a2log` file or URL. */\nexport async function loadCapture<D extends EventDefs>(\n source: CaptureSource,\n contract: Contract<D>,\n): Promise<LoadedCapture<D>> {\n const encoded = await readCapture(source)\n const capture = parseDevtoolsCapture(encoded)\n return prepareCapture(capture, contract)\n}\n\nconst cloneInitial = <S>(initialState: S): S => {\n try {\n return structuredClone(initialState)\n } catch {\n return initialState\n }\n}\n\n/** Purely fold captured events through a reducer, optionally to a cutpoint. */\nexport function replayCapture<D extends EventDefs, S>(\n loaded: LoadedCapture<D>,\n reducer: Reducer<D, S>,\n options: ReplayCaptureOptions = {},\n): CaptureReplay<S> {\n const { throughIndex } = options\n if (\n throughIndex !== undefined &&\n (!Number.isInteger(throughIndex) || throughIndex < 0)\n ) {\n throw new TypeError('throughIndex must be a non-negative integer')\n }\n\n let state = cloneInitial(reducer.initialState)\n let index = 0\n for (const event of loaded.events) {\n if (throughIndex !== undefined && event.index > throughIndex) break\n state = reducer.fold(state, event)\n index = event.index\n }\n return { state, index }\n}\n"],"mappings":"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"}
@@ -26,3 +26,5 @@ function assertSyncSchema(schema, what) {
26
26
  }
27
27
  //#endregion
28
28
  export { validateSync as n, assertSyncSchema as t };
29
+
30
+ //# sourceMappingURL=validate-XKT4FSNn.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate-XKT4FSNn.js","names":[],"sources":["../src/validate.ts"],"sourcesContent":["import type { StandardSchemaV1 } from './standard-schema.ts'\n\n/**\n * Run a Standard Schema validator, requiring a synchronous result.\n * Validators must be synchronous (specs/a2-api.md §1) — an async one is\n * a definition bug, rejected at definition time where possible and here\n * as a backstop. `what` names the schema in error messages, e.g.\n * \"event 'created'\" or \"the stateSchema\".\n */\nexport function validateSync<Input, Output>(\n schema: StandardSchemaV1<Input, Output>,\n value: unknown,\n what: string,\n): StandardSchemaV1.Result<Output> {\n const result = schema['~standard'].validate(value)\n if (result instanceof Promise) {\n throw new TypeError(\n `the schema for ${what} is asynchronous — a2 schemas must validate synchronously`,\n )\n }\n return result\n}\n\n/**\n * Probe a schema to reject always-async validators early. The probe\n * value is deliberately garbage — we only care whether the result is a\n * Promise, not whether it passes.\n */\nexport function assertSyncSchema(schema: StandardSchemaV1, what: string): void {\n const probe = Symbol('a2.schema-probe')\n const result = schema['~standard'].validate(probe)\n if (result instanceof Promise) {\n // Swallow the eventual rejection — the probe input was garbage.\n result.catch(() => {})\n throw new TypeError(\n `the schema for ${what} is asynchronous — a2 schemas must validate synchronously`,\n )\n }\n}\n"],"mappings":";;;;;;;;AASA,SAAgB,aACd,QACA,OACA,MACiC;CACjC,MAAM,SAAS,OAAO,YAAY,CAAC,SAAS,KAAK;CACjD,IAAI,kBAAkB,SACpB,MAAM,IAAI,UACR,kBAAkB,KAAK,0DACzB;CAEF,OAAO;AACT;;;;;;AAOA,SAAgB,iBAAiB,QAA0B,MAAoB;CAC7E,MAAM,QAAQ,OAAO,iBAAiB;CACtC,MAAM,SAAS,OAAO,YAAY,CAAC,SAAS,KAAK;CACjD,IAAI,kBAAkB,SAAS;EAE7B,OAAO,YAAY,CAAC,CAAC;EACrB,MAAM,IAAI,UACR,kBAAkB,KAAK,0DACzB;CACF;AACF"}