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,780 @@
1
+ import { n as validateSync } from "./validate-XKT4FSNn.js";
2
+ import { n as asLogUnavailable, t as A2Error } from "./errors-BJRMd-h6.js";
3
+ import { i as serverInternals, t as DRAIN_TIMINGS } from "./internal-Dm8Ejnud.js";
4
+ import { n as serverInspection } from "./inspection-E7qbD0Xj.js";
5
+ import { t as retryableLazy } from "./retryable-lazy-DZWmHpii.js";
6
+ //#region src/deterministic-id.ts
7
+ /**
8
+ * Deterministic default ids for `ctx.append` (specs/a2-implementation.md
9
+ * §5). A handler re-run after a crash calls `ctx.append` again; giving
10
+ * each call a deterministic id — derived from the triggering event's id
11
+ * and the call's position — makes the re-run a no-op via the log's
12
+ * unique index on event ids.
13
+ *
14
+ * The id is a UUIDv5-shaped SHA-1 over
15
+ * `(namespace, triggerEventId, callOrdinal, itemOrdinal)`, computed with
16
+ * WebCrypto so it runs on any platform a2 core targets.
17
+ */
18
+ const NAMESPACE = "a2:ctx-append:v1";
19
+ async function deterministicEventId(triggerEventId, callOrdinal, itemOrdinal) {
20
+ const input = [
21
+ NAMESPACE,
22
+ triggerEventId,
23
+ callOrdinal,
24
+ itemOrdinal
25
+ ].join("\0");
26
+ const digest = await crypto.subtle.digest("SHA-1", new TextEncoder().encode(input));
27
+ const bytes = new Uint8Array(digest).slice(0, 16);
28
+ bytes[6] = (bytes[6] ?? 0) & 15 | 80;
29
+ bytes[8] = (bytes[8] ?? 0) & 63 | 128;
30
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
31
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
32
+ }
33
+ //#endregion
34
+ //#region src/platform.ts
35
+ const SYMBOL_FOR_REQ_CONTEXT = Symbol.for("@vercel/request-context");
36
+ function requestContext() {
37
+ try {
38
+ return Reflect.get(globalThis, SYMBOL_FOR_REQ_CONTEXT)?.get?.() ?? {};
39
+ } catch {
40
+ return {};
41
+ }
42
+ }
43
+ function platformWaitUntil(promise) {
44
+ try {
45
+ requestContext().waitUntil?.(promise);
46
+ } catch {}
47
+ }
48
+ /** The invocation's termination time in epoch ms, or null if unknowable. */
49
+ function invocationDeadlineMs() {
50
+ const deadline = requestContext().deadline;
51
+ if (deadline === void 0 || deadline === null) return null;
52
+ const ms = new Date(deadline).getTime();
53
+ return Number.isNaN(ms) ? null : ms;
54
+ }
55
+ //#endregion
56
+ //#region src/telemetry.ts
57
+ const NOOP_HANDLE = {
58
+ setAttribute: () => {},
59
+ recordError: () => {}
60
+ };
61
+ /** The zero-cost default when no telemetry is configured. */
62
+ const NOOP_TELEMETRY = { span: (_name, _attributes, fn) => fn(NOOP_HANDLE) };
63
+ //#endregion
64
+ //#region src/server.ts
65
+ const MAX_FAILURES = 10;
66
+ /**
67
+ * Deadlines tighten the lease/watchdog window; they never decide whether a
68
+ * handler starts. A platform timeout therefore follows the same path as any
69
+ * other process death: the in-flight event remains pending and is retried.
70
+ */
71
+ function nextLeaseWindow() {
72
+ const nowMs = Date.now();
73
+ const deadlineMs = invocationDeadlineMs();
74
+ const deadlineTtlMs = deadlineMs === null ? DRAIN_TIMINGS.leaseTtlMs : deadlineMs - nowMs;
75
+ const deadlineCapped = deadlineMs !== null && deadlineTtlMs <= DRAIN_TIMINGS.leaseTtlMs;
76
+ const ttlMs = Math.max(1, Math.min(DRAIN_TIMINGS.leaseTtlMs, deadlineTtlMs));
77
+ const expiresAtMs = nowMs + ttlMs;
78
+ return {
79
+ ttlMs,
80
+ expiresAtMs,
81
+ recoveryAtMs: expiresAtMs + DRAIN_TIMINGS.recoveryGraceMs,
82
+ deadlineCapped
83
+ };
84
+ }
85
+ function recoverySlot(dueAt) {
86
+ return Math.ceil(dueAt / 1e3) * 1e3;
87
+ }
88
+ /**
89
+ * The namespace separator between machine name and session id in
90
+ * storage. Machines sharing one log backend (the dev-default sqlite
91
+ * file, a shared Postgres) must not collide on session ids; the machine
92
+ * name is what makes multi-machine apps unambiguous (a2-api.md §1), so
93
+ * it prefixes every storage key.
94
+ */
95
+ const NS = "";
96
+ const devDefaultLog = retryableLazy(() => import("./log-sqlite.js").then((m) => m.sqlite()));
97
+ function environment() {
98
+ const env = typeof process === "undefined" ? void 0 : process.env?.["NODE_ENV"];
99
+ if (env === "test") return "test";
100
+ if (env === "production") return "production";
101
+ return "development";
102
+ }
103
+ function describeError(err) {
104
+ if (err instanceof Error) return err.stack ?? `${err.name}: ${err.message}`;
105
+ return String(err);
106
+ }
107
+ /** Implement a contract: bind its vocabulary to storage and reactions. */
108
+ function createServer(options) {
109
+ const serverContract = options?.contract;
110
+ 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)");
111
+ const name = serverContract.name;
112
+ const defs = serverContract.events;
113
+ let makeLog;
114
+ if (options.log) {
115
+ const explicit = options.log;
116
+ makeLog = () => Promise.resolve(explicit);
117
+ } else {
118
+ const env = environment();
119
+ if (env === "production") {
120
+ const error = () => new A2Error("LOG_NOT_CONFIGURED", `the server for '${name}' has no log configured and NODE_ENV is 'production' — pass an explicit log backend (e.g. postgres from 'a2/log-postgres')`);
121
+ if (process.env["NEXT_PHASE"] !== "phase-production-build") throw error();
122
+ makeLog = () => Promise.reject(error());
123
+ } else makeLog = env === "test" ? () => import("./log-memory.js").then((m) => m.memory()) : devDefaultLog.get;
124
+ }
125
+ const resolveLog = retryableLazy(makeLog).get;
126
+ const telemetry = options.telemetry ?? NOOP_TELEMETRY;
127
+ const recovery = options.recovery;
128
+ const handlers = /* @__PURE__ */ new Map();
129
+ for (const [type, entry] of Object.entries(options.handlers ?? {})) {
130
+ if (entry === void 0) continue;
131
+ if (!Object.hasOwn(defs, type)) throw new TypeError(`contract '${name}' has no event type '${type}' — cannot register a handler for it`);
132
+ const handler = typeof entry === "function" ? entry : entry?.handler;
133
+ if (typeof handler !== "function") throw new TypeError(`handler for '${type}' must be a function`);
134
+ let abortOn = null;
135
+ const spec = typeof entry === "function" ? void 0 : entry.abortOn;
136
+ if (spec !== void 0) {
137
+ abortOn = /* @__PURE__ */ new Map();
138
+ const pairs = Array.isArray(spec) ? spec.map((abortType) => [abortType, true]) : Object.entries(spec);
139
+ for (const [abortType, matcher] of pairs) {
140
+ if (matcher === void 0) continue;
141
+ if (!Object.hasOwn(defs, abortType)) throw new TypeError(`contract '${name}' has no event type '${String(abortType)}' — cannot abort on it`);
142
+ abortOn.set(String(abortType), matcher);
143
+ }
144
+ if (abortOn.size === 0) abortOn = null;
145
+ }
146
+ handlers.set(type, {
147
+ handler,
148
+ abortOn
149
+ });
150
+ }
151
+ const dormantSignal = new AbortController().signal;
152
+ const prefix = `${name}${NS}`;
153
+ const nsId = (sessionId) => `${prefix}${sessionId}`;
154
+ const stripNs = (stored) => stored.slice(prefix.length);
155
+ const toPublic = (row) => ({
156
+ id: row.id,
157
+ type: row.type,
158
+ payload: row.payload,
159
+ index: row.index,
160
+ sessionId: stripNs(row.sessionId),
161
+ createdAt: row.createdAt
162
+ });
163
+ const inFlight = /* @__PURE__ */ new Set();
164
+ const track = (p) => {
165
+ inFlight.add(p);
166
+ const drop = () => {
167
+ inFlight.delete(p);
168
+ };
169
+ p.then(drop, drop);
170
+ };
171
+ const recoveryArmSlots = /* @__PURE__ */ new Map();
172
+ /**
173
+ * Start an optional recovery arm without putting it on the execution path.
174
+ * The returned raw promise lets the initial append report failure; the
175
+ * tracked copy is always rejection-safe for heartbeat callers.
176
+ */
177
+ const startRecoveryArm = (sessionId, dueAt) => {
178
+ if (!recovery) return null;
179
+ const slottedDueAt = recoverySlot(dueAt);
180
+ const slotKey = JSON.stringify([sessionId, slottedDueAt]);
181
+ const existing = recoveryArmSlots.get(slotKey);
182
+ if (existing) return existing.promise;
183
+ const raw = Promise.resolve().then(() => recovery.arm({
184
+ contract: name,
185
+ sessionId,
186
+ dueAt: slottedDueAt
187
+ }));
188
+ const forget = setTimeout(() => {
189
+ if (recoveryArmSlots.get(slotKey)?.promise === raw) recoveryArmSlots.delete(slotKey);
190
+ }, Math.max(0, slottedDueAt - Date.now()) + 1e3);
191
+ forget.unref?.();
192
+ recoveryArmSlots.set(slotKey, {
193
+ promise: raw,
194
+ forget
195
+ });
196
+ raw.then(void 0, () => {
197
+ const current = recoveryArmSlots.get(slotKey);
198
+ if (current?.promise !== raw) return;
199
+ clearTimeout(current.forget);
200
+ recoveryArmSlots.delete(slotKey);
201
+ });
202
+ const safe = raw.catch(() => {});
203
+ track(safe);
204
+ platformWaitUntil(safe);
205
+ return raw;
206
+ };
207
+ /**
208
+ * Schedule the fire-and-forget inline drain for an append. The drain
209
+ * is tree-scoped: it owns the append's causal tree — everything up to
210
+ * `frontier` (which ordering forces it through anyway) plus whatever
211
+ * its handlers append — and stops there. Later events from other
212
+ * callers belong to those callers' drains.
213
+ */
214
+ const scheduleDrain = (sessionId, frontier, recoveryDueAt) => {
215
+ const p = drainSession(sessionId, {
216
+ full: false,
217
+ frontier
218
+ }, recoveryDueAt).catch(() => {});
219
+ track(p);
220
+ platformWaitUntil(p);
221
+ };
222
+ const inProcessHolds = /* @__PURE__ */ new Map();
223
+ const inProcessWaiters = /* @__PURE__ */ new Map();
224
+ const acquireSlot = async (ns, opts) => {
225
+ if (!opts.wait && inProcessHolds.has(ns)) return null;
226
+ for (;;) {
227
+ const current = inProcessHolds.get(ns);
228
+ if (!current) break;
229
+ inProcessWaiters.set(ns, (inProcessWaiters.get(ns) ?? 0) + 1);
230
+ try {
231
+ await current;
232
+ } finally {
233
+ const waiting = (inProcessWaiters.get(ns) ?? 1) - 1;
234
+ if (waiting === 0) inProcessWaiters.delete(ns);
235
+ else inProcessWaiters.set(ns, waiting);
236
+ }
237
+ }
238
+ let release;
239
+ const done = new Promise((resolve) => {
240
+ release = resolve;
241
+ });
242
+ inProcessHolds.set(ns, done);
243
+ return () => {
244
+ if (inProcessHolds.get(ns) === done) inProcessHolds.delete(ns);
245
+ release();
246
+ };
247
+ };
248
+ const hasInProcessActivity = (ns) => inProcessHolds.has(ns) || (inProcessWaiters.get(ns) ?? 0) > 0;
249
+ const validateEvents = (events) => {
250
+ if (events.length === 0) throw new TypeError("append requires at least one event");
251
+ return events.map((e) => {
252
+ const schema = Object.hasOwn(defs, e.type) ? defs[e.type] : void 0;
253
+ if (!schema) throw new A2Error("UNKNOWN_EVENT_TYPE", `contract '${name}' has no event type '${String(e.type)}'`);
254
+ const result = validateSync(schema, e.payload, `event '${e.type}'`);
255
+ if (result.issues) throw new A2Error("INVALID_PAYLOAD", `invalid payload for event '${e.type}' on machine '${name}'`, { details: result.issues });
256
+ const validated = {
257
+ type: e.type,
258
+ payload: result.value
259
+ };
260
+ if (e.id !== void 0) validated.id = e.id;
261
+ return validated;
262
+ });
263
+ };
264
+ /**
265
+ * Validate + write. `onAppended` runs inside the telemetry span, so
266
+ * the drain it schedules is created in the append's active context —
267
+ * with a real tracer, the whole causal tree lands in one trace.
268
+ */
269
+ const appendCore = (sessionId, events, source, onAppended, cause) => telemetry.span("a2.append", {
270
+ "a2.contract": name,
271
+ "a2.session_id": sessionId,
272
+ "a2.append.source": source,
273
+ "a2.append.types": events.map((e) => String(e.type)).join(","),
274
+ "a2.append.count": events.length
275
+ }, async (span) => {
276
+ assertSessionId(sessionId);
277
+ const validated = validateEvents(events);
278
+ if (cause) for (const event of validated) event.cause = cause;
279
+ const log = await resolveLog();
280
+ let rows;
281
+ try {
282
+ rows = await log.append(nsId(sessionId), validated);
283
+ } catch (err) {
284
+ throw asLogUnavailable(err);
285
+ }
286
+ const appended = rows.map(toPublic);
287
+ const initialRecoveryAt = recovery && source === "external" ? recoverySlot(nextLeaseWindow().recoveryAtMs) : void 0;
288
+ const initialArm = initialRecoveryAt !== void 0 ? startRecoveryArm(sessionId, initialRecoveryAt) : null;
289
+ onAppended(appended, initialRecoveryAt);
290
+ if (initialArm) {
291
+ let timeout = null;
292
+ try {
293
+ await Promise.race([initialArm, new Promise((_, reject) => {
294
+ timeout = setTimeout(() => {
295
+ reject(/* @__PURE__ */ new Error(`a2 recovery arm timed out after ${DRAIN_TIMINGS.recoveryArmTimeoutMs}ms`));
296
+ }, DRAIN_TIMINGS.recoveryArmTimeoutMs);
297
+ })]);
298
+ } catch (err) {
299
+ span.setAttribute("a2.append.armed", false);
300
+ span.recordError(err);
301
+ } finally {
302
+ if (timeout) clearTimeout(timeout);
303
+ }
304
+ }
305
+ return appended;
306
+ });
307
+ const readAll = async (sessionId) => {
308
+ const log = await resolveLog();
309
+ try {
310
+ return await log.read(nsId(sessionId));
311
+ } catch (err) {
312
+ throw asLogUnavailable(err);
313
+ }
314
+ };
315
+ const makeCtx = (trigger, attempt, onCtxAppend, signal) => {
316
+ let callOrdinal = 0;
317
+ const publicSessionId = stripNs(trigger.sessionId);
318
+ return {
319
+ event: toPublic(trigger),
320
+ attempt,
321
+ append: async (...events) => {
322
+ const call = callOrdinal++;
323
+ const withIds = await Promise.all(events.map(async (e, item) => {
324
+ if (e.id !== void 0) return e;
325
+ const id = await deterministicEventId(trigger.id, call, item);
326
+ return {
327
+ ...e,
328
+ id
329
+ };
330
+ }));
331
+ return appendCore(publicSessionId, withIds, "handler", (rows) => {
332
+ const maxIndex = rows.at(-1)?.index ?? 0;
333
+ if (!onCtxAppend(maxIndex)) scheduleDrain(publicSessionId, maxIndex);
334
+ }, {
335
+ index: trigger.index,
336
+ attempt
337
+ });
338
+ },
339
+ history: async () => (await readAll(publicSessionId)).map(toPublic),
340
+ signal
341
+ };
342
+ };
343
+ /**
344
+ * Run a handler registered with `abortOn` (a2-api.md §7): before
345
+ * invoking, check events already appended past the trigger (the
346
+ * abort-landed-before-we-started case); then hold a live subscription
347
+ * for the handler's duration and fire the signal on a match. Closed
348
+ * when the handler settles. Handlers without `abortOn` never get
349
+ * here — no subscription, zero cost.
350
+ */
351
+ const runAbortable = async (log, ns, trigger, attempt, registration, onCtxAppend) => {
352
+ const controller = new AbortController();
353
+ const publicTrigger = toPublic(trigger);
354
+ const matches = (event) => {
355
+ const matcher = registration.abortOn.get(event.type);
356
+ if (matcher === void 0) return false;
357
+ if (matcher === true) return true;
358
+ return matcher(toPublic(event), publicTrigger);
359
+ };
360
+ const already = await log.read(ns, { afterIndex: trigger.index });
361
+ if (already.some((e) => matches(e))) controller.abort();
362
+ let watcher = null;
363
+ let watch = null;
364
+ if (!controller.signal.aborted) {
365
+ const tail = already.at(-1)?.index ?? trigger.index;
366
+ watcher = log.stream(ns, { startAt: tail })[Symbol.asyncIterator]();
367
+ const iterator = watcher;
368
+ watch = (async () => {
369
+ for (;;) {
370
+ const { value, done } = await iterator.next();
371
+ if (done || value === void 0) return;
372
+ if (matches(value)) {
373
+ controller.abort();
374
+ return;
375
+ }
376
+ }
377
+ })().catch(() => {});
378
+ }
379
+ try {
380
+ await registration.handler(makeCtx(trigger, attempt, onCtxAppend, controller.signal));
381
+ } finally {
382
+ await watcher?.return?.();
383
+ await watch;
384
+ }
385
+ return { aborted: controller.signal.aborted };
386
+ };
387
+ const drainHoldingLease = async (log, ns, scope, recoveryDueAt) => {
388
+ let processed = 0;
389
+ const holder = crypto.randomUUID();
390
+ const publicSessionId = stripNs(ns);
391
+ const alignRecoveryAt = (minimumDueAt) => {
392
+ if (recoveryDueAt === void 0) return minimumDueAt;
393
+ return recoveryDueAt + Math.max(0, Math.ceil((minimumDueAt - recoveryDueAt) / DRAIN_TIMINGS.leaseHeartbeatMs)) * DRAIN_TIMINGS.leaseHeartbeatMs;
394
+ };
395
+ let armInFlight = null;
396
+ let pendingRecoveryAt = 0;
397
+ const pumpRecoveryArm = () => {
398
+ if (armInFlight || pendingRecoveryAt === 0) return armInFlight;
399
+ const dueAt = pendingRecoveryAt;
400
+ pendingRecoveryAt = 0;
401
+ const raw = startRecoveryArm(publicSessionId, dueAt);
402
+ if (!raw) return null;
403
+ armInFlight = raw;
404
+ const finished = () => {
405
+ if (armInFlight === raw) armInFlight = null;
406
+ pumpRecoveryArm();
407
+ };
408
+ raw.then(finished, finished);
409
+ return raw;
410
+ };
411
+ const requestRecoveryArm = (dueAt) => {
412
+ pendingRecoveryAt = Math.max(pendingRecoveryAt, dueAt);
413
+ return pumpRecoveryArm();
414
+ };
415
+ let leaseLost = false;
416
+ let stopRenewing = false;
417
+ let renewalInFlight = null;
418
+ let heartbeat = null;
419
+ const stopHeartbeat = () => {
420
+ if (heartbeat) clearInterval(heartbeat);
421
+ heartbeat = null;
422
+ };
423
+ const renewLease = () => {
424
+ if (stopRenewing || leaseLost) return Promise.resolve();
425
+ if (renewalInFlight) return renewalInFlight;
426
+ const window = nextLeaseWindow();
427
+ requestRecoveryArm(alignRecoveryAt(window.recoveryAtMs));
428
+ const renewal = log.lease.acquire({
429
+ sessionId: ns,
430
+ holder,
431
+ ttlMs: window.ttlMs,
432
+ ...window.deadlineCapped ? { expiresAtMs: window.expiresAtMs } : {}
433
+ }).then((renewed) => {
434
+ if (!renewed) leaseLost = true;
435
+ if (window.deadlineCapped) {
436
+ stopRenewing = true;
437
+ stopHeartbeat();
438
+ }
439
+ }, () => {});
440
+ let tracked;
441
+ tracked = renewal.finally(() => {
442
+ if (renewalInFlight === tracked) renewalInFlight = null;
443
+ });
444
+ renewalInFlight = tracked;
445
+ return tracked;
446
+ };
447
+ const initialWindow = nextLeaseWindow();
448
+ const initialClaim = await log.claimNext({
449
+ sessionId: ns,
450
+ holder,
451
+ ttlMs: initialWindow.ttlMs,
452
+ ...initialWindow.deadlineCapped ? { expiresAtMs: initialWindow.expiresAtMs } : {},
453
+ ...scope.full ? {} : { maxIndex: scope.frontier }
454
+ });
455
+ if (initialClaim.outcome === "settled") return {
456
+ outcome: "settled",
457
+ processed
458
+ };
459
+ const initialRecoveryArm = requestRecoveryArm(alignRecoveryAt(initialWindow.recoveryAtMs));
460
+ if (initialClaim.outcome === "busy") return {
461
+ outcome: "busy",
462
+ processed,
463
+ ...initialRecoveryArm ? { recoveryArm: initialRecoveryArm } : {}
464
+ };
465
+ stopRenewing = initialWindow.deadlineCapped;
466
+ if (!stopRenewing) {
467
+ heartbeat = setInterval(() => {
468
+ renewLease();
469
+ }, DRAIN_TIMINGS.leaseHeartbeatMs);
470
+ heartbeat.unref?.();
471
+ }
472
+ let inHold = true;
473
+ const onCtxAppend = (maxIndex) => {
474
+ if (!inHold) return false;
475
+ if (!scope.full && maxIndex > scope.frontier) scope.frontier = maxIndex;
476
+ return true;
477
+ };
478
+ let next = initialClaim.event;
479
+ try {
480
+ for (;;) {
481
+ const registration = handlers.get(next.type);
482
+ const step = await telemetry.span("a2.event", {
483
+ "a2.contract": name,
484
+ "a2.session_id": stripNs(ns),
485
+ "a2.event.type": next.type,
486
+ "a2.event.index": next.index,
487
+ "a2.event.id": next.id,
488
+ "a2.event.attempt": next.attemptCount,
489
+ "a2.event.handled": registration !== void 0
490
+ }, async (span) => {
491
+ try {
492
+ if (registration) try {
493
+ if (registration.abortOn) {
494
+ const { aborted } = await runAbortable(log, ns, next, next.attemptCount, {
495
+ handler: registration.handler,
496
+ abortOn: registration.abortOn
497
+ }, onCtxAppend);
498
+ if (aborted) span.setAttribute("a2.event.aborted", true);
499
+ } else await registration.handler(makeCtx(next, next.attemptCount, onCtxAppend, dormantSignal));
500
+ } catch (err) {
501
+ span.recordError(err);
502
+ const failure = await log.failAttempt({
503
+ sessionId: ns,
504
+ index: next.index,
505
+ attempt: next.attemptCount,
506
+ error: describeError(err),
507
+ maxFailures: MAX_FAILURES
508
+ });
509
+ if (failure.outcome === "superseded") leaseLost = true;
510
+ span.setAttribute("a2.event.outcome", failure.outcome);
511
+ return { outcome: failure.outcome };
512
+ }
513
+ await renewalInFlight;
514
+ const claimed = await log.completeAndClaimNext({
515
+ sessionId: ns,
516
+ holder,
517
+ completedIndex: next.index,
518
+ attempt: next.attemptCount,
519
+ ...scope.full ? {} : { maxIndex: scope.frontier }
520
+ });
521
+ if (claimed.outcome === "superseded") {
522
+ leaseLost = true;
523
+ span.setAttribute("a2.event.outcome", "superseded");
524
+ return { outcome: "superseded" };
525
+ }
526
+ if (claimed.outcome === "claimed") leaseLost = false;
527
+ else if (claimed.outcome === "busy") leaseLost = true;
528
+ span.setAttribute("a2.event.outcome", "processed");
529
+ return {
530
+ outcome: "processed",
531
+ next: claimed
532
+ };
533
+ } finally {
534
+ if (leaseLost) span.setAttribute("a2.event.lease_lost", true);
535
+ }
536
+ });
537
+ if (step.outcome === "dead_lettered") return {
538
+ outcome: "settled",
539
+ processed
540
+ };
541
+ if (step.outcome === "failed") return {
542
+ outcome: "stalled",
543
+ processed
544
+ };
545
+ if (step.outcome === "superseded") return {
546
+ outcome: "busy",
547
+ processed
548
+ };
549
+ processed += 1;
550
+ if (step.next.outcome === "claimed") {
551
+ next = step.next.event;
552
+ continue;
553
+ }
554
+ return {
555
+ outcome: step.next.outcome,
556
+ processed
557
+ };
558
+ }
559
+ } finally {
560
+ inHold = false;
561
+ stopRenewing = true;
562
+ stopHeartbeat();
563
+ await renewalInFlight;
564
+ await log.lease.release({
565
+ sessionId: ns,
566
+ holder
567
+ });
568
+ }
569
+ };
570
+ const drainSession = (sessionId, scope, recoveryDueAt) => telemetry.span("a2.drain", {
571
+ "a2.contract": name,
572
+ "a2.session_id": sessionId,
573
+ "a2.drain.scope": scope.full ? "full" : "tree"
574
+ }, async (span) => {
575
+ const log = await resolveLog();
576
+ const ns = nsId(sessionId);
577
+ const releaseSlot = await acquireSlot(ns, { wait: !scope.full });
578
+ if (!releaseSlot) {
579
+ span.setAttribute("a2.drain.outcome", "busy");
580
+ return {
581
+ settled: false,
582
+ outcome: "busy",
583
+ processed: 0
584
+ };
585
+ }
586
+ let outcome;
587
+ let processed;
588
+ let recoveryArm;
589
+ try {
590
+ ({outcome, processed, recoveryArm} = await drainHoldingLease(log, ns, scope, recoveryDueAt));
591
+ } finally {
592
+ releaseSlot();
593
+ }
594
+ span.setAttribute("a2.drain.processed", processed);
595
+ if (outcome === "busy" || outcome === "stalled") {
596
+ span.setAttribute("a2.drain.outcome", outcome);
597
+ return {
598
+ settled: false,
599
+ outcome,
600
+ processed,
601
+ ...recoveryArm ? { recoveryArm } : {}
602
+ };
603
+ }
604
+ const leftover = await log.read(ns, { unprocessedOnly: true });
605
+ if (leftover.length === 0 || leftover[0]?.failedAt) {
606
+ span.setAttribute("a2.drain.outcome", "settled");
607
+ return {
608
+ settled: true,
609
+ outcome: "settled",
610
+ processed
611
+ };
612
+ }
613
+ if (!hasInProcessActivity(ns)) scheduleDrain(sessionId, leftover.at(-1)?.index ?? 0, recoveryDueAt);
614
+ span.setAttribute("a2.drain.outcome", "handed_off");
615
+ return {
616
+ settled: false,
617
+ outcome: "handed_off",
618
+ processed
619
+ };
620
+ });
621
+ const self = {
622
+ contract: serverContract,
623
+ session(id) {
624
+ assertSessionId(id);
625
+ return {
626
+ sessionId: id,
627
+ append: (...events) => appendCore(id, events, "external", (rows, recoveryDueAt) => {
628
+ scheduleDrain(id, rows.at(-1)?.index ?? 0, recoveryDueAt);
629
+ }),
630
+ history: async () => (await readAll(id)).map(toPublic),
631
+ state: async (reducer) => {
632
+ const log = await resolveLog();
633
+ return telemetry.span("a2.state", {
634
+ "a2.contract": name,
635
+ "a2.session_id": id,
636
+ "a2.state.reducer": reducer.name
637
+ }, async (span) => {
638
+ let state = cloneInitial(reducer.initialState);
639
+ let index = 0;
640
+ let snapshotOutcome = "miss";
641
+ let stateRead;
642
+ try {
643
+ stateRead = await log.readState(nsId(id), reducer.name);
644
+ } catch {
645
+ try {
646
+ stateRead = {
647
+ snapshot: null,
648
+ events: await log.read(nsId(id))
649
+ };
650
+ } catch (err) {
651
+ throw asLogUnavailable(err);
652
+ }
653
+ }
654
+ const snap = stateRead.snapshot;
655
+ let rows = stateRead.events;
656
+ if (snap) {
657
+ if (reducer.stateSchema) {
658
+ const result = validateSync(reducer.stateSchema, snap.state, "the stateSchema");
659
+ if (result.issues) snapshotOutcome = "rejected";
660
+ else {
661
+ state = result.value;
662
+ index = snap.index;
663
+ snapshotOutcome = "hit";
664
+ }
665
+ } else {
666
+ state = snap.state;
667
+ index = snap.index;
668
+ snapshotOutcome = "hit";
669
+ }
670
+ }
671
+ span.setAttribute("a2.state.snapshot", snapshotOutcome);
672
+ if (snapshotOutcome === "rejected") try {
673
+ rows = await log.read(nsId(id));
674
+ } catch (err) {
675
+ throw asLogUnavailable(err);
676
+ }
677
+ for (const row of rows) {
678
+ state = reducer.fold(state, toPublic(row));
679
+ index = row.index;
680
+ }
681
+ span.setAttribute("a2.state.folded", rows.length);
682
+ span.setAttribute("a2.state.index", index);
683
+ if (rows.length > 0) {
684
+ const snapshotState = cloneInitial(state);
685
+ const write = Promise.resolve().then(() => log.putSnapshot(nsId(id), reducer.name, index, snapshotState)).catch(() => {});
686
+ track(write);
687
+ platformWaitUntil(write);
688
+ }
689
+ return {
690
+ state,
691
+ index
692
+ };
693
+ });
694
+ },
695
+ stream: (opts) => {
696
+ const log = resolveLog();
697
+ const startAt = opts?.startAt ?? 0;
698
+ const outer = async function* () {
699
+ const inner = (await log).stream(nsId(id), { startAt });
700
+ for await (const event of inner) yield {
701
+ id: event.id,
702
+ type: event.type,
703
+ payload: event.payload,
704
+ index: event.index,
705
+ sessionId: id,
706
+ createdAt: event.createdAt
707
+ };
708
+ };
709
+ return outer();
710
+ }
711
+ };
712
+ },
713
+ async drain(sessionId) {
714
+ assertSessionId(sessionId);
715
+ const { settled } = await drainSession(sessionId, {
716
+ full: true,
717
+ frontier: 0
718
+ });
719
+ return { settled };
720
+ }
721
+ };
722
+ serverInternals.set(self, {
723
+ settle: async () => {
724
+ while (inFlight.size > 0) await Promise.allSettled(inFlight);
725
+ },
726
+ recoveryDrain: (sessionId, opts) => {
727
+ assertSessionId(sessionId);
728
+ return drainSession(sessionId, {
729
+ full: true,
730
+ frontier: 0
731
+ }, opts?.recoveryDueAt);
732
+ }
733
+ });
734
+ serverInspection.set(self, {
735
+ async listSessions(inspectionOptions) {
736
+ const log = await resolveLog();
737
+ if (!log.inspect) throw new TypeError("this log backend does not support inspection");
738
+ const page = await log.inspect.listSessions({
739
+ prefix: `${name}${NS}`,
740
+ limit: inspectionOptions.limit,
741
+ ...inspectionOptions.cursor !== void 0 ? { cursor: inspectionOptions.cursor } : {}
742
+ });
743
+ return {
744
+ cursor: page.cursor,
745
+ sessions: page.sessions.map((session) => ({
746
+ ...session,
747
+ sessionId: stripNs(session.sessionId)
748
+ }))
749
+ };
750
+ },
751
+ async readSession(sessionId) {
752
+ assertSessionId(sessionId);
753
+ const log = await resolveLog();
754
+ if (!log.inspect) throw new TypeError("this log backend does not support inspection");
755
+ const ns = nsId(sessionId);
756
+ const [events, snapshots] = await Promise.all([log.read(ns), log.inspect.listSnapshots(ns)]);
757
+ return {
758
+ events: events.map((event) => ({
759
+ ...event,
760
+ sessionId
761
+ })),
762
+ snapshots
763
+ };
764
+ }
765
+ });
766
+ return self;
767
+ }
768
+ function assertSessionId(id) {
769
+ if (typeof id !== "string" || id.length === 0) throw new TypeError("session id must be a non-empty string");
770
+ if (id.includes(NS)) throw new TypeError("session id contains a reserved control character");
771
+ }
772
+ function cloneInitial(initial) {
773
+ try {
774
+ return structuredClone(initial);
775
+ } catch {
776
+ return initial;
777
+ }
778
+ }
779
+ //#endregion
780
+ export { createServer as t };