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
@@ -0,0 +1,1405 @@
1
+ import { n as validateSync } from "./validate-XKT4FSNn.js";
2
+ import { n as asStoreUnavailable, t as A2Error } from "./errors-BQuJpe82.js";
3
+ import { g as nullProtoRecord, h as markSchedulerSendFailure, i as POLL_TIMINGS, o as RESERVED_PARTICIPANT_IDS, t as DRAIN_TIMINGS, v as serverInternals, y as serverSchedulerBindings } from "./internal-aEotMzu_.js";
4
+ import { n as InspectionUnsupportedError, r as serverInspection } from "./inspection-DaxB5jM2.js";
5
+ import { t as defaultSleep } from "./store-polling-DgrrAE3d.js";
6
+ import { r as platformWaitUntil, t as invocationDeadlineMs } from "./platform-B4TnJtWu.js";
7
+ import { t as retryableLazy } from "./retryable-lazy-DZWmHpii.js";
8
+ //#region src/deterministic-id.ts
9
+ /**
10
+ * Deterministic default ids for handler `session.append` calls. A handler
11
+ * re-run after a crash calls `session.append` again; giving
12
+ * each call a deterministic id — derived from the triggering event's id
13
+ * and the caller's stable name — makes the re-run a no-op via the store's
14
+ * unique index on event ids.
15
+ *
16
+ * The id is a UUIDv5-shaped SHA-1 over
17
+ * `(namespace, triggerEventId, name, itemOrdinal)`, computed with
18
+ * WebCrypto so it runs on any platform a2 core targets.
19
+ */
20
+ const APPEND_NAMESPACE = "a2:handler-append:v2";
21
+ const RETURN_NAMESPACE = "a2:handler-return:v1";
22
+ const SCHEDULE_NAMESPACE = "a2:schedule:v1";
23
+ const SCHEDULED_EVENT_NAMESPACE = "a2:scheduled-event:v1";
24
+ async function deterministicIdFromInput(input) {
25
+ const digest = await crypto.subtle.digest("SHA-1", new TextEncoder().encode(input));
26
+ const bytes = new Uint8Array(digest).slice(0, 16);
27
+ bytes[6] = (bytes[6] ?? 0) & 15 | 80;
28
+ bytes[8] = (bytes[8] ?? 0) & 63 | 128;
29
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
30
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
31
+ }
32
+ async function deterministicId(parts) {
33
+ return deterministicIdFromInput(parts.join("\0"));
34
+ }
35
+ async function deterministicEventId(triggerEventId, name, itemOrdinal) {
36
+ if (name.length === 0) throw new TypeError("append name must not be empty");
37
+ return deterministicId([
38
+ APPEND_NAMESPACE,
39
+ triggerEventId,
40
+ name,
41
+ itemOrdinal
42
+ ]);
43
+ }
44
+ async function deterministicReturnedEventId(triggerEventId, itemOrdinal) {
45
+ return deterministicId([
46
+ RETURN_NAMESPACE,
47
+ triggerEventId,
48
+ itemOrdinal
49
+ ]);
50
+ }
51
+ async function deterministicScheduleId(contract, sessionId, triggerEventId, name) {
52
+ return deterministicIdFromInput(JSON.stringify([
53
+ SCHEDULE_NAMESPACE,
54
+ contract,
55
+ sessionId,
56
+ triggerEventId,
57
+ name
58
+ ]));
59
+ }
60
+ async function deterministicScheduledEventId(scheduleId, itemOrdinal) {
61
+ return deterministicId([
62
+ SCHEDULED_EVENT_NAMESPACE,
63
+ scheduleId,
64
+ itemOrdinal
65
+ ]);
66
+ }
67
+ //#endregion
68
+ //#region src/telemetry.ts
69
+ const NOOP_HANDLE = {
70
+ setAttribute: () => {},
71
+ recordError: () => {}
72
+ };
73
+ /** The zero-cost default when no telemetry is configured. */
74
+ const NOOP_TELEMETRY = { span: (_name, _attributes, fn) => fn(NOOP_HANDLE) };
75
+ //#endregion
76
+ //#region src/server.ts
77
+ /**
78
+ * Deliver one authenticated scheduler append through an A2 server's ordinary
79
+ * top-level append path. Custom scheduler adapters call this after validating
80
+ * their transport envelope; application code normally uses `session.schedule`.
81
+ */
82
+ async function deliverSchedulerAppend(server, task) {
83
+ if (server.contract.name !== task.contract) throw new TypeError(`a2 scheduler: append task for contract '${task.contract}' cannot be delivered to '${server.contract.name}'`);
84
+ const internals = serverInternals.get(server);
85
+ if (!internals) throw new TypeError(`a2 scheduler: server for contract '${task.contract}' cannot receive scheduled appends`);
86
+ await internals.schedulerAppend(task.sessionId, task.events);
87
+ }
88
+ const MAX_FAILURES = 10;
89
+ /** Default: how long a presence value survives without a refreshing set. */
90
+ const PRESENCE_TTL_MS = 6e4;
91
+ function createDrainSignal() {
92
+ const listeners = /* @__PURE__ */ new Set();
93
+ return {
94
+ version: 0,
95
+ closed: false,
96
+ notify() {
97
+ this.version += 1;
98
+ for (const listener of listeners) listener();
99
+ listeners.clear();
100
+ },
101
+ wait(version) {
102
+ if (this.version !== version) return {
103
+ promise: Promise.resolve(),
104
+ cancel: () => {}
105
+ };
106
+ let resolve;
107
+ const promise = new Promise((done) => {
108
+ resolve = done;
109
+ });
110
+ listeners.add(resolve);
111
+ return {
112
+ promise,
113
+ cancel: () => listeners.delete(resolve)
114
+ };
115
+ }
116
+ };
117
+ }
118
+ function dispatchAbortEvent(hub, event) {
119
+ let relevant = false;
120
+ for (const subscription of hub.subscriptions) {
121
+ if (!subscription.types.has(event.type)) continue;
122
+ relevant = true;
123
+ if (subscription.matches(event)) subscription.controller.abort();
124
+ }
125
+ if (relevant) hub.seen.set(event.index, event);
126
+ }
127
+ function pruneAbortHub(hub) {
128
+ let minimum = Infinity;
129
+ const types = /* @__PURE__ */ new Set();
130
+ for (const subscription of hub.subscriptions) {
131
+ minimum = Math.min(minimum, subscription.triggerIndex);
132
+ for (const type of subscription.types) types.add(type);
133
+ }
134
+ for (const [index, event] of hub.seen) if (index <= minimum || !types.has(event.type)) hub.seen.delete(index);
135
+ }
136
+ /**
137
+ * Deadlines tighten the claim/watchdog window; they never decide whether a
138
+ * handler starts. A platform timeout therefore follows the same path as any
139
+ * other process death: the in-flight event remains pending and is retried.
140
+ */
141
+ function nextClaimWindow() {
142
+ const nowMs = Date.now();
143
+ const deadlineMs = invocationDeadlineMs();
144
+ const deadlineTtlMs = deadlineMs === null ? DRAIN_TIMINGS.claimTtlMs : deadlineMs - nowMs;
145
+ const deadlineCapped = deadlineMs !== null && deadlineTtlMs <= DRAIN_TIMINGS.claimTtlMs;
146
+ const ttlMs = Math.max(1, Math.min(DRAIN_TIMINGS.claimTtlMs, deadlineTtlMs));
147
+ const expiresAtMs = nowMs + ttlMs;
148
+ return {
149
+ ttlMs,
150
+ expiresAtMs,
151
+ watchdogAtMs: expiresAtMs + DRAIN_TIMINGS.watchdogGraceMs,
152
+ deadlineCapped
153
+ };
154
+ }
155
+ function schedulerSlot(dueAt) {
156
+ return Math.ceil(dueAt / 1e3) * 1e3;
157
+ }
158
+ function currentSchedulerSlot(now) {
159
+ return Math.floor(now / 1e3) * 1e3;
160
+ }
161
+ /**
162
+ * The namespace separator between machine name and session id in
163
+ * storage. Machines sharing one store backend (the dev-default sqlite
164
+ * file, a shared Postgres) must not collide on session ids; the machine
165
+ * name is what makes multi-machine apps unambiguous (a2-api.md §1), so
166
+ * it prefixes every storage key.
167
+ */
168
+ const NS = "";
169
+ const devDefaultStore = retryableLazy(() => import("./store-sqlite.js").then((m) => m.sqlite()));
170
+ function environment() {
171
+ const env = typeof process === "undefined" ? void 0 : process.env?.["NODE_ENV"];
172
+ if (env === "test") return "test";
173
+ if (env === "production") return "production";
174
+ return "development";
175
+ }
176
+ function describeError(err) {
177
+ if (err instanceof Error) return err.stack ?? `${err.name}: ${err.message}`;
178
+ return String(err);
179
+ }
180
+ /**
181
+ * Comparison key for a presence value in the degraded-tier diff — an
182
+ * LWW tie can replace a value without moving its `seen`/`at` stamp.
183
+ */
184
+ function fingerprint(value) {
185
+ return JSON.stringify(value) ?? "";
186
+ }
187
+ function foldPresenceRows(rows) {
188
+ const map = nullProtoRecord();
189
+ for (const row of rows) (map[row.participant] ??= nullProtoRecord())[row.field] = {
190
+ value: row.value,
191
+ seen: row.seen,
192
+ at: row.at
193
+ };
194
+ return map;
195
+ }
196
+ /** Implement a contract: bind its vocabulary to storage and reactions. */
197
+ function createServer(options) {
198
+ const serverContract = options?.contract;
199
+ if (serverContract === null || typeof serverContract !== "object" || typeof serverContract.name !== "string" || serverContract.events === null) throw new TypeError("createServer expects options with a contract (see a2.contract)");
200
+ const name = serverContract.name;
201
+ const defs = serverContract.events;
202
+ const presenceDefs = serverContract.presence;
203
+ const declaresPresence = Object.keys(presenceDefs).length > 0;
204
+ const presenceNotSupported = () => new A2Error("PRESENCE_NOT_SUPPORTED", `the contract '${name}' declares presence but the store backend for its server has no presence capability (A2Store.presence)`);
205
+ if (options.presence !== void 0) {
206
+ if (!declaresPresence) throw new TypeError(`the presence option requires a contract that declares presence fields — '${name}' has none`);
207
+ const { ttlMs } = options.presence;
208
+ if (ttlMs !== void 0 && (!Number.isInteger(ttlMs) || ttlMs <= 0)) throw new TypeError("presence.ttlMs must be a positive integer of milliseconds");
209
+ }
210
+ const presenceTtlMs = options.presence?.ttlMs ?? PRESENCE_TTL_MS;
211
+ let makeStore;
212
+ if (options.store) {
213
+ const explicit = options.store;
214
+ if (declaresPresence && !explicit.presence) throw presenceNotSupported();
215
+ makeStore = () => Promise.resolve(explicit);
216
+ } else {
217
+ const env = environment();
218
+ if (env === "production") {
219
+ const error = () => new A2Error("STORE_NOT_CONFIGURED", `the server for '${name}' has no store configured and NODE_ENV is 'production' — pass an explicit store backend (e.g. postgres from 'experimental-a2/store-postgres')`);
220
+ if (process.env["NEXT_PHASE"] !== "phase-production-build") throw error();
221
+ makeStore = () => Promise.reject(error());
222
+ } else makeStore = env === "test" ? () => import("./store-memory.js").then((m) => m.memory()) : devDefaultStore.get;
223
+ }
224
+ if (declaresPresence && !options.store) {
225
+ const inner = makeStore;
226
+ makeStore = async () => {
227
+ const store = await inner();
228
+ if (!store.presence) throw presenceNotSupported();
229
+ return store;
230
+ };
231
+ }
232
+ const resolveStore = retryableLazy(makeStore).get;
233
+ const telemetry = options.telemetry ?? NOOP_TELEMETRY;
234
+ const scheduler = options.scheduler;
235
+ const handlers = /* @__PURE__ */ new Map();
236
+ for (const [type, entry] of Object.entries(options.handlers ?? {})) {
237
+ if (entry === void 0) continue;
238
+ if (!Object.hasOwn(defs, type)) throw new TypeError(`contract '${name}' has no event type '${type}' — cannot register a handler for it`);
239
+ const handler = typeof entry === "function" ? entry : entry?.handler;
240
+ if (typeof handler !== "function") throw new TypeError(`handler for '${type}' must be a function`);
241
+ let abortOn = null;
242
+ const spec = typeof entry === "function" ? void 0 : entry.abortOn;
243
+ if (spec !== void 0) {
244
+ abortOn = /* @__PURE__ */ new Map();
245
+ const pairs = Array.isArray(spec) ? spec.map((abortType) => [abortType, true]) : Object.entries(spec);
246
+ for (const [abortType, matcher] of pairs) {
247
+ if (matcher === void 0) continue;
248
+ if (!Object.hasOwn(defs, abortType)) throw new TypeError(`contract '${name}' has no event type '${String(abortType)}' — cannot abort on it`);
249
+ abortOn.set(String(abortType), matcher);
250
+ }
251
+ if (abortOn.size === 0) abortOn = null;
252
+ }
253
+ const lane = typeof entry === "function" ? null : entry.lane ?? null;
254
+ if (lane !== null && typeof lane !== "function" && (typeof lane !== "string" || lane.length === 0)) throw new TypeError(`lane for '${type}' must be a non-empty string or a function`);
255
+ handlers.set(type, {
256
+ handler,
257
+ abortOn,
258
+ lane
259
+ });
260
+ }
261
+ const prefix = `${name}${NS}`;
262
+ const nsId = (sessionId) => `${prefix}${sessionId}`;
263
+ const stripNs = (stored) => stored.slice(prefix.length);
264
+ const toPublic = (row) => ({
265
+ id: row.id,
266
+ type: row.type,
267
+ payload: row.payload,
268
+ index: row.index,
269
+ sessionId: stripNs(row.sessionId),
270
+ createdAt: row.createdAt
271
+ });
272
+ const inFlight = /* @__PURE__ */ new Set();
273
+ const track = (p) => {
274
+ inFlight.add(p);
275
+ const drop = () => {
276
+ inFlight.delete(p);
277
+ };
278
+ p.then(drop, drop);
279
+ };
280
+ const schedulerArmSlots = /* @__PURE__ */ new Map();
281
+ /**
282
+ * Start an optional scheduler arm without putting it on the execution path.
283
+ * The returned raw promise lets the initial append report failure; the
284
+ * tracked copy is always rejection-safe for heartbeat callers.
285
+ */
286
+ const startSchedulerArm = (sessionId, dueAt) => {
287
+ if (!scheduler) return null;
288
+ const slottedDueAt = schedulerSlot(dueAt);
289
+ const slotKey = JSON.stringify([sessionId, slottedDueAt]);
290
+ const existing = schedulerArmSlots.get(slotKey);
291
+ if (existing) return existing.promise;
292
+ const raw = Promise.resolve().then(() => scheduler.schedule({
293
+ version: 1,
294
+ kind: "drain",
295
+ contract: name,
296
+ sessionId,
297
+ dueAt: slottedDueAt
298
+ }));
299
+ const forget = setTimeout(() => {
300
+ if (schedulerArmSlots.get(slotKey)?.promise === raw) schedulerArmSlots.delete(slotKey);
301
+ }, Math.max(0, slottedDueAt - Date.now()) + 1e3);
302
+ forget.unref?.();
303
+ schedulerArmSlots.set(slotKey, {
304
+ promise: raw,
305
+ forget
306
+ });
307
+ raw.then(void 0, () => {
308
+ const current = schedulerArmSlots.get(slotKey);
309
+ if (current?.promise !== raw) return;
310
+ clearTimeout(current.forget);
311
+ schedulerArmSlots.delete(slotKey);
312
+ });
313
+ const safe = raw.catch(() => {});
314
+ track(safe);
315
+ platformWaitUntil(safe);
316
+ return raw;
317
+ };
318
+ const scheduledDrains = /* @__PURE__ */ new Map();
319
+ const scheduleDrain = (sessionId, watchdogDueAt) => {
320
+ const existing = scheduledDrains.get(sessionId);
321
+ if (existing && !existing.signal.closed) {
322
+ existing.signal.notify();
323
+ return;
324
+ }
325
+ if (existing) scheduledDrains.delete(sessionId);
326
+ const signal = createDrainSignal();
327
+ const p = new Promise((resolve) => {
328
+ queueMicrotask(() => {
329
+ drainSession(sessionId, watchdogDueAt, signal).then(() => resolve()).catch(() => resolve());
330
+ });
331
+ }).finally(() => {
332
+ if (scheduledDrains.get(sessionId)?.promise === p) scheduledDrains.delete(sessionId);
333
+ });
334
+ scheduledDrains.set(sessionId, {
335
+ signal,
336
+ promise: p
337
+ });
338
+ track(p);
339
+ platformWaitUntil(p);
340
+ };
341
+ const validateEvents = (sessionId, events, resolveDispatchMetadata = true) => {
342
+ if (events.length === 0) throw new TypeError("append requires at least one event");
343
+ return events.map((e) => {
344
+ const schema = Object.hasOwn(defs, e.type) ? defs[e.type] : void 0;
345
+ if (!schema) throw new A2Error("UNKNOWN_EVENT_TYPE", `contract '${name}' has no event type '${String(e.type)}'`);
346
+ const result = validateSync(schema, e.payload, `event '${e.type}'`);
347
+ if (result.issues) throw new A2Error("INVALID_PAYLOAD", `invalid payload for event '${e.type}' on machine '${name}'`, { details: result.issues });
348
+ const validated = {
349
+ type: e.type,
350
+ payload: result.value
351
+ };
352
+ if (e.id !== void 0) validated.id = e.id;
353
+ if (!resolveDispatchMetadata) return validated;
354
+ const registration = handlers.get(e.type);
355
+ if (!registration) validated.settled = true;
356
+ else if (registration.lane !== null) {
357
+ const lane = typeof registration.lane === "function" ? registration.lane({
358
+ sessionId,
359
+ event: {
360
+ type: e.type,
361
+ payload: structuredClone(result.value),
362
+ ...e.id === void 0 ? {} : { id: e.id }
363
+ }
364
+ }) : registration.lane;
365
+ if (typeof lane !== "string" || lane.length === 0) throw new TypeError(`lane for '${e.type}' must resolve to a non-empty string`);
366
+ validated.lane = lane;
367
+ }
368
+ return validated;
369
+ });
370
+ };
371
+ /**
372
+ * Validate + write. `onAppended` runs inside the telemetry span, so
373
+ * the drain it schedules is created in the append's active context —
374
+ * with a real tracer, causal work remains connected.
375
+ */
376
+ const appendCore = (sessionId, events, source, mode, onAppended, cause, generatedIds, requireDurableInitialArm = false) => telemetry.span("a2.append", {
377
+ "a2.contract": name,
378
+ "a2.session_id": sessionId,
379
+ "a2.append.source": source,
380
+ "a2.append.mode": mode,
381
+ "a2.append.types": events.map((e) => String(e.type)).join(","),
382
+ "a2.append.count": events.length
383
+ }, async (span) => {
384
+ if (mode === "dispatch" && !scheduler) throw new TypeError("append.dispatch requires a configured scheduler adapter");
385
+ assertSessionId(sessionId);
386
+ const validated = validateEvents(sessionId, events);
387
+ if (cause) for (const [index, event] of validated.entries()) event.cause = generatedIds?.[index] ? {
388
+ ...cause,
389
+ batchSize: events.length
390
+ } : cause;
391
+ const store = await resolveStore();
392
+ let rows;
393
+ let shouldHealSession;
394
+ try {
395
+ const result = await store.append(nsId(sessionId), validated);
396
+ rows = result.events;
397
+ shouldHealSession = result.hasPending;
398
+ } catch (err) {
399
+ throw asStoreUnavailable(err);
400
+ }
401
+ const appended = rows.map(toPublic);
402
+ const initialWatchdogAt = shouldHealSession && scheduler && source === "external" && mode === "inline" ? schedulerSlot(nextClaimWindow().watchdogAtMs) : void 0;
403
+ const initialArm = initialWatchdogAt !== void 0 ? startSchedulerArm(sessionId, initialWatchdogAt) : null;
404
+ const dispatchArm = shouldHealSession && mode === "dispatch" ? startSchedulerArm(sessionId, currentSchedulerSlot(Date.now())) : null;
405
+ if (shouldHealSession && mode === "inline") onAppended(appended, initialWatchdogAt);
406
+ if (dispatchArm) await dispatchArm;
407
+ if (initialArm) {
408
+ let timeout = null;
409
+ try {
410
+ await Promise.race([initialArm, new Promise((_, reject) => {
411
+ timeout = setTimeout(() => {
412
+ reject(/* @__PURE__ */ new Error(`a2 scheduler arm timed out after ${DRAIN_TIMINGS.schedulerArmTimeoutMs}ms`));
413
+ }, DRAIN_TIMINGS.schedulerArmTimeoutMs);
414
+ })]);
415
+ } catch (err) {
416
+ span.setAttribute("a2.append.armed", false);
417
+ span.recordError(err);
418
+ if (requireDurableInitialArm) throw err;
419
+ } finally {
420
+ if (timeout) clearTimeout(timeout);
421
+ }
422
+ }
423
+ return appended;
424
+ });
425
+ const readHistory = async (sessionId, bounds) => {
426
+ const gte = bounds?.gte;
427
+ const lte = bounds?.lte;
428
+ assertHistoryIndex("gte", gte);
429
+ assertHistoryIndex("lte", lte);
430
+ if (gte !== void 0 && lte !== void 0 && gte > lte) throw new RangeError("history.gte must be less than or equal to history.lte");
431
+ if (lte === 0) return [];
432
+ const store = await resolveStore();
433
+ try {
434
+ const readOptions = gte === void 0 && lte === void 0 ? void 0 : {
435
+ ...gte === void 0 ? {} : { afterIndex: Math.max(0, gte - 1) },
436
+ ...lte === void 0 ? {} : { throughIndex: lte }
437
+ };
438
+ return (await store.read(nsId(sessionId), readOptions)).filter((row) => (gte === void 0 || row.index >= gte) && (lte === void 0 || row.index <= lte));
439
+ } catch (err) {
440
+ throw asStoreUnavailable(err);
441
+ }
442
+ };
443
+ const readState = async (sessionId, reducer) => {
444
+ const store = await resolveStore();
445
+ return telemetry.span("a2.state", {
446
+ "a2.contract": name,
447
+ "a2.session_id": sessionId,
448
+ "a2.state.reducer": reducer.name
449
+ }, async (span) => {
450
+ let state = cloneInitial(reducer.initialState);
451
+ let index = 0;
452
+ let snapshotOutcome = "miss";
453
+ let stateRead;
454
+ try {
455
+ stateRead = await store.readState(nsId(sessionId), reducer.name);
456
+ } catch {
457
+ try {
458
+ stateRead = {
459
+ snapshot: null,
460
+ events: await store.read(nsId(sessionId))
461
+ };
462
+ } catch (err) {
463
+ throw asStoreUnavailable(err);
464
+ }
465
+ }
466
+ const snap = stateRead.snapshot;
467
+ let rows = stateRead.events;
468
+ if (snap) {
469
+ if (reducer.stateSchema) {
470
+ const result = validateSync(reducer.stateSchema, snap.state, "the stateSchema");
471
+ if (result.issues) snapshotOutcome = "rejected";
472
+ else {
473
+ state = result.value;
474
+ index = snap.index;
475
+ snapshotOutcome = "hit";
476
+ }
477
+ } else {
478
+ state = snap.state;
479
+ index = snap.index;
480
+ snapshotOutcome = "hit";
481
+ }
482
+ }
483
+ span.setAttribute("a2.state.snapshot", snapshotOutcome);
484
+ if (snapshotOutcome === "rejected") try {
485
+ rows = await store.read(nsId(sessionId));
486
+ } catch (err) {
487
+ throw asStoreUnavailable(err);
488
+ }
489
+ for (const row of rows) {
490
+ state = reducer.fold(state, toPublic(row));
491
+ index = row.index;
492
+ }
493
+ span.setAttribute("a2.state.folded", rows.length);
494
+ span.setAttribute("a2.state.index", index);
495
+ if (rows.length > 0) {
496
+ const snapshotState = cloneInitial(state);
497
+ const write = Promise.resolve().then(() => store.putSnapshot(nsId(sessionId), reducer.name, index, snapshotState)).catch(() => {});
498
+ track(write);
499
+ platformWaitUntil(write);
500
+ }
501
+ return {
502
+ state,
503
+ index
504
+ };
505
+ });
506
+ };
507
+ const makeSchedule = (sessionId, trigger) => {
508
+ const triggerEventId = trigger?.id ?? null;
509
+ const triggerCreatedAt = trigger?.createdAt.getTime();
510
+ return async (scheduleName, timing, ...events) => {
511
+ if (!scheduler) throw new TypeError("session.schedule requires a configured scheduler adapter");
512
+ assertScheduleName(scheduleName);
513
+ if (events.length === 0) throw new TypeError("schedule requires at least one event");
514
+ const dueAt = scheduleDueAt(timing, triggerCreatedAt ?? Date.now());
515
+ const payloads = events.map((event) => cloneScheduledPayload(event.payload));
516
+ const snapshottedEvents = events.map((event, index) => ({
517
+ type: event.type,
518
+ payload: payloads[index],
519
+ ...event.id === void 0 ? {} : { id: event.id }
520
+ }));
521
+ const validated = validateEvents(sessionId, structuredClone(snapshottedEvents), false);
522
+ const scheduleId = await deterministicScheduleId(name, sessionId, triggerEventId, scheduleName);
523
+ const scheduledEvents = await Promise.all(validated.map(async (event, item) => ({
524
+ id: event.id ?? await deterministicScheduledEventId(scheduleId, item),
525
+ type: event.type,
526
+ payload: payloads[item]
527
+ })));
528
+ if (new Set(scheduledEvents.map((event) => event.id)).size !== scheduledEvents.length) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "scheduled batch contains the same event id more than once");
529
+ try {
530
+ await scheduler.schedule({
531
+ version: 1,
532
+ kind: "append",
533
+ id: scheduleId,
534
+ contract: name,
535
+ sessionId,
536
+ dueAt,
537
+ events: scheduledEvents
538
+ });
539
+ } catch (error) {
540
+ throw markSchedulerSendFailure(error);
541
+ }
542
+ };
543
+ };
544
+ const toSessionEvent = (id, event) => ({
545
+ id: event.id,
546
+ type: event.type,
547
+ payload: event.payload,
548
+ index: event.index,
549
+ sessionId: id,
550
+ createdAt: event.createdAt
551
+ });
552
+ const streamEvents = (id, startAfter) => {
553
+ const store = resolveStore();
554
+ const outer = async function* () {
555
+ const inner = (await store).stream(nsId(id), { startAfter });
556
+ for await (const event of inner) yield toSessionEvent(id, event);
557
+ };
558
+ return outer();
559
+ };
560
+ const requirePresence = (store) => {
561
+ if (!store.presence) throw presenceNotSupported();
562
+ return store.presence;
563
+ };
564
+ const setPresence = async (sessionId, patch, fallbackSeen) => {
565
+ if (Reflect.get(patch, "~a2.pushed") === true) {
566
+ Object.freeze(patch.values);
567
+ await options.validatePush?.({
568
+ sessionId,
569
+ events: [],
570
+ presence: Object.freeze(patch)
571
+ });
572
+ }
573
+ if (typeof patch.participant !== "string" || patch.participant.length === 0) throw new TypeError("presence participant must be a non-empty string");
574
+ if (RESERVED_PARTICIPANT_IDS.has(patch.participant)) throw new TypeError(`presence participant must not be '${patch.participant}'`);
575
+ if (patch.at !== void 0 && (typeof patch.at !== "number" || !Number.isFinite(patch.at) || patch.at < 0 || patch.at > 864e13)) throw new TypeError("presence at must be non-negative epoch milliseconds within the Date range");
576
+ const validated = nullProtoRecord();
577
+ for (const [field, value] of Object.entries(patch.values)) {
578
+ if (value === void 0) continue;
579
+ const schema = Object.hasOwn(presenceDefs, field) ? presenceDefs[field] : void 0;
580
+ if (!schema) throw new A2Error("UNKNOWN_PRESENCE_FIELD", `contract '${name}' has no presence field '${field}'`);
581
+ if (value === null) {
582
+ validated[field] = null;
583
+ continue;
584
+ }
585
+ const result = validateSync(schema, value, `presence field '${field}'`);
586
+ if (result.issues) throw new A2Error("INVALID_PAYLOAD", `invalid value for presence field '${field}' on machine '${name}'`, { details: result.issues });
587
+ validated[field] = result.value;
588
+ }
589
+ const store = await resolveStore();
590
+ try {
591
+ await requirePresence(store).set(nsId(sessionId), patch.participant, validated, {
592
+ seen: patch.seen ?? fallbackSeen,
593
+ at: new Date(patch.at ?? Date.now()),
594
+ ttlMs: presenceTtlMs
595
+ });
596
+ } catch (err) {
597
+ throw asStoreUnavailable(err);
598
+ }
599
+ };
600
+ const readPresenceMap = async (sessionId) => {
601
+ const store = await resolveStore();
602
+ let rows;
603
+ try {
604
+ rows = await requirePresence(store).read(nsId(sessionId));
605
+ } catch (err) {
606
+ throw asStoreUnavailable(err);
607
+ }
608
+ return foldPresenceRows(rows);
609
+ };
610
+ /**
611
+ * The two-plane feed. Patches ride `subscribe` when the adapter has
612
+ * the push tier; without it the map is re-read and diffed whenever
613
+ * the event feed wakes, and at the poll idle ceiling while it is
614
+ * silent — degraded means later, not lost while watched.
615
+ */
616
+ const streamWithPresence = (id, startAfter) => {
617
+ const ns = nsId(id);
618
+ const outer = async function* () {
619
+ const store = await resolveStore();
620
+ const api = requirePresence(store);
621
+ const readRows = async () => {
622
+ try {
623
+ return await api.read(ns);
624
+ } catch (err) {
625
+ throw asStoreUnavailable(err);
626
+ }
627
+ };
628
+ const painted = /* @__PURE__ */ new Map();
629
+ const diffRows = (rows) => {
630
+ const patches = [];
631
+ const live = /* @__PURE__ */ new Map();
632
+ for (const row of rows) {
633
+ let liveFields = live.get(row.participant);
634
+ if (!liveFields) {
635
+ liveFields = /* @__PURE__ */ new Set();
636
+ live.set(row.participant, liveFields);
637
+ }
638
+ liveFields.add(row.field);
639
+ const stamp = {
640
+ seen: row.seen,
641
+ atMs: row.at.getTime(),
642
+ value: fingerprint(row.value)
643
+ };
644
+ const prior = painted.get(row.participant)?.get(row.field);
645
+ if (prior && prior.seen === stamp.seen && prior.atMs === stamp.atMs && prior.value === stamp.value) continue;
646
+ let paintedFields = painted.get(row.participant);
647
+ if (!paintedFields) {
648
+ paintedFields = /* @__PURE__ */ new Map();
649
+ painted.set(row.participant, paintedFields);
650
+ }
651
+ paintedFields.set(row.field, stamp);
652
+ patches.push({
653
+ participant: row.participant,
654
+ values: { [row.field]: row.value },
655
+ seen: row.seen,
656
+ at: row.at
657
+ });
658
+ }
659
+ for (const [participant, fields] of painted) {
660
+ for (const [field, prior] of fields) {
661
+ if (live.get(participant)?.has(field)) continue;
662
+ fields.delete(field);
663
+ patches.push({
664
+ participant,
665
+ values: { [field]: null },
666
+ seen: prior.seen,
667
+ at: /* @__PURE__ */ new Date()
668
+ });
669
+ }
670
+ if (fields.size === 0) painted.delete(participant);
671
+ }
672
+ return patches;
673
+ };
674
+ const paint = (patch) => {
675
+ let fields = painted.get(patch.participant);
676
+ for (const [field, value] of Object.entries(patch.values)) {
677
+ if (value === null || value === void 0) {
678
+ fields?.delete(field);
679
+ continue;
680
+ }
681
+ if (!fields) {
682
+ fields = /* @__PURE__ */ new Map();
683
+ painted.set(patch.participant, fields);
684
+ }
685
+ fields.set(field, {
686
+ seen: patch.seen,
687
+ atMs: patch.at.getTime(),
688
+ value: fingerprint(value)
689
+ });
690
+ }
691
+ if (fields?.size === 0) painted.delete(patch.participant);
692
+ };
693
+ const queue = /* @__PURE__ */ new Map();
694
+ let wakeQueue = null;
695
+ const enqueue = (patch) => {
696
+ for (const [field, value] of Object.entries(patch.values)) {
697
+ if (value === void 0) continue;
698
+ let fields = queue.get(patch.participant);
699
+ if (!fields) {
700
+ fields = /* @__PURE__ */ new Map();
701
+ queue.set(patch.participant, fields);
702
+ }
703
+ const queued = fields.get(field);
704
+ if (queued && queued.at.getTime() > patch.at.getTime()) continue;
705
+ fields.set(field, {
706
+ value,
707
+ seen: patch.seen,
708
+ at: patch.at
709
+ });
710
+ }
711
+ wakeQueue?.();
712
+ };
713
+ const dequeue = () => {
714
+ for (const [participant, fields] of queue) {
715
+ for (const [field, stamp] of fields) {
716
+ fields.delete(field);
717
+ if (fields.size === 0) queue.delete(participant);
718
+ return {
719
+ participant,
720
+ values: { [field]: stamp.value },
721
+ seen: stamp.seen,
722
+ at: stamp.at
723
+ };
724
+ }
725
+ queue.delete(participant);
726
+ }
727
+ return null;
728
+ };
729
+ const unsubscribe = api.subscribe?.(ns, enqueue);
730
+ const events = store.stream(ns, { startAfter })[Symbol.asyncIterator]();
731
+ let pendingEvent = null;
732
+ try {
733
+ const snapshotRows = await readRows();
734
+ for (const row of snapshotRows) {
735
+ let fields = painted.get(row.participant);
736
+ if (!fields) {
737
+ fields = /* @__PURE__ */ new Map();
738
+ painted.set(row.participant, fields);
739
+ }
740
+ fields.set(row.field, {
741
+ seen: row.seen,
742
+ atMs: row.at.getTime(),
743
+ value: fingerprint(row.value)
744
+ });
745
+ }
746
+ yield { snapshot: foldPresenceRows(snapshotRows) };
747
+ for (;;) {
748
+ for (;;) {
749
+ const patch = dequeue();
750
+ if (!patch) break;
751
+ paint(patch);
752
+ yield patch;
753
+ }
754
+ pendingEvent ??= events.next();
755
+ let woke;
756
+ if (unsubscribe) {
757
+ const arrival = new Promise((resolve) => {
758
+ wakeQueue = resolve;
759
+ });
760
+ woke = await Promise.race([pendingEvent.then(() => "event"), arrival.then(() => "patch")]);
761
+ wakeQueue = null;
762
+ if (woke === "patch") continue;
763
+ } else {
764
+ const sleeper = defaultSleep(POLL_TIMINGS.idleCeilingMs);
765
+ woke = await Promise.race([pendingEvent.then(() => "event"), sleeper.promise.then(() => "tick")]);
766
+ sleeper.cancel();
767
+ }
768
+ if (woke === "event") {
769
+ const result = await pendingEvent;
770
+ pendingEvent = null;
771
+ if (result.done) return;
772
+ yield toSessionEvent(id, result.value);
773
+ }
774
+ if (!unsubscribe) for (const patch of diffRows(await readRows())) yield patch;
775
+ }
776
+ } finally {
777
+ wakeQueue = null;
778
+ unsubscribe?.();
779
+ pendingEvent?.catch(() => {});
780
+ await events.return?.();
781
+ }
782
+ };
783
+ return outer();
784
+ };
785
+ const makeSession = (id, append, trigger) => ({
786
+ id,
787
+ append,
788
+ schedule: makeSchedule(id, trigger),
789
+ history: async (bounds) => (await readHistory(id, bounds)).map(toPublic),
790
+ state: (reducer) => readState(id, reducer),
791
+ stream: (opts) => {
792
+ const startAfter = opts?.startAfter ?? 0;
793
+ assertStreamIndex(startAfter);
794
+ return opts?.presence === true ? streamWithPresence(id, startAfter) : streamEvents(id, startAfter);
795
+ },
796
+ ...declaresPresence ? {
797
+ setPresence: (patch) => setPresence(id, patch, trigger?.index ?? 0),
798
+ presence: () => readPresenceMap(id)
799
+ } : {}
800
+ });
801
+ const makeCtx = (trigger, attempt, signal) => {
802
+ const publicSessionId = stripNs(trigger.sessionId);
803
+ const append = async (appendName, ...events) => {
804
+ assertAppendName(appendName);
805
+ const generatedIds = events.map((event) => event.id === void 0);
806
+ const withIds = await Promise.all(events.map(async (event, item) => {
807
+ if (event.id !== void 0) return event;
808
+ const id = await deterministicEventId(trigger.id, appendName, item);
809
+ return {
810
+ ...event,
811
+ id
812
+ };
813
+ }));
814
+ return appendCore(publicSessionId, withIds, "handler", "inline", (_rows, watchdogDueAt) => scheduleDrain(publicSessionId, watchdogDueAt), {
815
+ index: trigger.index,
816
+ attempt
817
+ }, generatedIds);
818
+ };
819
+ return {
820
+ event: toPublic(trigger),
821
+ attempt,
822
+ session: makeSession(publicSessionId, append, trigger),
823
+ signal
824
+ };
825
+ };
826
+ const abortHubs = /* @__PURE__ */ new Map();
827
+ const createAbortHub = (store, ns, startAfter) => {
828
+ const hub = {
829
+ subscriptions: /* @__PURE__ */ new Set(),
830
+ seen: /* @__PURE__ */ new Map(),
831
+ startAfter,
832
+ ready: Promise.resolve(),
833
+ watch: null,
834
+ error: null,
835
+ iterator: null,
836
+ stopped: false
837
+ };
838
+ hub.ready = (async () => {
839
+ const existing = await store.read(ns, { afterIndex: startAfter });
840
+ for (const event of existing) dispatchAbortEvent(hub, event);
841
+ const tail = existing.at(-1)?.index ?? startAfter;
842
+ const iterator = store.stream(ns, { startAfter: tail })[Symbol.asyncIterator]();
843
+ hub.iterator = iterator;
844
+ hub.watch = (async () => {
845
+ for (;;) {
846
+ const { value, done } = await iterator.next();
847
+ if (done || value === void 0 || hub.stopped) return;
848
+ dispatchAbortEvent(hub, value);
849
+ }
850
+ })().catch((error) => {
851
+ hub.error = error;
852
+ });
853
+ })().catch((error) => {
854
+ hub.error = error;
855
+ if (abortHubs.get(ns) === hub) abortHubs.delete(ns);
856
+ throw error;
857
+ });
858
+ abortHubs.set(ns, hub);
859
+ return hub;
860
+ };
861
+ const subscribeAbort = async (store, ns, trigger, abortOn, attempt) => {
862
+ let hub = abortHubs.get(ns);
863
+ if (!hub) hub = createAbortHub(store, ns, trigger.index);
864
+ const activeTypes = /* @__PURE__ */ new Set();
865
+ for (const active of hub.subscriptions) for (const type of active.types) activeTypes.add(type);
866
+ const publicTrigger = toPublic(trigger);
867
+ const subscription = {
868
+ controller: new AbortController(),
869
+ triggerIndex: trigger.index,
870
+ types: new Set(abortOn.keys()),
871
+ matches: (event) => {
872
+ if (event.index <= trigger.index) return false;
873
+ const matcher = abortOn.get(event.type);
874
+ if (matcher === void 0) return false;
875
+ return matcher === true ? true : matcher(toPublic(event), publicTrigger, { attempt });
876
+ }
877
+ };
878
+ hub.subscriptions.add(subscription);
879
+ if (trigger.index < hub.startAfter || [...subscription.types].some((type) => !activeTypes.has(type))) {
880
+ const earlier = await store.read(ns, { afterIndex: trigger.index });
881
+ hub.startAfter = Math.min(hub.startAfter, trigger.index);
882
+ for (const event of earlier) dispatchAbortEvent(hub, event);
883
+ }
884
+ for (const event of hub.seen.values()) if (subscription.matches(event)) subscription.controller.abort();
885
+ await hub.ready;
886
+ if (hub.error !== null) throw hub.error;
887
+ return {
888
+ signal: subscription.controller.signal,
889
+ monitor: async (operation) => {
890
+ const outcome = await Promise.race([operation.then((value) => ({
891
+ type: "completed",
892
+ value
893
+ }), (error) => ({
894
+ type: "handler-failed",
895
+ error
896
+ })), hub.watch.then(() => hub.error === null ? { type: "monitor-closed" } : {
897
+ type: "monitor-failed",
898
+ error: hub.error
899
+ })]);
900
+ if (outcome.type === "completed") return outcome.value;
901
+ if (outcome.type === "handler-failed") throw outcome.error;
902
+ if (outcome.type === "monitor-failed") {
903
+ for (const active of hub.subscriptions) active.controller.abort(outcome.error);
904
+ await operation.catch(() => {});
905
+ throw outcome.error;
906
+ }
907
+ await operation.catch(() => {});
908
+ throw new Error("abort monitor closed while handlers were active");
909
+ },
910
+ close: async () => {
911
+ hub.subscriptions.delete(subscription);
912
+ if (hub.subscriptions.size > 0) {
913
+ pruneAbortHub(hub);
914
+ return;
915
+ }
916
+ hub.stopped = true;
917
+ abortHubs.delete(ns);
918
+ await hub.ready;
919
+ await hub.iterator?.return?.();
920
+ await hub.watch;
921
+ }
922
+ };
923
+ };
924
+ const returnedEvents = async (sessionId, trigger, result) => {
925
+ if (result === void 0) return [];
926
+ const events = Array.isArray(result) ? [...result] : [result];
927
+ if (events.length === 0) return [];
928
+ const withIds = await Promise.all(events.map(async (event, item) => {
929
+ const withId = { ...event };
930
+ withId.id = event.id ?? await deterministicReturnedEventId(trigger.id, item);
931
+ return withId;
932
+ }));
933
+ return validateEvents(sessionId, withIds).map((event) => Object.assign(event, {
934
+ id: event.id,
935
+ cause: {
936
+ index: trigger.index,
937
+ attempt: trigger.attemptCount
938
+ }
939
+ }));
940
+ };
941
+ const drainSession = (sessionId, watchdogDueAt, signal) => telemetry.span("a2.drain", {
942
+ "a2.contract": name,
943
+ "a2.session_id": sessionId
944
+ }, async (span) => {
945
+ const store = await resolveStore();
946
+ const ns = nsId(sessionId);
947
+ const holder = crypto.randomUUID();
948
+ const active = /* @__PURE__ */ new Map();
949
+ const excluded = /* @__PURE__ */ new Set();
950
+ let processed = 0;
951
+ let sawFailure = false;
952
+ let schedulerArm;
953
+ let heartbeat = null;
954
+ let lapseTimer = null;
955
+ let retryTimer = null;
956
+ let renewal = null;
957
+ let stopRenewing = false;
958
+ const lapseDue = () => {
959
+ lapseTimer = null;
960
+ const nowMs = Date.now();
961
+ for (const [index, entry] of active) {
962
+ if (entry.lapsed || entry.expiresAtMs > nowMs) continue;
963
+ entry.lapsed = true;
964
+ entry.supersede.abort(new A2Error("CLAIM_EXPIRED", `the claim on event ${index} in session '${sessionId}' lapsed without renewal`));
965
+ }
966
+ scheduleLapse();
967
+ };
968
+ const scheduleLapse = () => {
969
+ if (lapseTimer) {
970
+ clearTimeout(lapseTimer);
971
+ lapseTimer = null;
972
+ }
973
+ let next = Number.POSITIVE_INFINITY;
974
+ for (const entry of active.values()) if (!entry.lapsed) next = Math.min(next, entry.expiresAtMs);
975
+ if (next === Number.POSITIVE_INFINITY) return;
976
+ lapseTimer = setTimeout(lapseDue, Math.max(0, next - Date.now()));
977
+ lapseTimer.unref?.();
978
+ };
979
+ const scheduleRenewRetry = () => {
980
+ if (stopRenewing || retryTimer) return;
981
+ const nowMs = Date.now();
982
+ if (![...active.values()].some((entry) => !entry.lapsed && entry.expiresAtMs > nowMs)) return;
983
+ const delay = DRAIN_TIMINGS.renewRetryMs * (.5 + Math.random() * .5);
984
+ retryTimer = setTimeout(() => {
985
+ retryTimer = null;
986
+ renew();
987
+ }, delay);
988
+ retryTimer.unref?.();
989
+ };
990
+ const alignWatchdogAt = (minimumDueAt) => {
991
+ if (watchdogDueAt === void 0) return minimumDueAt;
992
+ return watchdogDueAt + Math.max(0, Math.ceil((minimumDueAt - watchdogDueAt) / DRAIN_TIMINGS.claimHeartbeatMs)) * DRAIN_TIMINGS.claimHeartbeatMs;
993
+ };
994
+ const requestWatchdog = (dueAt) => {
995
+ const arm = startSchedulerArm(sessionId, alignWatchdogAt(dueAt));
996
+ if (arm) schedulerArm = arm;
997
+ };
998
+ const renew = () => {
999
+ if (stopRenewing || renewal || active.size === 0) return renewal ?? Promise.resolve();
1000
+ const window = nextClaimWindow();
1001
+ requestWatchdog(window.watchdogAtMs);
1002
+ const claims = [...active.entries()].map(([index, entry]) => ({
1003
+ index,
1004
+ attempt: entry.attempt
1005
+ }));
1006
+ const operation = store.renewClaims({
1007
+ sessionId: ns,
1008
+ holder,
1009
+ claims,
1010
+ ttlMs: window.ttlMs,
1011
+ ...window.deadlineCapped ? { expiresAtMs: window.expiresAtMs } : {}
1012
+ }).then((result) => {
1013
+ for (const index of result.renewed) {
1014
+ const entry = active.get(index);
1015
+ if (entry && !entry.lapsed) entry.expiresAtMs = window.expiresAtMs;
1016
+ }
1017
+ for (const index of result.superseded) {
1018
+ const entry = active.get(index);
1019
+ if (!entry) continue;
1020
+ entry.supersede.abort(new A2Error("SUPERSEDED_ATTEMPT", `a newer attempt owns event ${index} in session '${sessionId}'`));
1021
+ entry.fence({
1022
+ status: "superseded",
1023
+ opensLane: false,
1024
+ appendsEvents: false
1025
+ });
1026
+ }
1027
+ if (window.deadlineCapped) stopRenewing = true;
1028
+ scheduleLapse();
1029
+ }, () => {
1030
+ scheduleRenewRetry();
1031
+ });
1032
+ let tracked;
1033
+ tracked = operation.finally(() => {
1034
+ if (renewal === tracked) renewal = null;
1035
+ });
1036
+ renewal = tracked;
1037
+ return tracked;
1038
+ };
1039
+ const runEvent = async (event, supersedeSignal) => {
1040
+ const registration = handlers.get(event.type);
1041
+ return telemetry.span("a2.event", {
1042
+ "a2.contract": name,
1043
+ "a2.session_id": sessionId,
1044
+ "a2.event.type": event.type,
1045
+ "a2.event.index": event.index,
1046
+ "a2.event.id": event.id,
1047
+ "a2.event.attempt": event.attemptCount,
1048
+ "a2.event.handled": registration !== void 0,
1049
+ ...event.lane === null ? {} : { "a2.event.lane": event.lane }
1050
+ }, async (eventSpan) => {
1051
+ let subscription;
1052
+ let handlerSignal = supersedeSignal;
1053
+ let returned;
1054
+ try {
1055
+ if (registration?.abortOn) {
1056
+ subscription = await subscribeAbort(store, ns, event, registration.abortOn, event.attemptCount);
1057
+ handlerSignal = AbortSignal.any([supersedeSignal, subscription.signal]);
1058
+ }
1059
+ const operation = registration?.handler(makeCtx(event, event.attemptCount, handlerSignal));
1060
+ const result = operation ? await (subscription ? subscription.monitor(operation) : operation) : void 0;
1061
+ if (subscription) {
1062
+ await subscription.close();
1063
+ subscription = void 0;
1064
+ }
1065
+ returned = await returnedEvents(sessionId, event, result);
1066
+ } catch (err) {
1067
+ if (supersedeSignal.aborted && err === supersedeSignal.reason) {
1068
+ eventSpan.setAttribute("a2.event.outcome", "surrendered");
1069
+ return {
1070
+ status: "surrendered",
1071
+ opensLane: false,
1072
+ appendsEvents: false
1073
+ };
1074
+ }
1075
+ eventSpan.recordError(err);
1076
+ const failure = await store.failAttempt({
1077
+ sessionId: ns,
1078
+ index: event.index,
1079
+ attempt: event.attemptCount,
1080
+ error: describeError(err),
1081
+ maxFailures: MAX_FAILURES
1082
+ });
1083
+ eventSpan.setAttribute("a2.event.outcome", failure.outcome);
1084
+ return {
1085
+ status: failure.outcome,
1086
+ opensLane: false,
1087
+ appendsEvents: false
1088
+ };
1089
+ } finally {
1090
+ await subscription?.close();
1091
+ }
1092
+ await renewal;
1093
+ const completion = await store.completeAttempt({
1094
+ sessionId: ns,
1095
+ index: event.index,
1096
+ attempt: event.attemptCount,
1097
+ events: returned
1098
+ });
1099
+ eventSpan.setAttribute("a2.event.outcome", completion.outcome === "completed" ? "processed" : completion.outcome);
1100
+ if (completion.outcome === "superseded") return {
1101
+ status: "superseded",
1102
+ opensLane: false,
1103
+ appendsEvents: false
1104
+ };
1105
+ if (handlerSignal.aborted) eventSpan.setAttribute("a2.event.aborted", true);
1106
+ return {
1107
+ status: "processed",
1108
+ opensLane: event.lane !== null,
1109
+ appendsEvents: returned.length > 0
1110
+ };
1111
+ });
1112
+ };
1113
+ let claimAgain = false;
1114
+ const start = (event, expiresAtMs) => {
1115
+ const previous = active.get(event.index);
1116
+ if (previous) {
1117
+ previous.supersede.abort(new A2Error("SUPERSEDED_ATTEMPT", `a newer attempt owns event ${event.index} in session '${sessionId}'`));
1118
+ previous.fence({
1119
+ status: "superseded",
1120
+ opensLane: false,
1121
+ appendsEvents: false
1122
+ });
1123
+ }
1124
+ const supersede = new AbortController();
1125
+ let fence;
1126
+ const fenced = new Promise((resolve) => {
1127
+ fence = resolve;
1128
+ });
1129
+ let entry;
1130
+ const execution = Promise.race([runEvent(event, supersede.signal), fenced]).then((eventOutcome) => {
1131
+ if (active.get(event.index) === entry) active.delete(event.index);
1132
+ if (eventOutcome.status === "processed") processed += 1;
1133
+ if (eventOutcome.status === "failed") {
1134
+ excluded.add(event.index);
1135
+ sawFailure = true;
1136
+ }
1137
+ claimAgain ||= eventOutcome.opensLane || eventOutcome.appendsEvents;
1138
+ return eventOutcome;
1139
+ });
1140
+ entry = {
1141
+ attempt: event.attemptCount,
1142
+ execution,
1143
+ supersede,
1144
+ fence,
1145
+ expiresAtMs,
1146
+ lapsed: false
1147
+ };
1148
+ active.set(event.index, entry);
1149
+ scheduleLapse();
1150
+ if (!heartbeat && !stopRenewing) {
1151
+ heartbeat = setInterval(() => void renew(), DRAIN_TIMINGS.claimHeartbeatMs);
1152
+ heartbeat.unref?.();
1153
+ }
1154
+ };
1155
+ let outcome = "settled";
1156
+ let shouldClaim = true;
1157
+ try {
1158
+ for (;;) {
1159
+ const signalVersion = signal?.version ?? 0;
1160
+ let claim;
1161
+ if (shouldClaim) {
1162
+ const window = nextClaimWindow();
1163
+ claim = await store.claimAvailable({
1164
+ sessionId: ns,
1165
+ holder,
1166
+ ttlMs: window.ttlMs,
1167
+ ...window.deadlineCapped ? { expiresAtMs: window.expiresAtMs } : {},
1168
+ ...excluded.size === 0 ? {} : { excludeIndexes: [...excluded] }
1169
+ });
1170
+ if (claim.outcome === "claimed") {
1171
+ requestWatchdog(window.watchdogAtMs);
1172
+ if (window.deadlineCapped) stopRenewing = true;
1173
+ for (const event of claim.events) start(event, window.expiresAtMs);
1174
+ shouldClaim = false;
1175
+ }
1176
+ }
1177
+ if (active.size > 0) {
1178
+ const wake = signal?.wait(signalVersion);
1179
+ await Promise.race([...[...active.values()].map((entry) => entry.execution), ...wake ? [wake.promise] : []]);
1180
+ wake?.cancel();
1181
+ shouldClaim = claimAgain || active.size === 0 || signal !== void 0 && signal.version !== signalVersion;
1182
+ claimAgain = false;
1183
+ continue;
1184
+ }
1185
+ if (!shouldClaim) {
1186
+ shouldClaim = true;
1187
+ continue;
1188
+ }
1189
+ if (claim === void 0) continue;
1190
+ if (signal && signal.version !== signalVersion) continue;
1191
+ if (claim.outcome === "busy") {
1192
+ requestWatchdog(claim.retryAt.getTime() + DRAIN_TIMINGS.watchdogGraceMs);
1193
+ outcome = "busy";
1194
+ } else if (sawFailure) outcome = "stalled";
1195
+ if (signal) signal.closed = true;
1196
+ break;
1197
+ }
1198
+ } finally {
1199
+ if (signal) signal.closed = true;
1200
+ stopRenewing = true;
1201
+ if (heartbeat) clearInterval(heartbeat);
1202
+ if (retryTimer) clearTimeout(retryTimer);
1203
+ await renewal;
1204
+ await Promise.allSettled([...active.values()].map((entry) => entry.execution));
1205
+ if (lapseTimer) clearTimeout(lapseTimer);
1206
+ }
1207
+ span.setAttribute("a2.drain.processed", processed);
1208
+ span.setAttribute("a2.drain.outcome", outcome);
1209
+ return {
1210
+ settled: outcome === "settled",
1211
+ outcome,
1212
+ processed,
1213
+ ...schedulerArm ? { schedulerArm } : {}
1214
+ };
1215
+ });
1216
+ const appendExternal = async (sessionId, mode, events, requireDurableInitialArm = false) => {
1217
+ const pushed = events.filter((event) => Reflect.get(event, "~a2.pushed") === true);
1218
+ if (pushed.length > 0) await options.validatePush?.({
1219
+ sessionId,
1220
+ events: pushed
1221
+ });
1222
+ return appendCore(sessionId, events, "external", mode, (_rows, watchdogDueAt) => scheduleDrain(sessionId, watchdogDueAt), void 0, void 0, requireDurableInitialArm);
1223
+ };
1224
+ const self = {
1225
+ contract: serverContract,
1226
+ session(id) {
1227
+ assertSessionId(id);
1228
+ const appendInline = async (...events) => appendExternal(id, "inline", events);
1229
+ const appendDispatch = async (...events) => appendExternal(id, "dispatch", events);
1230
+ const append = Object.assign(appendInline, { dispatch: appendDispatch });
1231
+ return makeSession(id, append, null);
1232
+ },
1233
+ async drain(sessionId) {
1234
+ assertSessionId(sessionId);
1235
+ const { settled } = await drainSession(sessionId);
1236
+ return { settled };
1237
+ }
1238
+ };
1239
+ serverInternals.set(self, {
1240
+ settle: async () => {
1241
+ while (inFlight.size > 0) await Promise.allSettled(inFlight);
1242
+ },
1243
+ schedulerDrain: (sessionId, opts) => {
1244
+ assertSessionId(sessionId);
1245
+ return drainSession(sessionId, opts?.watchdogDueAt);
1246
+ },
1247
+ schedulerAppend: async (sessionId, events) => {
1248
+ if (!scheduler) throw new TypeError("scheduled append delivery requires a configured scheduler adapter");
1249
+ assertSessionId(sessionId);
1250
+ await appendExternal(sessionId, "inline", [...events], true);
1251
+ }
1252
+ });
1253
+ serverSchedulerBindings.set(self, { scheduler });
1254
+ serverInspection.set(self, {
1255
+ async listSessions(inspectionOptions) {
1256
+ const store = await resolveStore();
1257
+ if (!store.inspect) throw new InspectionUnsupportedError("this store backend does not support inspection");
1258
+ const page = await store.inspect.listSessions({
1259
+ prefix: `${name}${NS}`,
1260
+ limit: inspectionOptions.limit,
1261
+ ...inspectionOptions.cursor !== void 0 ? { cursor: inspectionOptions.cursor } : {}
1262
+ });
1263
+ return {
1264
+ cursor: page.cursor,
1265
+ sessions: page.sessions.map((session) => {
1266
+ const publicSession = { ...session };
1267
+ publicSession.sessionId = stripNs(session.sessionId);
1268
+ return publicSession;
1269
+ })
1270
+ };
1271
+ },
1272
+ async readSessionPage(sessionId, inspectionOptions) {
1273
+ assertSessionId(sessionId);
1274
+ if (!Number.isSafeInteger(inspectionOptions.afterIndex) || inspectionOptions.afterIndex < 0 || !Number.isSafeInteger(inspectionOptions.limit) || inspectionOptions.limit < 1 || inspectionOptions.throughIndex !== void 0 && (!Number.isSafeInteger(inspectionOptions.throughIndex) || inspectionOptions.throughIndex < 0)) throw new TypeError("invalid inspection event page bounds");
1275
+ const store = await resolveStore();
1276
+ if (!store.inspect) throw new InspectionUnsupportedError("this store backend does not support inspection");
1277
+ const ns = nsId(sessionId);
1278
+ const snapshots = inspectionOptions.afterIndex === 0 ? await store.inspect.listSnapshots(ns) : [];
1279
+ const { events, throughIndex } = store.inspect.readEvents ? await store.inspect.readEvents(ns, inspectionOptions) : await (async () => {
1280
+ const all = await store.read(ns);
1281
+ const currentFrontier = all.at(-1)?.index ?? 0;
1282
+ const throughIndex = Math.min(inspectionOptions.throughIndex ?? currentFrontier, currentFrontier);
1283
+ return {
1284
+ events: all.filter((event) => event.index > inspectionOptions.afterIndex && event.index <= throughIndex).slice(0, inspectionOptions.limit),
1285
+ throughIndex
1286
+ };
1287
+ })();
1288
+ if (!Number.isSafeInteger(throughIndex) || throughIndex < 0 || events.length > inspectionOptions.limit || inspectionOptions.throughIndex !== void 0 && inspectionOptions.afterIndex > 0 && throughIndex !== inspectionOptions.throughIndex) throw new A2Error("STORE_UNAVAILABLE", `inspection event page for session '${sessionId}' has invalid bounds`);
1289
+ for (let position = 0; position < events.length; position += 1) if (events[position].index !== inspectionOptions.afterIndex + position + 1 || events[position].index > throughIndex) throw new A2Error("STORE_UNAVAILABLE", `inspection event page for session '${sessionId}' is not contiguous`);
1290
+ if (inspectionOptions.afterIndex < throughIndex && events.length === 0) throw new A2Error("STORE_UNAVAILABLE", `inspection event page for session '${sessionId}' ended before index ${throughIndex}`);
1291
+ const lastIndex = events.at(-1)?.index ?? inspectionOptions.afterIndex;
1292
+ return {
1293
+ events: events.map((event) => {
1294
+ const publicEvent = { ...event };
1295
+ publicEvent.sessionId = sessionId;
1296
+ return publicEvent;
1297
+ }),
1298
+ snapshots: snapshots.filter((snapshot) => snapshot.index <= throughIndex),
1299
+ throughIndex,
1300
+ nextIndex: lastIndex < throughIndex ? lastIndex : null
1301
+ };
1302
+ }
1303
+ });
1304
+ return self;
1305
+ }
1306
+ function assertSessionId(id) {
1307
+ if (typeof id !== "string" || id.length === 0) throw new TypeError("session id must be a non-empty string");
1308
+ if (id.includes(NS)) throw new TypeError("session id contains a reserved control character");
1309
+ }
1310
+ function assertHistoryIndex(name, value) {
1311
+ if (value !== void 0 && (!Number.isSafeInteger(value) || value < 0)) throw new TypeError(`history.${name} must be a non-negative safe integer`);
1312
+ }
1313
+ function assertStreamIndex(value) {
1314
+ if (!Number.isSafeInteger(value) || value < 0) throw new TypeError("stream.startAfter must be a non-negative safe integer");
1315
+ }
1316
+ function assertAppendName(name) {
1317
+ if (typeof name !== "string" || name.length === 0) throw new TypeError("handler append name must be a non-empty string");
1318
+ }
1319
+ function assertScheduleName(name) {
1320
+ if (typeof name !== "string" || name.length === 0) throw new TypeError("schedule name must be a non-empty string");
1321
+ }
1322
+ const SCHEDULE_DELAY = /^(?:0|[1-9]\d*)(?:\.\d+)?(ms|s|m|h|d)$/;
1323
+ const SCHEDULE_DELAY_MULTIPLIER = {
1324
+ ms: 1,
1325
+ s: 1e3,
1326
+ m: 6e4,
1327
+ h: 36e5,
1328
+ d: 864e5
1329
+ };
1330
+ function scheduleDueAt(timing, baseTime) {
1331
+ if (timing === null || typeof timing !== "object" || Array.isArray(timing)) throw new TypeError("schedule timing must contain exactly one of 'delay' or 'at'");
1332
+ const record = timing;
1333
+ const hasDelay = Object.hasOwn(record, "delay");
1334
+ const hasAt = Object.hasOwn(record, "at");
1335
+ if (hasDelay === hasAt) throw new TypeError("schedule timing must contain exactly one of 'delay' or 'at'");
1336
+ let dueAt;
1337
+ if (hasAt) {
1338
+ const at = record["at"];
1339
+ if (!(at instanceof Date) || !Number.isFinite(at.getTime())) throw new TypeError("schedule at must be a valid Date");
1340
+ dueAt = at.getTime();
1341
+ } else {
1342
+ const delay = record["delay"];
1343
+ if (typeof delay !== "string") throw new TypeError("schedule delay must use ms, s, m, h, or d (for example '30s')");
1344
+ const match = SCHEDULE_DELAY.exec(delay);
1345
+ const amount = match ? Number(delay.slice(0, -match[1].length)) : NaN;
1346
+ if (!match || !Number.isFinite(amount) || amount <= 0) throw new TypeError("schedule delay must be positive and use ms, s, m, h, or d (for example '30s')");
1347
+ dueAt = baseTime + amount * SCHEDULE_DELAY_MULTIPLIER[match[1]];
1348
+ }
1349
+ if (!Number.isFinite(dueAt)) throw new TypeError("schedule due time must be a finite epoch time");
1350
+ return dueAt;
1351
+ }
1352
+ function assertScheduledPayload(payload) {
1353
+ try {
1354
+ if (isJsonValue(payload, /* @__PURE__ */ new WeakSet())) return;
1355
+ } catch {}
1356
+ throw new TypeError("session.schedule event payload must be JSON-serializable");
1357
+ }
1358
+ function cloneScheduledPayload(payload) {
1359
+ assertScheduledPayload(payload);
1360
+ try {
1361
+ const cloned = structuredClone(payload);
1362
+ const encoded = JSON.stringify(cloned);
1363
+ if (encoded === void 0) throw new TypeError();
1364
+ return JSON.parse(encoded);
1365
+ } catch {
1366
+ throw new TypeError("session.schedule event payload must be JSON-serializable");
1367
+ }
1368
+ }
1369
+ function isJsonValue(value, ancestors) {
1370
+ if (value === null) return true;
1371
+ if (typeof value === "string" || typeof value === "boolean") return true;
1372
+ if (typeof value === "number") return Number.isFinite(value) && !Object.is(value, -0);
1373
+ if (typeof value !== "object") return false;
1374
+ if (ancestors.has(value)) return false;
1375
+ ancestors.add(value);
1376
+ try {
1377
+ if (Array.isArray(value)) {
1378
+ const keys = Reflect.ownKeys(value);
1379
+ if (keys.length !== value.length + 1 || !keys.includes("length")) return false;
1380
+ for (let index = 0; index < value.length; index += 1) {
1381
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
1382
+ if (!descriptor || !descriptor.enumerable || !("value" in descriptor) || !isJsonValue(descriptor.value, ancestors)) return false;
1383
+ }
1384
+ return true;
1385
+ }
1386
+ if (Object.getPrototypeOf(value) !== Object.prototype) return false;
1387
+ for (const key of Reflect.ownKeys(value)) {
1388
+ if (typeof key !== "string") return false;
1389
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
1390
+ if (!descriptor || !descriptor.enumerable || !("value" in descriptor) || !isJsonValue(descriptor.value, ancestors)) return false;
1391
+ }
1392
+ return true;
1393
+ } finally {
1394
+ ancestors.delete(value);
1395
+ }
1396
+ }
1397
+ function cloneInitial(initial) {
1398
+ try {
1399
+ return structuredClone(initial);
1400
+ } catch {
1401
+ return initial;
1402
+ }
1403
+ }
1404
+ //#endregion
1405
+ export { deliverSchedulerAppend as n, createServer as t };