experimental-a2 0.0.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 (68) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/dist/ai-server.browser.d.ts +1 -0
  3. package/dist/ai-server.browser.js +4 -0
  4. package/dist/ai-server.d.ts +65 -0
  5. package/dist/ai-server.js +494 -0
  6. package/dist/ai.d.ts +282 -0
  7. package/dist/ai.js +922 -0
  8. package/dist/cache-indexeddb.d.ts +1 -0
  9. package/dist/cache-indexeddb.js +0 -0
  10. package/dist/client.d.ts +90 -0
  11. package/dist/client.js +410 -0
  12. package/dist/contract-B0kAXoaL.js +60 -0
  13. package/dist/contract-DL8btVd9.d.ts +161 -0
  14. package/dist/devtools-server.browser.d.ts +1 -0
  15. package/dist/devtools-server.browser.js +4 -0
  16. package/dist/devtools-server.d.ts +22 -0
  17. package/dist/devtools-server.js +1087 -0
  18. package/dist/errors-BJRMd-h6.js +23 -0
  19. package/dist/errors-xL_JTXsY.d.ts +20 -0
  20. package/dist/http.d.ts +44 -0
  21. package/dist/http.js +119 -0
  22. package/dist/index.d.ts +5 -0
  23. package/dist/index.js +3 -0
  24. package/dist/inspection-E7qbD0Xj.js +10 -0
  25. package/dist/internal-Dm8Ejnud.js +36 -0
  26. package/dist/log-Dg1I8NRr.d.ts +245 -0
  27. package/dist/log-memory.d.ts +11 -0
  28. package/dist/log-memory.js +345 -0
  29. package/dist/log-polling-RO7kclzR.js +83 -0
  30. package/dist/log-postgres.d.ts +40 -0
  31. package/dist/log-postgres.js +628 -0
  32. package/dist/log-redis.d.ts +31 -0
  33. package/dist/log-redis.js +711 -0
  34. package/dist/log-sqlite.d.ts +17 -0
  35. package/dist/log-sqlite.js +450 -0
  36. package/dist/log-yJbXUf72.js +5 -0
  37. package/dist/otel.d.ts +12 -0
  38. package/dist/otel.js +41 -0
  39. package/dist/react.d.ts +54 -0
  40. package/dist/react.js +85 -0
  41. package/dist/recovery-vercel.d.ts +60 -0
  42. package/dist/recovery-vercel.js +120 -0
  43. package/dist/retryable-lazy-DZWmHpii.js +19 -0
  44. package/dist/server-DYsnKTTy.js +780 -0
  45. package/dist/server.browser.d.ts +1 -0
  46. package/dist/server.browser.js +11 -0
  47. package/dist/server.d.ts +136 -0
  48. package/dist/server.js +2 -0
  49. package/dist/telemetry-C78al20p.d.ts +32 -0
  50. package/dist/validate-XKT4FSNn.js +28 -0
  51. package/dist/wire-2QpU1EtJ.js +62 -0
  52. package/docs/01-quickstart.mdx +214 -0
  53. package/docs/concepts/01-contracts.mdx +138 -0
  54. package/docs/concepts/02-handlers.mdx +146 -0
  55. package/docs/concepts/03-durability.mdx +230 -0
  56. package/docs/concepts/04-state.mdx +133 -0
  57. package/docs/guides/01-timers.mdx +85 -0
  58. package/docs/guides/02-cancellation.mdx +107 -0
  59. package/docs/guides/03-react.mdx +234 -0
  60. package/docs/guides/04-local-first.mdx +88 -0
  61. package/docs/guides/05-production.mdx +179 -0
  62. package/docs/guides/06-ai-agents.mdx +659 -0
  63. package/docs/guides/07-devtools.mdx +101 -0
  64. package/docs/guides/08-application-data.mdx +114 -0
  65. package/docs/index.mdx +282 -0
  66. package/docs/reference/01-api.mdx +637 -0
  67. package/docs/reference/02-errors.mdx +77 -0
  68. package/package.json +111 -0
@@ -0,0 +1,345 @@
1
+ import { t as A2Error } from "./errors-BJRMd-h6.js";
2
+ import { n as SYSTEM_CLOCK, t as RANDOM_IDS } from "./log-yJbXUf72.js";
3
+ //#region src/log-memory.ts
4
+ /**
5
+ * a2/log-memory — in-memory log backend (the test default).
6
+ *
7
+ * Implements the A2Log interface with zero dependencies. See
8
+ * specs/a2-implementation.md §3 for the contract; the conformance suite
9
+ * in test/conformance is the executable version of it.
10
+ */
11
+ const toStored = (row) => ({
12
+ id: row.id,
13
+ type: row.type,
14
+ payload: structuredClone(row.payload),
15
+ index: row.index,
16
+ sessionId: row.sessionId,
17
+ createdAt: new Date(row.createdAt),
18
+ cause: row.cause ? { ...row.cause } : null,
19
+ processedAt: row.processedAt ? new Date(row.processedAt) : null,
20
+ processedByAttempt: row.processedByAttempt,
21
+ firstClaimedAt: row.firstClaimedAt ? new Date(row.firstClaimedAt) : null,
22
+ lastClaimedAt: row.lastClaimedAt ? new Date(row.lastClaimedAt) : null,
23
+ attemptCount: row.attemptCount,
24
+ failureCount: row.failureCount,
25
+ lastFailedAt: row.lastFailedAt ? new Date(row.lastFailedAt) : null,
26
+ lastFailedAttempt: row.lastFailedAttempt,
27
+ lastError: row.lastError,
28
+ failedAt: row.failedAt ? new Date(row.failedAt) : null
29
+ });
30
+ const toEvent = (row) => ({
31
+ id: row.id,
32
+ type: row.type,
33
+ payload: structuredClone(row.payload),
34
+ index: row.index,
35
+ sessionId: row.sessionId,
36
+ createdAt: new Date(row.createdAt)
37
+ });
38
+ function memory(options = {}) {
39
+ const clock = options.clock ?? SYSTEM_CLOCK;
40
+ const generateId = options.ids ?? RANDOM_IDS;
41
+ const sessions = /* @__PURE__ */ new Map();
42
+ /** Global unique index on event ids, like the SQL schema's. */
43
+ const byEventId = /* @__PURE__ */ new Map();
44
+ const leases = /* @__PURE__ */ new Map();
45
+ /** Array offset of the first row not known to be processed. */
46
+ const pendingHeads = /* @__PURE__ */ new Map();
47
+ /** Keyed by `${sessionId}\u0000${reducerName}` — a pure cache. */
48
+ const snapshots = /* @__PURE__ */ new Map();
49
+ const streamSubscribers = /* @__PURE__ */ new Map();
50
+ const notifyStreams = (sessionId, rows) => {
51
+ const subs = streamSubscribers.get(sessionId);
52
+ if (!subs) return;
53
+ for (const listener of subs) for (const row of rows) listener(row);
54
+ };
55
+ const rowsOf = (sessionId) => {
56
+ let rows = sessions.get(sessionId);
57
+ if (!rows) {
58
+ rows = [];
59
+ sessions.set(sessionId, rows);
60
+ }
61
+ return rows;
62
+ };
63
+ const find = (sessionId, index) => {
64
+ const row = sessions.get(sessionId)?.find((candidate) => candidate.index === index);
65
+ if (!row) throw new TypeError(`no event at index ${index} in session '${sessionId}'`);
66
+ return row;
67
+ };
68
+ const pendingHead = (sessionId) => {
69
+ const rows = sessions.get(sessionId) ?? [];
70
+ let offset = pendingHeads.get(sessionId) ?? 0;
71
+ while (offset < rows.length && rows[offset]?.processedAt !== null) offset += 1;
72
+ pendingHeads.set(sessionId, offset);
73
+ return rows[offset];
74
+ };
75
+ const claim = (row, now) => {
76
+ row.attemptCount += 1;
77
+ row.firstClaimedAt ??= new Date(now);
78
+ row.lastClaimedAt = new Date(now);
79
+ return toStored(row);
80
+ };
81
+ return {
82
+ async append(sessionId, events) {
83
+ if (events.length === 0) return [];
84
+ const supplied = events.filter((e) => e.id !== void 0);
85
+ if (new Set(supplied.map((e) => e.id)).size !== supplied.length) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "batch contains the same event id more than once");
86
+ const existing = supplied.map((e) => byEventId.get(e.id)).filter((row) => row !== void 0);
87
+ if (existing.length > 0) {
88
+ const foreign = existing.find((row) => row.sessionId !== sessionId);
89
+ if (foreign) throw new A2Error("PARTIAL_DUPLICATE_BATCH", `event id '${foreign.id}' already exists in another session`);
90
+ if (existing.length === events.length) return existing.toSorted((a, b) => a.index - b.index).map(toStored);
91
+ throw new A2Error("PARTIAL_DUPLICATE_BATCH", `batch mixes ${existing.length} already-appended and ${events.length - existing.length} fresh events`);
92
+ }
93
+ const rows = rowsOf(sessionId);
94
+ const now = clock.now();
95
+ const base = rows.length === 0 ? 0 : rows[rows.length - 1].index;
96
+ const inserted = events.map((e, i) => {
97
+ return {
98
+ id: e.id ?? generateId(),
99
+ type: e.type,
100
+ payload: structuredClone(e.payload),
101
+ index: base + 1 + i,
102
+ sessionId,
103
+ createdAt: new Date(now),
104
+ cause: e.cause ? { ...e.cause } : null,
105
+ processedAt: null,
106
+ processedByAttempt: null,
107
+ firstClaimedAt: null,
108
+ lastClaimedAt: null,
109
+ attemptCount: 0,
110
+ failureCount: 0,
111
+ lastFailedAt: null,
112
+ lastFailedAttempt: null,
113
+ lastError: null,
114
+ failedAt: null
115
+ };
116
+ });
117
+ for (const row of inserted) if (byEventId.has(row.id)) throw new A2Error("PARTIAL_DUPLICATE_BATCH", `event id '${row.id}' already exists`);
118
+ for (const row of inserted) {
119
+ rows.push(row);
120
+ byEventId.set(row.id, row);
121
+ }
122
+ notifyStreams(sessionId, inserted);
123
+ return inserted.map(toStored);
124
+ },
125
+ async read(sessionId, opts) {
126
+ return (sessions.get(sessionId) ?? []).filter((row) => {
127
+ if (opts?.afterIndex !== void 0 && row.index <= opts.afterIndex) return false;
128
+ if (opts?.unprocessedOnly && row.processedAt !== null) return false;
129
+ return true;
130
+ }).map(toStored);
131
+ },
132
+ async claimNext({ sessionId, holder, ttlMs, expiresAtMs, maxIndex }) {
133
+ const next = pendingHead(sessionId);
134
+ if (!next || next.failedAt || maxIndex !== void 0 && next.index > maxIndex) return { outcome: "settled" };
135
+ const claimedAt = clock.now();
136
+ const now = claimedAt.getTime();
137
+ const current = leases.get(sessionId);
138
+ if (current && current.expiresAt > now && current.holder !== holder) return { outcome: "busy" };
139
+ leases.set(sessionId, {
140
+ holder,
141
+ expiresAt: expiresAtMs ?? now + ttlMs
142
+ });
143
+ return {
144
+ outcome: "claimed",
145
+ event: claim(next, claimedAt)
146
+ };
147
+ },
148
+ async completeAndClaimNext({ sessionId, holder, completedIndex, attempt, maxIndex }) {
149
+ const claimedAt = clock.now();
150
+ const completed = find(sessionId, completedIndex);
151
+ if (completed.processedAt || completed.attemptCount !== attempt) return { outcome: "superseded" };
152
+ completed.processedAt = new Date(claimedAt);
153
+ completed.processedByAttempt = attempt;
154
+ const next = pendingHead(sessionId);
155
+ if (!next || next.failedAt || maxIndex !== void 0 && next.index > maxIndex) return { outcome: "settled" };
156
+ const now = claimedAt.getTime();
157
+ const current = leases.get(sessionId);
158
+ if (!current || current.holder !== holder || current.expiresAt <= now) return { outcome: "busy" };
159
+ return {
160
+ outcome: "claimed",
161
+ event: claim(next, claimedAt)
162
+ };
163
+ },
164
+ async markProcessed(sessionId, index) {
165
+ const row = find(sessionId, index);
166
+ row.processedAt ??= clock.now();
167
+ },
168
+ async failAttempt({ sessionId, index, attempt, error, maxFailures }) {
169
+ const row = find(sessionId, index);
170
+ if (row.processedAt || row.attemptCount !== attempt) return {
171
+ outcome: "superseded",
172
+ failureCount: row.failureCount
173
+ };
174
+ if (row.failedAt) return {
175
+ outcome: "dead_lettered",
176
+ failureCount: row.failureCount
177
+ };
178
+ row.failureCount += 1;
179
+ row.lastError = error;
180
+ const failedAt = clock.now();
181
+ row.lastFailedAt = new Date(failedAt);
182
+ row.lastFailedAttempt = attempt;
183
+ if (row.failureCount >= maxFailures) {
184
+ row.failedAt = new Date(failedAt);
185
+ return {
186
+ outcome: "dead_lettered",
187
+ failureCount: row.failureCount
188
+ };
189
+ }
190
+ return {
191
+ outcome: "failed",
192
+ failureCount: row.failureCount
193
+ };
194
+ },
195
+ async markFailed(sessionId, index) {
196
+ find(sessionId, index).failedAt = clock.now();
197
+ },
198
+ async readState(sessionId, reducerName) {
199
+ const snap = snapshots.get(`${sessionId}\u0000${reducerName}`);
200
+ const snapshot = snap ? {
201
+ index: snap.index,
202
+ state: structuredClone(snap.state)
203
+ } : null;
204
+ const afterIndex = snapshot?.index ?? 0;
205
+ return {
206
+ snapshot,
207
+ events: (sessions.get(sessionId) ?? []).filter((row) => row.index > afterIndex).map(toEvent)
208
+ };
209
+ },
210
+ async putSnapshot(sessionId, reducerName, index, state) {
211
+ const key = `${sessionId}\u0000${reducerName}`;
212
+ const existing = snapshots.get(key);
213
+ if (existing && existing.index >= index) return;
214
+ snapshots.set(key, {
215
+ index,
216
+ state: structuredClone(state),
217
+ updatedAt: clock.now()
218
+ });
219
+ },
220
+ inspect: {
221
+ async listSessions(inspectionOptions) {
222
+ const ids = [...sessions.keys()].filter((sessionId) => sessionId.startsWith(inspectionOptions.prefix) && (inspectionOptions.cursor === void 0 || sessionId > inspectionOptions.cursor)).toSorted();
223
+ const visible = ids.slice(0, inspectionOptions.limit);
224
+ return {
225
+ sessions: visible.map((sessionId) => {
226
+ const rows = sessions.get(sessionId);
227
+ const first = rows[0];
228
+ let updatedAt = first.createdAt;
229
+ let pendingCount = 0;
230
+ let failedCount = 0;
231
+ let attemptCount = 0;
232
+ let failureCount = 0;
233
+ for (const row of rows) {
234
+ attemptCount += row.attemptCount;
235
+ failureCount += row.failureCount;
236
+ if (row.processedAt === null && row.failedAt === null) pendingCount += 1;
237
+ if (row.failedAt !== null) failedCount += 1;
238
+ for (const timestamp of [
239
+ row.createdAt,
240
+ row.firstClaimedAt,
241
+ row.lastClaimedAt,
242
+ row.lastFailedAt,
243
+ row.processedAt,
244
+ row.failedAt
245
+ ]) if (timestamp && timestamp > updatedAt) updatedAt = timestamp;
246
+ }
247
+ return {
248
+ sessionId,
249
+ eventCount: rows.length,
250
+ pendingCount,
251
+ failedCount,
252
+ attemptCount,
253
+ failureCount,
254
+ firstEventAt: new Date(first.createdAt),
255
+ updatedAt: new Date(updatedAt)
256
+ };
257
+ }),
258
+ cursor: ids.length > visible.length ? visible.at(-1) ?? null : null
259
+ };
260
+ },
261
+ async listSnapshots(sessionId) {
262
+ const prefix = `${sessionId}\u0000`;
263
+ return [...snapshots.entries()].filter(([key]) => key.startsWith(prefix)).map(([key, snapshot]) => ({
264
+ reducerName: key.slice(prefix.length),
265
+ index: snapshot.index,
266
+ updatedAt: new Date(snapshot.updatedAt)
267
+ })).toSorted((a, b) => a.reducerName.localeCompare(b.reducerName));
268
+ }
269
+ },
270
+ stream(sessionId, opts) {
271
+ const startAt = opts?.startAt ?? 0;
272
+ return { [Symbol.asyncIterator]() {
273
+ let last = startAt;
274
+ const buffer = [];
275
+ let wake = null;
276
+ let closed = false;
277
+ const onRow = (row) => {
278
+ buffer.push(row);
279
+ wake?.();
280
+ };
281
+ let subs = streamSubscribers.get(sessionId);
282
+ if (!subs) {
283
+ subs = /* @__PURE__ */ new Set();
284
+ streamSubscribers.set(sessionId, subs);
285
+ }
286
+ subs.add(onRow);
287
+ const existing = sessions.get(sessionId) ?? [];
288
+ buffer.unshift(...existing.filter((row) => row.index > startAt));
289
+ const unsubscribe = () => {
290
+ subs.delete(onRow);
291
+ if (subs.size === 0) streamSubscribers.delete(sessionId);
292
+ };
293
+ return {
294
+ async next() {
295
+ for (;;) {
296
+ while (buffer.length > 0) {
297
+ const row = buffer.shift();
298
+ if (row.index <= last) continue;
299
+ last = row.index;
300
+ return {
301
+ value: toStored(row),
302
+ done: false
303
+ };
304
+ }
305
+ if (closed) return {
306
+ value: void 0,
307
+ done: true
308
+ };
309
+ await new Promise((resolve) => {
310
+ wake = resolve;
311
+ });
312
+ wake = null;
313
+ }
314
+ },
315
+ async return() {
316
+ closed = true;
317
+ unsubscribe();
318
+ wake?.();
319
+ return {
320
+ value: void 0,
321
+ done: true
322
+ };
323
+ }
324
+ };
325
+ } };
326
+ },
327
+ lease: {
328
+ async acquire({ sessionId, holder, ttlMs, expiresAtMs }) {
329
+ const now = clock.now().getTime();
330
+ const current = leases.get(sessionId);
331
+ if (current && current.expiresAt > now && current.holder !== holder) return false;
332
+ leases.set(sessionId, {
333
+ holder,
334
+ expiresAt: expiresAtMs ?? now + ttlMs
335
+ });
336
+ return true;
337
+ },
338
+ async release({ sessionId, holder }) {
339
+ if (leases.get(sessionId)?.holder === holder) leases.delete(sessionId);
340
+ }
341
+ }
342
+ };
343
+ }
344
+ //#endregion
345
+ export { memory };
@@ -0,0 +1,83 @@
1
+ import { n as POLL_TIMINGS } from "./internal-Dm8Ejnud.js";
2
+ //#region src/log-polling.ts
3
+ /**
4
+ * The shared polling stream — how poll-based backends (sqlite,
5
+ * postgres) implement `A2Log.stream`.
6
+ *
7
+ * The cadence is activity-adaptive: while events are flowing, polls
8
+ * run at a tight floor (25ms) so streams feel pushed — a token
9
+ * generation reads smoothly instead of clumping at the idle interval;
10
+ * every empty poll doubles the gap until the idle ceiling. The cadence
11
+ * is owned (POLL_TIMINGS in internal.ts), not a public option: the
12
+ * floor already keeps active streams smooth, so tuning it is a cost
13
+ * decision a2 makes, not one it delegates. Correctness never depends
14
+ * on the cadence — each poll is a catch-up read (`index > last`, in
15
+ * order), so nothing can be missed, only arrive a beat later.
16
+ */
17
+ const defaultSleep = (ms) => {
18
+ let timer;
19
+ let wake;
20
+ return {
21
+ promise: new Promise((resolve) => {
22
+ wake = resolve;
23
+ timer = setTimeout(resolve, ms);
24
+ timer.unref?.();
25
+ }),
26
+ cancel: () => {
27
+ clearTimeout(timer);
28
+ wake?.();
29
+ }
30
+ };
31
+ };
32
+ const ceiling = () => POLL_TIMINGS.idleCeilingMs;
33
+ const floor = () => Math.min(POLL_TIMINGS.activeFloorMs, POLL_TIMINGS.idleCeilingMs);
34
+ /**
35
+ * Build the `stream()` iterable from a backend's catch-up read. The
36
+ * iterable ends when the consumer calls `return()` (a disconnecting
37
+ * SSE client), waking any pending sleep immediately.
38
+ */
39
+ function pollingStream(readAfter, options) {
40
+ const startAt = options.startAt ?? 0;
41
+ const sleep = options.sleep ?? defaultSleep;
42
+ return { [Symbol.asyncIterator]() {
43
+ let last = startAt;
44
+ let buffer = [];
45
+ let closed = false;
46
+ let pending = null;
47
+ let interval = floor();
48
+ return {
49
+ async next() {
50
+ for (;;) {
51
+ if (closed) return {
52
+ value: void 0,
53
+ done: true
54
+ };
55
+ if (buffer.length === 0) buffer = await readAfter(last);
56
+ const row = buffer.shift();
57
+ if (row) {
58
+ interval = floor();
59
+ last = row.index;
60
+ return {
61
+ value: row,
62
+ done: false
63
+ };
64
+ }
65
+ pending = sleep(interval);
66
+ await pending.promise;
67
+ pending = null;
68
+ interval = Math.min(interval * 2, ceiling());
69
+ }
70
+ },
71
+ async return() {
72
+ closed = true;
73
+ pending?.cancel();
74
+ return {
75
+ value: void 0,
76
+ done: true
77
+ };
78
+ }
79
+ };
80
+ } };
81
+ }
82
+ //#endregion
83
+ export { pollingStream as t };
@@ -0,0 +1,40 @@
1
+ import { c as IdSource, i as Clock, t as A2Log } from "./log-Dg1I8NRr.js";
2
+ //#region src/log-postgres.d.ts
3
+ /** The result shape this backend reads: just rows. */
4
+ type PostgresQueryResult = {
5
+ rows: Record<string, unknown>[];
6
+ };
7
+ /** One checked-out connection — what transactions run on. */
8
+ type PostgresConnection = {
9
+ query(text: string, values?: unknown[]): Promise<PostgresQueryResult>;
10
+ release(): void;
11
+ };
12
+ /**
13
+ * The minimal client this backend needs. `pg.Pool` matches it
14
+ * structurally; so does PGlite (which has no `connect` — transactions
15
+ * then serialize on an internal mutex, fine for a single-connection
16
+ * client).
17
+ */
18
+ type PostgresClient = {
19
+ query(text: string, values?: unknown[]): Promise<PostgresQueryResult>;
20
+ connect?(): Promise<PostgresConnection>;
21
+ end?(): Promise<void>;
22
+ };
23
+ type PostgresLogOptions = {
24
+ /** Creates a `pg` Pool lazily (requires the optional peer dep `pg`). */
25
+ connectionString?: string | undefined;
26
+ /** Bring your own client — a `pg` Pool, PGlite, a pooler wrapper. */
27
+ client?: PostgresClient;
28
+ /** Injectable clock — every stored timestamp comes from here. */
29
+ clock?: Clock;
30
+ /** Injectable id source for generated event ids. */
31
+ ids?: IdSource;
32
+ };
33
+ type PostgresLog = A2Log & {
34
+ /** End the underlying pool/client (when this backend created it, or
35
+ * the injected client has `end`). */
36
+ close(): Promise<void>;
37
+ };
38
+ declare function postgres(options?: PostgresLogOptions): PostgresLog;
39
+ //#endregion
40
+ export { PostgresClient, PostgresConnection, PostgresLog, PostgresLogOptions, PostgresQueryResult, postgres };