experimental-a2 0.2.0 → 0.4.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 (92) hide show
  1. package/CHANGELOG.md +169 -0
  2. package/dist/ai-B4YhEnfw.d.ts +333 -0
  3. package/dist/ai-server.d.ts +48 -9
  4. package/dist/ai-server.js +121 -49
  5. package/dist/ai.d.ts +2 -303
  6. package/dist/ai.js +231 -86
  7. package/dist/cli-B3VuxoDe.js +597 -0
  8. package/dist/cli-bin.d.ts +1 -0
  9. package/dist/cli-bin.js +5 -0
  10. package/dist/cli.d.ts +19 -0
  11. package/dist/cli.js +2 -0
  12. package/dist/client-BrfDXQ8A.d.ts +155 -0
  13. package/dist/client-Bt4tAKi9.js +798 -0
  14. package/dist/client.d.ts +2 -90
  15. package/dist/client.js +1 -409
  16. package/dist/{contract-CG_adnu_.js → contract-48bUMgcL.js} +10 -2
  17. package/dist/{contract-C_3dIIEU.d.ts → contract-jIfaR085.d.ts} +62 -8
  18. package/dist/devtools-J_jZ2vQf.d.ts +151 -0
  19. package/dist/devtools-kJJaORn-.js +338 -0
  20. package/dist/devtools-server.browser.js +1 -1
  21. package/dist/devtools-server.d.ts +2 -2
  22. package/dist/devtools-server.js +224 -43
  23. package/dist/devtools.d.ts +2 -0
  24. package/dist/devtools.js +2 -0
  25. package/dist/{errors-BJRMd-h6.js → errors-BQuJpe82.js} +4 -4
  26. package/dist/{errors-xL_JTXsY.d.ts → errors-W6nwJ-fm.d.ts} +1 -1
  27. package/dist/http.d.ts +71 -13
  28. package/dist/http.js +302 -41
  29. package/dist/{idempotent-replay-BMyHrP0L.js → idempotent-replay-DuqEkYA7.js} +2 -2
  30. package/dist/index.d.ts +5 -5
  31. package/dist/index.js +2 -2
  32. package/dist/{inspection-E7qbD0Xj.js → inspection-DaxB5jM2.js} +2 -1
  33. package/dist/internal-aEotMzu_.js +209 -0
  34. package/dist/otel.d.ts +1 -1
  35. package/dist/platform-B4TnJtWu.js +32 -0
  36. package/dist/react.d.ts +37 -14
  37. package/dist/react.js +26 -15
  38. package/dist/scheduler-qstash.d.ts +78 -0
  39. package/dist/scheduler-qstash.js +499 -0
  40. package/dist/scheduler-task-BpzhPnRS.js +54 -0
  41. package/dist/{recovery-vercel.d.ts → scheduler-vercel.d.ts} +17 -24
  42. package/dist/scheduler-vercel.js +226 -0
  43. package/dist/server-CcNnFnoW.js +1405 -0
  44. package/dist/server-YtPq7hjw.d.ts +260 -0
  45. package/dist/server.d.ts +4 -155
  46. package/dist/server.js +2 -2
  47. package/dist/{log-ldf5g8Cx.d.ts → store-C3sNAaBT.d.ts} +111 -35
  48. package/dist/{log-yJbXUf72.js → store-N8PXxDAS.js} +1 -1
  49. package/dist/store-codec-DTG0Ftek.js +8 -0
  50. package/dist/store-memory.d.ts +11 -0
  51. package/dist/{log-memory.js → store-memory.js} +127 -24
  52. package/dist/{log-polling-6COoN60V.js → store-polling-DgrrAE3d.js} +7 -6
  53. package/dist/{log-postgres.d.ts → store-postgres.d.ts} +6 -6
  54. package/dist/{log-postgres.js → store-postgres.js} +158 -24
  55. package/dist/{log-redis.js → store-redis-core-DWqx3F47.js} +294 -156
  56. package/dist/store-redis-http.d.ts +21 -0
  57. package/dist/store-redis-http.js +70 -0
  58. package/dist/store-redis.d.ts +37 -0
  59. package/dist/store-redis.js +298 -0
  60. package/dist/{log-sqlite.d.ts → store-sqlite.d.ts} +6 -6
  61. package/dist/{log-sqlite.js → store-sqlite.js} +116 -22
  62. package/dist/{telemetry-Cso0qyHQ.d.ts → telemetry-BjYHTfh2.d.ts} +1 -1
  63. package/dist/testing.browser.d.ts +1 -0
  64. package/dist/testing.browser.js +4 -0
  65. package/dist/testing.d.ts +31 -0
  66. package/dist/testing.js +101 -0
  67. package/dist/wire-DCUZBUlT.js +222 -0
  68. package/docs/01-quickstart.mdx +4 -5
  69. package/docs/concepts/01-contracts.mdx +21 -17
  70. package/docs/concepts/02-handlers.mdx +7 -7
  71. package/docs/concepts/03-durability.mdx +26 -29
  72. package/docs/concepts/04-state.mdx +18 -21
  73. package/docs/guides/01-timers.mdx +154 -54
  74. package/docs/guides/02-cancellation.mdx +30 -4
  75. package/docs/guides/03-react.mdx +20 -21
  76. package/docs/guides/04-local-first.mdx +1 -1
  77. package/docs/guides/05-production.mdx +321 -60
  78. package/docs/guides/06-ai-agents.mdx +249 -49
  79. package/docs/guides/07-devtools.mdx +137 -12
  80. package/docs/guides/08-application-data.mdx +1 -1
  81. package/docs/guides/09-presence.mdx +284 -0
  82. package/docs/guides/10-transports.mdx +131 -0
  83. package/docs/index.mdx +22 -46
  84. package/docs/reference/01-api.mdx +751 -102
  85. package/docs/reference/02-errors.mdx +10 -5
  86. package/package.json +23 -6
  87. package/dist/internal-D6wNxTck.js +0 -36
  88. package/dist/log-memory.d.ts +0 -11
  89. package/dist/log-redis.d.ts +0 -31
  90. package/dist/recovery-vercel.js +0 -119
  91. package/dist/server-DJgD2YWP.js +0 -877
  92. 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-DgrrAE3d.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 { t as pollingStream } from "./log-polling-6COoN60V.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",
@@ -592,6 +650,63 @@ function postgres(options = {}) {
592
650
  ]);
593
651
  });
594
652
  },
653
+ presence: {
654
+ async set(ns, participant, values, meta) {
655
+ const entries = Object.entries(values);
656
+ if (entries.length === 0) return;
657
+ await wrap(async () => {
658
+ const c = await client();
659
+ const atMs = meta.at.getTime();
660
+ const expiresAtMs = clock.now().getTime() + meta.ttlMs;
661
+ for (const [field, value] of entries) await (value === null ? c.query(`delete from a2_presence
662
+ where ns = $1 and participant = $2 and field = $3
663
+ and at <= $4`, [
664
+ ns,
665
+ participant,
666
+ field,
667
+ atMs
668
+ ]) : c.query(`insert into a2_presence
669
+ (ns, participant, field, value, seen, at, expires_at)
670
+ values ($1, $2, $3, $4::jsonb, $5, $6, $7)
671
+ on conflict (ns, participant, field) do update set
672
+ value = excluded.value,
673
+ seen = excluded.seen,
674
+ at = excluded.at,
675
+ expires_at = excluded.expires_at
676
+ where a2_presence.at <= excluded.at`, [
677
+ ns,
678
+ participant,
679
+ field,
680
+ jsonb(value),
681
+ meta.seen,
682
+ atMs,
683
+ expiresAtMs
684
+ ]));
685
+ });
686
+ },
687
+ async read(ns) {
688
+ return wrap(async () => {
689
+ const c = await client();
690
+ const now = clock.now().getTime();
691
+ const { rows } = await c.query(`with swept as (
692
+ delete from a2_presence
693
+ where ns = $1 and expires_at <= $2
694
+ )
695
+ select participant, field, value, seen, at, expires_at
696
+ from a2_presence
697
+ where ns = $1 and expires_at > $2
698
+ order by participant, field`, [ns, now]);
699
+ return rows.map((row) => ({
700
+ participant: row["participant"],
701
+ field: row["field"],
702
+ value: row["value"],
703
+ seen: asNumber(row["seen"]),
704
+ at: new Date(asNumber(row["at"])),
705
+ expiresAt: new Date(asNumber(row["expires_at"]))
706
+ }));
707
+ });
708
+ }
709
+ },
595
710
  inspect: {
596
711
  async listSessions(inspectionOptions) {
597
712
  return wrap(async () => {
@@ -641,6 +756,25 @@ function postgres(options = {}) {
641
756
  };
642
757
  });
643
758
  },
759
+ async readEvents(sessionId, pageOptions) {
760
+ return wrap(async () => {
761
+ const c = await client();
762
+ 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);
763
+ const throughIndex = Math.min(pageOptions.throughIndex ?? currentFrontier, currentFrontier);
764
+ const { rows } = await c.query(`select * from a2_events
765
+ where session_id = $1 and idx > $2 and idx <= $3
766
+ order by idx limit $4`, [
767
+ sessionId,
768
+ pageOptions.afterIndex,
769
+ throughIndex,
770
+ pageOptions.limit
771
+ ]);
772
+ return {
773
+ events: rows.map(toStored),
774
+ throughIndex
775
+ };
776
+ });
777
+ },
644
778
  async listSnapshots(sessionId) {
645
779
  return wrap(async () => {
646
780
  const { rows } = await (await client()).query(`select reducer_name, up_to_index, updated_at
@@ -658,7 +792,7 @@ function postgres(options = {}) {
658
792
  const { rows } = await (await client()).query("select * from a2_events where session_id = $1 and idx > $2 order by idx", [sessionId, afterIndex]);
659
793
  return rows.map(toStored);
660
794
  });
661
- return pollingStream(readAfter, opts?.startAt !== void 0 ? { startAt: opts.startAt } : {});
795
+ return pollingStream(readAfter, opts?.startAfter !== void 0 ? { startAfter: opts.startAfter } : {});
662
796
  },
663
797
  async close() {
664
798
  const current = connection.peek();