experimental-a2 0.0.0 → 0.2.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 (55) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/ai-server.browser.js +2 -2
  3. package/dist/ai-server.d.ts +19 -7
  4. package/dist/ai-server.js +730 -96
  5. package/dist/ai.d.ts +32 -11
  6. package/dist/ai.js +253 -75
  7. package/dist/client.d.ts +1 -1
  8. package/dist/client.js +4 -4
  9. package/dist/{contract-B0kAXoaL.js → contract-CG_adnu_.js} +2 -1
  10. package/dist/{contract-DL8btVd9.d.ts → contract-C_3dIIEU.d.ts} +4 -1
  11. package/dist/devtools-server.browser.js +2 -2
  12. package/dist/devtools-server.js +1 -1
  13. package/dist/http.d.ts +1 -1
  14. package/dist/http.js +4 -3
  15. package/dist/idempotent-replay-BMyHrP0L.js +19 -0
  16. package/dist/index.d.ts +4 -4
  17. package/dist/index.js +1 -1
  18. package/dist/{internal-Dm8Ejnud.js → internal-D6wNxTck.js} +3 -3
  19. package/dist/{log-Dg1I8NRr.d.ts → log-ldf5g8Cx.d.ts} +74 -56
  20. package/dist/log-memory.d.ts +1 -1
  21. package/dist/log-memory.js +173 -96
  22. package/dist/{log-polling-RO7kclzR.js → log-polling-6COoN60V.js} +1 -1
  23. package/dist/log-postgres.d.ts +1 -1
  24. package/dist/log-postgres.js +235 -192
  25. package/dist/log-redis.d.ts +1 -1
  26. package/dist/log-redis.js +453 -263
  27. package/dist/log-sqlite.d.ts +1 -1
  28. package/dist/log-sqlite.js +216 -127
  29. package/dist/otel.d.ts +1 -1
  30. package/dist/otel.js +1 -1
  31. package/dist/react.d.ts +1 -1
  32. package/dist/react.js +1 -1
  33. package/dist/recovery-vercel.d.ts +2 -2
  34. package/dist/recovery-vercel.js +9 -10
  35. package/dist/server-DJgD2YWP.js +877 -0
  36. package/dist/server.browser.js +4 -4
  37. package/dist/server.d.ts +46 -27
  38. package/dist/server.js +1 -1
  39. package/dist/{telemetry-C78al20p.d.ts → telemetry-Cso0qyHQ.d.ts} +1 -1
  40. package/dist/{wire-2QpU1EtJ.js → wire-BVsgR8o9.js} +1 -1
  41. package/docs/01-quickstart.mdx +7 -7
  42. package/docs/concepts/01-contracts.mdx +22 -22
  43. package/docs/concepts/02-handlers.mdx +223 -89
  44. package/docs/concepts/03-durability.mdx +199 -112
  45. package/docs/concepts/04-state.mdx +27 -1
  46. package/docs/guides/01-timers.mdx +4 -4
  47. package/docs/guides/02-cancellation.mdx +32 -4
  48. package/docs/guides/05-production.mdx +61 -27
  49. package/docs/guides/06-ai-agents.mdx +151 -70
  50. package/docs/guides/07-devtools.mdx +6 -3
  51. package/docs/guides/08-application-data.mdx +5 -6
  52. package/docs/index.mdx +30 -14
  53. package/docs/reference/01-api.mdx +305 -70
  54. package/package.json +31 -31
  55. package/dist/server-DYsnKTTy.js +0 -780
@@ -1,8 +1,9 @@
1
1
  import { t as A2Error } from "./errors-BJRMd-h6.js";
2
+ import { t as idempotentReplay } from "./idempotent-replay-BMyHrP0L.js";
2
3
  import { n as SYSTEM_CLOCK, t as RANDOM_IDS } from "./log-yJbXUf72.js";
3
4
  //#region src/log-memory.ts
4
5
  /**
5
- * a2/log-memory — in-memory log backend (the test default).
6
+ * experimental-a2/log-memory — in-memory log backend (the test default).
6
7
  *
7
8
  * Implements the A2Log interface with zero dependencies. See
8
9
  * specs/a2-implementation.md §3 for the contract; the conformance suite
@@ -16,11 +17,15 @@ const toStored = (row) => ({
16
17
  sessionId: row.sessionId,
17
18
  createdAt: new Date(row.createdAt),
18
19
  cause: row.cause ? { ...row.cause } : null,
20
+ lane: row.lane,
19
21
  processedAt: row.processedAt ? new Date(row.processedAt) : null,
20
22
  processedByAttempt: row.processedByAttempt,
23
+ returnedEventIds: row.returnedEventIds ? [...row.returnedEventIds] : null,
21
24
  firstClaimedAt: row.firstClaimedAt ? new Date(row.firstClaimedAt) : null,
22
25
  lastClaimedAt: row.lastClaimedAt ? new Date(row.lastClaimedAt) : null,
23
26
  attemptCount: row.attemptCount,
27
+ claimHolder: row.claimHolder,
28
+ claimExpiresAt: row.claimExpiresAt ? new Date(row.claimExpiresAt) : null,
24
29
  failureCount: row.failureCount,
25
30
  lastFailedAt: row.lastFailedAt ? new Date(row.lastFailedAt) : null,
26
31
  lastFailedAttempt: row.lastFailedAttempt,
@@ -39,11 +44,9 @@ function memory(options = {}) {
39
44
  const clock = options.clock ?? SYSTEM_CLOCK;
40
45
  const generateId = options.ids ?? RANDOM_IDS;
41
46
  const sessions = /* @__PURE__ */ new Map();
47
+ const dispatch = /* @__PURE__ */ new Map();
42
48
  /** Global unique index on event ids, like the SQL schema's. */
43
49
  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
50
  /** Keyed by `${sessionId}\u0000${reducerName}` — a pure cache. */
48
51
  const snapshots = /* @__PURE__ */ new Map();
49
52
  const streamSubscribers = /* @__PURE__ */ new Map();
@@ -60,117 +63,206 @@ function memory(options = {}) {
60
63
  }
61
64
  return rows;
62
65
  };
66
+ const dispatchOf = (sessionId) => {
67
+ let state = dispatch.get(sessionId);
68
+ if (!state) {
69
+ state = {
70
+ ready: /* @__PURE__ */ new Map(),
71
+ lanes: /* @__PURE__ */ new Map()
72
+ };
73
+ dispatch.set(sessionId, state);
74
+ }
75
+ return state;
76
+ };
77
+ const enqueue = (sessionId, row) => {
78
+ if (row.processedAt !== null) return;
79
+ const state = dispatchOf(sessionId);
80
+ if (row.lane === null) {
81
+ state.ready.set(row.index, row);
82
+ return;
83
+ }
84
+ let queue = state.lanes.get(row.lane);
85
+ if (!queue) {
86
+ queue = {
87
+ rows: [],
88
+ head: 0
89
+ };
90
+ state.lanes.set(row.lane, queue);
91
+ }
92
+ queue.rows.push(row);
93
+ if (queue.rows.length === 1) state.ready.set(row.index, row);
94
+ };
95
+ const settle = (sessionId, row) => {
96
+ const state = dispatchOf(sessionId);
97
+ state.ready.delete(row.index);
98
+ if (row.lane === null) return;
99
+ const queue = state.lanes.get(row.lane);
100
+ if (queue.rows[queue.head] !== row) return;
101
+ queue.head += 1;
102
+ if (queue.head === queue.rows.length) {
103
+ state.lanes.delete(row.lane);
104
+ return;
105
+ }
106
+ const next = queue.rows[queue.head];
107
+ if (next.failedAt === null) state.ready.set(next.index, next);
108
+ };
63
109
  const find = (sessionId, index) => {
64
- const row = sessions.get(sessionId)?.find((candidate) => candidate.index === index);
110
+ const row = sessions.get(sessionId)?.[index - 1];
65
111
  if (!row) throw new TypeError(`no event at index ${index} in session '${sessionId}'`);
66
112
  return row;
67
113
  };
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) => {
114
+ const appendResult = (sessionId, events) => ({
115
+ events,
116
+ hasPending: (sessions.get(sessionId) ?? []).some((row) => row.processedAt === null)
117
+ });
118
+ const claim = (row, holder, now, expiresAt) => {
76
119
  row.attemptCount += 1;
77
120
  row.firstClaimedAt ??= new Date(now);
78
121
  row.lastClaimedAt = new Date(now);
122
+ row.claimHolder = holder;
123
+ row.claimExpiresAt = new Date(expiresAt);
79
124
  return toStored(row);
80
125
  };
126
+ const checkBatchIds = (sessionId, events) => {
127
+ const supplied = events.filter((candidate) => candidate.id !== void 0);
128
+ if (new Set(supplied.map((candidate) => candidate.id)).size !== supplied.length) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "batch contains the same event id more than once");
129
+ const existing = supplied.map((candidate) => byEventId.get(candidate.id)).filter((row) => row !== void 0);
130
+ const foreign = existing.find((row) => row.sessionId !== sessionId);
131
+ if (foreign) throw new A2Error("PARTIAL_DUPLICATE_BATCH", `event id '${foreign.id}' already exists in another session`);
132
+ return existing;
133
+ };
134
+ const insert = (sessionId, events, now, cause) => {
135
+ const rows = rowsOf(sessionId);
136
+ const base = rows.length === 0 ? 0 : rows[rows.length - 1].index;
137
+ const inserted = events.map((candidate, offset) => ({
138
+ id: candidate.id ?? generateId(),
139
+ type: candidate.type,
140
+ payload: structuredClone(candidate.payload),
141
+ index: base + 1 + offset,
142
+ sessionId,
143
+ createdAt: new Date(now),
144
+ cause: cause ? { ...cause } : candidate.cause ? { ...candidate.cause } : null,
145
+ lane: candidate.lane ?? null,
146
+ processedAt: candidate.settled ? new Date(now) : null,
147
+ processedByAttempt: null,
148
+ returnedEventIds: null,
149
+ firstClaimedAt: null,
150
+ lastClaimedAt: null,
151
+ attemptCount: 0,
152
+ claimHolder: null,
153
+ claimExpiresAt: null,
154
+ failureCount: 0,
155
+ lastFailedAt: null,
156
+ lastFailedAttempt: null,
157
+ lastError: null,
158
+ failedAt: null
159
+ }));
160
+ for (const row of inserted) if (byEventId.has(row.id)) throw new A2Error("PARTIAL_DUPLICATE_BATCH", `event id '${row.id}' already exists`);
161
+ for (const row of inserted) {
162
+ rows.push(row);
163
+ byEventId.set(row.id, row);
164
+ enqueue(sessionId, row);
165
+ }
166
+ return inserted;
167
+ };
81
168
  return {
82
169
  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);
170
+ if (events.length === 0) return appendResult(sessionId, []);
171
+ const existing = checkBatchIds(sessionId, events);
87
172
  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);
173
+ if (existing.length === events.length) return appendResult(sessionId, idempotentReplay(events, existing.map(toStored)));
91
174
  throw new A2Error("PARTIAL_DUPLICATE_BATCH", `batch mixes ${existing.length} already-appended and ${events.length - existing.length} fresh events`);
92
175
  }
93
- const rows = rowsOf(sessionId);
94
176
  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
- }
177
+ const inserted = insert(sessionId, events, now);
122
178
  notifyStreams(sessionId, inserted);
123
- return inserted.map(toStored);
179
+ return appendResult(sessionId, inserted.map(toStored));
124
180
  },
125
181
  async read(sessionId, opts) {
126
182
  return (sessions.get(sessionId) ?? []).filter((row) => {
127
183
  if (opts?.afterIndex !== void 0 && row.index <= opts.afterIndex) return false;
128
- if (opts?.unprocessedOnly && row.processedAt !== null) return false;
129
184
  return true;
130
185
  }).map(toStored);
131
186
  },
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" };
187
+ async claimAvailable({ sessionId, holder, ttlMs, expiresAtMs, excludeIndexes }) {
135
188
  const claimedAt = clock.now();
136
189
  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
- });
190
+ const expiresAt = new Date(expiresAtMs ?? now + ttlMs);
191
+ const excluded = new Set(excludeIndexes ?? []);
192
+ const eligible = [];
193
+ let retryAt = null;
194
+ for (const row of dispatchOf(sessionId).ready.values()) {
195
+ if (row.failedAt !== null || excluded.has(row.index)) continue;
196
+ if (row.claimExpiresAt && row.claimExpiresAt.getTime() > now) {
197
+ if (!retryAt || row.claimExpiresAt < retryAt) retryAt = new Date(row.claimExpiresAt);
198
+ continue;
199
+ }
200
+ eligible.push(row);
201
+ }
202
+ if (eligible.length === 0) return retryAt ? {
203
+ outcome: "busy",
204
+ retryAt
205
+ } : { outcome: "settled" };
206
+ eligible.sort((a, b) => a.index - b.index);
143
207
  return {
144
208
  outcome: "claimed",
145
- event: claim(next, claimedAt)
209
+ events: eligible.map((row) => claim(row, holder, claimedAt, expiresAt))
146
210
  };
147
211
  },
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" };
212
+ async renewClaims({ sessionId, holder, indexes, ttlMs, expiresAtMs }) {
213
+ const now = clock.now();
214
+ const expiresAt = new Date(expiresAtMs ?? now.getTime() + ttlMs);
215
+ const requested = new Set(indexes);
216
+ const renewed = [];
217
+ for (const row of sessions.get(sessionId) ?? []) if (requested.has(row.index) && row.processedAt === null && row.failedAt === null && row.claimHolder === holder && row.claimExpiresAt !== null && row.claimExpiresAt > now) {
218
+ row.claimExpiresAt = new Date(expiresAt);
219
+ renewed.push(row.index);
220
+ }
221
+ return renewed;
222
+ },
223
+ async completeAttempt({ sessionId, index, attempt, events }) {
224
+ const parent = find(sessionId, index);
225
+ const cause = {
226
+ index,
227
+ attempt
228
+ };
229
+ if (parent.processedAt !== null) {
230
+ if (parent.processedByAttempt !== attempt) return { outcome: "superseded" };
231
+ const requestedIds = events.map((event) => event.id);
232
+ if (parent.returnedEventIds === null || parent.returnedEventIds.length !== requestedIds.length || parent.returnedEventIds.some((id, offset) => id !== requestedIds[offset])) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "completed attempt does not match the returned event batch");
233
+ const existing = checkBatchIds(sessionId, events);
234
+ if (existing.length !== events.length || existing.some((row) => row.cause?.index !== index || row.cause.attempt !== attempt)) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "completed attempt does not match the returned event batch");
235
+ return {
236
+ outcome: "completed",
237
+ events: idempotentReplay(events, existing.map(toStored))
238
+ };
239
+ }
240
+ if (parent.attemptCount !== attempt || parent.failedAt !== null || parent.claimHolder === null) return { outcome: "superseded" };
241
+ if (checkBatchIds(sessionId, events).length > 0) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "returned event batch contains already-appended ids");
242
+ const now = clock.now();
243
+ const inserted = insert(sessionId, events, now, cause);
244
+ parent.processedAt = new Date(now);
245
+ parent.processedByAttempt = attempt;
246
+ parent.returnedEventIds = events.map((event) => event.id);
247
+ parent.claimHolder = null;
248
+ parent.claimExpiresAt = null;
249
+ settle(sessionId, parent);
250
+ notifyStreams(sessionId, inserted);
159
251
  return {
160
- outcome: "claimed",
161
- event: claim(next, claimedAt)
252
+ outcome: "completed",
253
+ events: inserted.map(toStored)
162
254
  };
163
255
  },
164
- async markProcessed(sessionId, index) {
165
- const row = find(sessionId, index);
166
- row.processedAt ??= clock.now();
167
- },
168
256
  async failAttempt({ sessionId, index, attempt, error, maxFailures }) {
169
257
  const row = find(sessionId, index);
170
- if (row.processedAt || row.attemptCount !== attempt) return {
258
+ if (row.processedAt || row.attemptCount !== attempt || row.claimHolder === null && row.lastFailedAttempt !== attempt) return {
171
259
  outcome: "superseded",
172
260
  failureCount: row.failureCount
173
261
  };
262
+ if (row.lastFailedAttempt === attempt && row.claimHolder === null) return {
263
+ outcome: row.failedAt ? "dead_lettered" : "failed",
264
+ failureCount: row.failureCount
265
+ };
174
266
  if (row.failedAt) return {
175
267
  outcome: "dead_lettered",
176
268
  failureCount: row.failureCount
@@ -180,8 +272,11 @@ function memory(options = {}) {
180
272
  const failedAt = clock.now();
181
273
  row.lastFailedAt = new Date(failedAt);
182
274
  row.lastFailedAttempt = attempt;
275
+ row.claimHolder = null;
276
+ row.claimExpiresAt = null;
183
277
  if (row.failureCount >= maxFailures) {
184
278
  row.failedAt = new Date(failedAt);
279
+ dispatchOf(sessionId).ready.delete(row.index);
185
280
  return {
186
281
  outcome: "dead_lettered",
187
282
  failureCount: row.failureCount
@@ -192,9 +287,6 @@ function memory(options = {}) {
192
287
  failureCount: row.failureCount
193
288
  };
194
289
  },
195
- async markFailed(sessionId, index) {
196
- find(sessionId, index).failedAt = clock.now();
197
- },
198
290
  async readState(sessionId, reducerName) {
199
291
  const snap = snapshots.get(`${sessionId}\u0000${reducerName}`);
200
292
  const snapshot = snap ? {
@@ -323,21 +415,6 @@ function memory(options = {}) {
323
415
  }
324
416
  };
325
417
  } };
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
418
  }
342
419
  };
343
420
  }
@@ -1,4 +1,4 @@
1
- import { n as POLL_TIMINGS } from "./internal-Dm8Ejnud.js";
1
+ import { n as POLL_TIMINGS } from "./internal-D6wNxTck.js";
2
2
  //#region src/log-polling.ts
3
3
  /**
4
4
  * The shared polling stream — how poll-based backends (sqlite,
@@ -1,4 +1,4 @@
1
- import { c as IdSource, i as Clock, t as A2Log } from "./log-Dg1I8NRr.js";
1
+ import { c as IdSource, i as Clock, t as A2Log } from "./log-ldf5g8Cx.js";
2
2
  //#region src/log-postgres.d.ts
3
3
  /** The result shape this backend reads: just rows. */
4
4
  type PostgresQueryResult = {