experimental-a2 0.6.0 → 0.8.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 (148) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/ai-server.d.ts +4 -5
  3. package/dist/ai-server.d.ts.map +1 -1
  4. package/dist/ai-server.js +20 -17
  5. package/dist/ai-server.js.map +1 -1
  6. package/dist/ai.d.ts +334 -2
  7. package/dist/ai.d.ts.map +1 -0
  8. package/dist/ai.js +1 -1
  9. package/dist/client.d.ts +202 -2
  10. package/dist/client.d.ts.map +1 -0
  11. package/dist/client.js +1025 -1
  12. package/dist/client.js.map +1 -0
  13. package/dist/errors-BQuJpe82.js.map +1 -1
  14. package/dist/index.d.ts +22 -3
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/{internal-DstsI6Re.js → internal-DRXJ56EI.js} +5 -28
  17. package/dist/internal-DRXJ56EI.js.map +1 -0
  18. package/dist/react.d.ts +1 -1
  19. package/dist/react.js +1 -1
  20. package/dist/scheduler-qstash.d.ts +3 -3
  21. package/dist/scheduler-qstash.js +4 -5
  22. package/dist/scheduler-qstash.js.map +1 -1
  23. package/dist/scheduler-vercel.d.ts +2 -2
  24. package/dist/scheduler-vercel.js +4 -4
  25. package/dist/scheduler-vercel.js.map +1 -1
  26. package/dist/{server-Duw6MVlB.js → server-B2XNevQA.js} +830 -131
  27. package/dist/server-B2XNevQA.js.map +1 -0
  28. package/dist/{server-DpvjhdoE.d.ts → server-DjPhHnbI.d.ts} +71 -50
  29. package/dist/server-DjPhHnbI.d.ts.map +1 -0
  30. package/dist/server.d.ts +3 -3
  31. package/dist/server.js +1 -1
  32. package/dist/store-N8PXxDAS.js.map +1 -1
  33. package/dist/{store-DysUkTH3.d.ts → store-RJO35BMj.d.ts} +24 -62
  34. package/dist/store-RJO35BMj.d.ts.map +1 -0
  35. package/dist/store-memory.d.ts +1 -1
  36. package/dist/store-memory.d.ts.map +1 -1
  37. package/dist/store-memory.js +80 -78
  38. package/dist/store-memory.js.map +1 -1
  39. package/dist/{store-polling-dSeLxzfb.js → store-polling-6DW7F1DT.js} +2 -2
  40. package/dist/{store-polling-dSeLxzfb.js.map → store-polling-6DW7F1DT.js.map} +1 -1
  41. package/dist/store-postgres.d.ts +1 -1
  42. package/dist/store-postgres.d.ts.map +1 -1
  43. package/dist/store-postgres.js +231 -182
  44. package/dist/store-postgres.js.map +1 -1
  45. package/dist/{store-redis-core-BFLwz0Wj.js → store-redis-core-DT01r4GZ.js} +213 -161
  46. package/dist/store-redis-core-DT01r4GZ.js.map +1 -0
  47. package/dist/store-redis-http.d.ts +1 -1
  48. package/dist/store-redis-http.js +3 -4
  49. package/dist/store-redis-http.js.map +1 -1
  50. package/dist/store-redis.d.ts +1 -1
  51. package/dist/store-redis.js +4 -5
  52. package/dist/store-redis.js.map +1 -1
  53. package/dist/store-sqlite.d.ts +1 -1
  54. package/dist/store-sqlite.d.ts.map +1 -1
  55. package/dist/store-sqlite.js +104 -91
  56. package/dist/store-sqlite.js.map +1 -1
  57. package/dist/{wire-BFQmSJ-9.js → wire-B6te_wns.js} +4 -3
  58. package/dist/wire-B6te_wns.js.map +1 -0
  59. package/docs/concepts/02-handlers.mdx +4 -0
  60. package/docs/concepts/04-state.mdx +57 -9
  61. package/docs/guides/03-react.mdx +20 -28
  62. package/docs/guides/05-production.mdx +9 -11
  63. package/docs/guides/06-ai-agents.mdx +10 -15
  64. package/docs/guides/09-presence.mdx +14 -19
  65. package/docs/guides/10-transports.mdx +104 -86
  66. package/docs/reference/01-api.mdx +182 -293
  67. package/docs/reference/02-errors.mdx +5 -7
  68. package/package.json +1 -14
  69. package/src/ai-server.ts +36 -15
  70. package/src/client.ts +2 -2
  71. package/src/errors.ts +1 -0
  72. package/src/internal.ts +3 -62
  73. package/src/push-envelope.ts +24 -21
  74. package/src/scheduler-qstash.ts +3 -3
  75. package/src/scheduler-vercel.ts +2 -2
  76. package/src/server-fetch.ts +344 -0
  77. package/src/server.ts +315 -312
  78. package/src/session-socket.ts +36 -20
  79. package/src/sse.ts +2 -2
  80. package/src/store-memory.ts +138 -101
  81. package/src/store-postgres.ts +355 -238
  82. package/src/store-redis-core.ts +247 -237
  83. package/src/store-redis-http.ts +1 -2
  84. package/src/store-redis.ts +1 -2
  85. package/src/store-sqlite.ts +191 -153
  86. package/src/store.ts +24 -66
  87. package/src/wire.ts +2 -1
  88. package/dist/ai-D_PGS-JR.d.ts +0 -334
  89. package/dist/ai-D_PGS-JR.d.ts.map +0 -1
  90. package/dist/cli-B3VuxoDe.js +0 -599
  91. package/dist/cli-B3VuxoDe.js.map +0 -1
  92. package/dist/cli-bin.d.ts +0 -1
  93. package/dist/cli-bin.js +0 -7
  94. package/dist/cli-bin.js.map +0 -1
  95. package/dist/cli.d.ts +0 -20
  96. package/dist/cli.d.ts.map +0 -1
  97. package/dist/cli.js +0 -2
  98. package/dist/client-CdMqi7mC.d.ts +0 -202
  99. package/dist/client-CdMqi7mC.d.ts.map +0 -1
  100. package/dist/client-Dj5d3SP_.js +0 -1026
  101. package/dist/client-Dj5d3SP_.js.map +0 -1
  102. package/dist/devtools-J_jZ2vQf.d.ts +0 -152
  103. package/dist/devtools-J_jZ2vQf.d.ts.map +0 -1
  104. package/dist/devtools-kJJaORn-.js +0 -340
  105. package/dist/devtools-kJJaORn-.js.map +0 -1
  106. package/dist/devtools-server.browser.d.ts +0 -1
  107. package/dist/devtools-server.browser.js +0 -6
  108. package/dist/devtools-server.browser.js.map +0 -1
  109. package/dist/devtools-server.d.ts +0 -23
  110. package/dist/devtools-server.d.ts.map +0 -1
  111. package/dist/devtools-server.js +0 -1270
  112. package/dist/devtools-server.js.map +0 -1
  113. package/dist/devtools.d.ts +0 -2
  114. package/dist/devtools.js +0 -2
  115. package/dist/errors-W6nwJ-fm.d.ts +0 -21
  116. package/dist/errors-W6nwJ-fm.d.ts.map +0 -1
  117. package/dist/http.d.ts +0 -151
  118. package/dist/http.d.ts.map +0 -1
  119. package/dist/http.js +0 -706
  120. package/dist/http.js.map +0 -1
  121. package/dist/inspection-DaxB5jM2.js +0 -13
  122. package/dist/inspection-DaxB5jM2.js.map +0 -1
  123. package/dist/internal-DstsI6Re.js.map +0 -1
  124. package/dist/platform-B4TnJtWu.js +0 -34
  125. package/dist/platform-B4TnJtWu.js.map +0 -1
  126. package/dist/server-DpvjhdoE.d.ts.map +0 -1
  127. package/dist/server-Duw6MVlB.js.map +0 -1
  128. package/dist/store-DysUkTH3.d.ts.map +0 -1
  129. package/dist/store-redis-core-BFLwz0Wj.js.map +0 -1
  130. package/dist/testing.browser.d.ts +0 -1
  131. package/dist/testing.browser.js +0 -6
  132. package/dist/testing.browser.js.map +0 -1
  133. package/dist/testing.d.ts +0 -32
  134. package/dist/testing.d.ts.map +0 -1
  135. package/dist/testing.js +0 -103
  136. package/dist/testing.js.map +0 -1
  137. package/dist/wire-BFQmSJ-9.js.map +0 -1
  138. package/docs/guides/07-devtools.mdx +0 -229
  139. package/src/cli-bin.ts +0 -5
  140. package/src/cli.ts +0 -1046
  141. package/src/devtools-app.ts +0 -989
  142. package/src/devtools-server.browser.ts +0 -5
  143. package/src/devtools-server.ts +0 -604
  144. package/src/devtools.ts +0 -716
  145. package/src/http.ts +0 -394
  146. package/src/inspection.ts +0 -39
  147. package/src/testing.browser.ts +0 -5
  148. package/src/testing.ts +0 -185
@@ -1,10 +1,687 @@
1
1
  import { n as validateSync } from "./validate-XKT4FSNn.js";
2
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-DstsI6Re.js";
4
- import { n as InspectionUnsupportedError, r as serverInspection } from "./inspection-DaxB5jM2.js";
5
- import { t as defaultSleep } from "./store-polling-dSeLxzfb.js";
6
- import { r as platformWaitUntil, t as invocationDeadlineMs } from "./platform-B4TnJtWu.js";
3
+ import { c as STREAM_TIMINGS, g as serverInternals, h as nullProtoRecord, i as POLL_TIMINGS, m as markSchedulerSendFailure, o as RESERVED_PARTICIPANT_IDS, s as SOCKET_TIMINGS, t as DRAIN_TIMINGS } from "./internal-DRXJ56EI.js";
4
+ import { t as defaultSleep } from "./store-polling-6DW7F1DT.js";
5
+ import { _ as socketErrorAckFor, a as errorToWire, b as socketUnsubscribedFor, g as socketAckFor, h as presenceSnapshotToWire, i as errorStatus, n as asA2Error, p as presencePatchToWire, s as eventToWire, t as SOCKET_PING_FRAME, v as socketFrameFor, y as socketSubscribedFor } from "./wire-B6te_wns.js";
7
6
  import { t as retryableLazy } from "./retryable-lazy-DZWmHpii.js";
7
+ //#region src/platform.ts
8
+ const SYMBOL_FOR_REQ_CONTEXT = Symbol.for("@vercel/request-context");
9
+ function requestContext() {
10
+ try {
11
+ return Reflect.get(globalThis, SYMBOL_FOR_REQ_CONTEXT)?.get?.() ?? {};
12
+ } catch {
13
+ return {};
14
+ }
15
+ }
16
+ /** The current invocation's Vercel deployment identity, when available. */
17
+ function platformVercelOidcToken() {
18
+ try {
19
+ const token = requestContext().headers?.["x-vercel-oidc-token"];
20
+ return typeof token === "string" && token.length > 0 ? token : null;
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+ function platformWaitUntil(promise) {
26
+ try {
27
+ requestContext().waitUntil?.(promise);
28
+ } catch {}
29
+ }
30
+ /** The invocation's termination time in epoch ms, or null if unknowable. */
31
+ function invocationDeadlineMs() {
32
+ const deadline = requestContext().deadline;
33
+ if (deadline === void 0 || deadline === null) return null;
34
+ const ms = new Date(deadline).getTime();
35
+ return Number.isNaN(ms) ? null : ms;
36
+ }
37
+ //#endregion
38
+ //#region src/push-envelope.ts
39
+ /**
40
+ * The push envelope's trust boundary for `{ sessionId, events, presence? }`,
41
+ * shared verbatim by the HTTP
42
+ * POST lane and the socket's push/presence frames so INVALID_PAYLOAD
43
+ * shapes are one implementation. Internal module, not public API.
44
+ */
45
+ const invalidPushBody = (message) => new A2Error("INVALID_PAYLOAD", `malformed push body: ${message}`);
46
+ const invalid = invalidPushBody;
47
+ function parsePresenceSibling(value) {
48
+ if (value === void 0) return void 0;
49
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw invalid("presence must be an object when present");
50
+ const { participant, values, seen, at } = value;
51
+ if (typeof participant !== "string" || participant.length === 0) throw invalid("presence.participant must be a non-empty string");
52
+ if (RESERVED_PARTICIPANT_IDS.has(participant)) throw invalid(`presence.participant must not be '${participant}'`);
53
+ if (values === null || typeof values !== "object" || Array.isArray(values)) throw invalid("presence.values must be an object");
54
+ if (seen !== void 0 && (typeof seen !== "number" || !Number.isFinite(seen) || seen < 0)) throw invalid("presence.seen must be a non-negative number when present");
55
+ if (at !== void 0 && (typeof at !== "number" || !Number.isFinite(at) || at < 0 || at > 864e13)) throw invalid("presence.at must be a non-negative epoch-milliseconds timestamp within the Date range when present");
56
+ const out = {
57
+ participant,
58
+ values
59
+ };
60
+ if (seen !== void 0) out.seen = seen;
61
+ if (at !== void 0) out.at = at;
62
+ return out;
63
+ }
64
+ /** The events half of the envelope, shared by the POST body and socket
65
+ * `push` frames so INVALID_PAYLOAD shapes have one implementation. */
66
+ function parsePushEvents(events) {
67
+ if (!Array.isArray(events) || events.length === 0) throw invalid("events must be a non-empty array");
68
+ return events.map((event, i) => {
69
+ if (event === null || typeof event !== "object") throw invalid(`events[${i}] must be an object`);
70
+ const { type, payload, id } = event;
71
+ if (typeof type !== "string" || type.length === 0) throw invalid(`events[${i}].type must be a non-empty string`);
72
+ if (id !== void 0 && typeof id !== "string") throw invalid(`events[${i}].id must be a string when present`);
73
+ const out = {
74
+ type,
75
+ payload
76
+ };
77
+ if (id !== void 0) out.id = id;
78
+ return out;
79
+ });
80
+ }
81
+ /**
82
+ * Validate the push envelope — `{ sessionId, events, presence? }` —
83
+ * throwing `INVALID_PAYLOAD` on a malformed body. Payload validation
84
+ * against the machine's schemas happens in `append` and `setPresence`,
85
+ * not here. Either plane may appear alone: a presence-only push has no
86
+ * (or empty) `events`.
87
+ */
88
+ async function parsePushBody(req) {
89
+ let body;
90
+ try {
91
+ body = await req.json();
92
+ } catch (cause) {
93
+ throw new A2Error("INVALID_PAYLOAD", "push body is not valid JSON", { cause });
94
+ }
95
+ if (body === null || typeof body !== "object") throw invalid("expected an object");
96
+ const { sessionId, events, presence } = body;
97
+ if (typeof sessionId !== "string" || sessionId.length === 0) throw invalid("sessionId must be a non-empty string");
98
+ const parsedPresence = parsePresenceSibling(presence);
99
+ if (parsedPresence !== void 0 && (events === void 0 || Array.isArray(events) && events.length === 0)) return {
100
+ sessionId,
101
+ events: [],
102
+ presence: parsedPresence
103
+ };
104
+ const parsed = parsePushEvents(events);
105
+ return parsedPresence !== void 0 ? {
106
+ sessionId,
107
+ events: parsed,
108
+ presence: parsedPresence
109
+ } : {
110
+ sessionId,
111
+ events: parsed
112
+ };
113
+ }
114
+ //#endregion
115
+ //#region src/session-socket.ts
116
+ /**
117
+ * The A2 socket protocol's server half (specs/a2-api.md §13): the
118
+ * per-socket shell — teardown, backpressure, heartbeat, the malformed
119
+ * budget, the in-flight bound — and `sessionsSocket`, the multiplexed
120
+ * frame handler riding it. Internal module: `handle`'s upgrade lane is
121
+ * the sanctioned mount; only `A2Socket` (the structural socket the
122
+ * platform hands over) is re-exported as public surface.
123
+ */
124
+ const messageText = (data) => {
125
+ if (typeof data === "string") return data;
126
+ if (data instanceof ArrayBuffer) return new TextDecoder().decode(data);
127
+ if (ArrayBuffer.isView(data)) return new TextDecoder().decode(data);
128
+ if (Array.isArray(data) && data.every(ArrayBuffer.isView)) {
129
+ const decoder = new TextDecoder();
130
+ let text = "";
131
+ for (const part of data) text += decoder.decode(part, { stream: true });
132
+ return text + decoder.decode();
133
+ }
134
+ return null;
135
+ };
136
+ /**
137
+ * How many unparseable or invalid frames a socket may send before it
138
+ * is closed (1008): the budget absorbs isolated client bugs without
139
+ * letting a broken peer spin the validation path forever. Push frames
140
+ * with a usable `req` never count — their failures are answered with
141
+ * error acks, exactly like the POST path's 4xx bodies.
142
+ */
143
+ const MAX_MALFORMED_FRAMES = 5;
144
+ function socketShell(socket, release, onFrame, deadlineAt) {
145
+ let torn = false;
146
+ let malformed = 0;
147
+ let heartbeat;
148
+ let deadline;
149
+ const teardown = () => {
150
+ if (torn) return;
151
+ torn = true;
152
+ if (heartbeat !== void 0) clearInterval(heartbeat);
153
+ if (deadline !== void 0) clearTimeout(deadline);
154
+ release();
155
+ };
156
+ const shutdown = (code, reason) => {
157
+ if (torn) return;
158
+ teardown();
159
+ try {
160
+ socket.close(code, reason);
161
+ } catch {}
162
+ };
163
+ const send = (data) => {
164
+ if (torn) return;
165
+ if (typeof socket.bufferedAmount === "number" && socket.bufferedAmount > SOCKET_TIMINGS.disconnectWaterMarkBytes) {
166
+ shutdown(1008, "backpressure: peer not reading");
167
+ return;
168
+ }
169
+ try {
170
+ socket.send(data);
171
+ } catch {
172
+ teardown();
173
+ }
174
+ };
175
+ const malformedFrame = (reason) => {
176
+ malformed += 1;
177
+ if (malformed >= MAX_MALFORMED_FRAMES) shutdown(1008, `too many malformed frames (last: ${reason})`);
178
+ };
179
+ let inFlight = 0;
180
+ const track = (work) => {
181
+ inFlight += 1;
182
+ work.finally(() => {
183
+ inFlight -= 1;
184
+ });
185
+ };
186
+ const handleMessage = (data) => {
187
+ if (torn) return;
188
+ const text = messageText(data);
189
+ if (text === null) {
190
+ malformedFrame("non-text frame");
191
+ return;
192
+ }
193
+ let parsed;
194
+ try {
195
+ parsed = JSON.parse(text);
196
+ } catch {
197
+ malformedFrame("frame is not valid JSON");
198
+ return;
199
+ }
200
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
201
+ malformedFrame("frame is not an object");
202
+ return;
203
+ }
204
+ onFrame(parsed);
205
+ };
206
+ socket.on("message", handleMessage);
207
+ socket.on("close", teardown);
208
+ socket.on("error", teardown);
209
+ heartbeat = setInterval(() => send(SOCKET_PING_FRAME), STREAM_TIMINGS.sseHeartbeatMs);
210
+ heartbeat.unref?.();
211
+ if (deadlineAt !== void 0) {
212
+ deadline = setTimeout(() => shutdown(1e3, "deadline"), Math.max(0, deadlineAt - Date.now()));
213
+ deadline.unref?.();
214
+ }
215
+ return {
216
+ torn: () => torn,
217
+ send,
218
+ shutdown,
219
+ malformedFrame,
220
+ buffered: () => typeof socket.bufferedAmount === "number" ? socket.bufferedAmount : 0,
221
+ overCap: () => inFlight >= SOCKET_TIMINGS.maxInFlightMessages,
222
+ track
223
+ };
224
+ }
225
+ const stopSubscription = (subscription) => {
226
+ subscription.stopped = true;
227
+ subscription.iterator.return?.()?.catch(() => {});
228
+ };
229
+ const parseSubscribeEntries = (value) => {
230
+ if (!Array.isArray(value) || value.length === 0) return null;
231
+ const entries = [];
232
+ for (const entry of value) {
233
+ if (entry === null || typeof entry !== "object") return null;
234
+ const { id, index } = entry;
235
+ if (typeof id !== "string" || id.length === 0) return null;
236
+ if (typeof index !== "number" || !Number.isInteger(index) || index < 0) return null;
237
+ entries.push({
238
+ id,
239
+ index
240
+ });
241
+ }
242
+ return entries;
243
+ };
244
+ /**
245
+ * Speak the multiplexed A2 socket protocol (specs/a2-api.md §13)
246
+ * against many sessions on one socket. `subscribe` frames open a
247
+ * per-session pump at that session's own resume frontier; push and
248
+ * presence frames route by `sessionId` through the same validation
249
+ * seams as the POST lane; every down frame carries the `sessionId` it
250
+ * belongs to. One heartbeat, one
251
+ * malformed budget, one in-flight bound, one backpressure gauge — per
252
+ * socket, shared by all sessions. A session's stream ending or failing
253
+ * answers `unsubscribed` for that session; only the peer, the
254
+ * deadline, the malformed budget, or backpressure closes the socket.
255
+ */
256
+ function sessionsSocket(resolve, socket, options) {
257
+ const presence = options?.presence === true;
258
+ const subscriptions = /* @__PURE__ */ new Map();
259
+ const retire = (id, subscription) => {
260
+ if (subscriptions.get(id) === subscription) subscriptions.delete(id);
261
+ };
262
+ const pump = async (id, subscription) => {
263
+ for (;;) {
264
+ for (;;) {
265
+ if (shell.torn() || subscription.stopped || shell.buffered() <= SOCKET_TIMINGS.highWaterMarkBytes) break;
266
+ await new Promise((wake) => setTimeout(wake, SOCKET_TIMINGS.resumePollMs));
267
+ }
268
+ if (shell.torn() || subscription.stopped) return;
269
+ const { value, done } = await subscription.iterator.next();
270
+ if (shell.torn() || subscription.stopped) return;
271
+ if (done) return;
272
+ shell.send(socketFrameFor(value, id));
273
+ }
274
+ };
275
+ const runSubscription = async (id, subscription) => {
276
+ try {
277
+ await pump(id, subscription);
278
+ if (shell.torn() || subscription.stopped) return;
279
+ retire(id, subscription);
280
+ shell.send(socketUnsubscribedFor(id));
281
+ } catch {
282
+ if (shell.torn() || subscription.stopped) return;
283
+ retire(id, subscription);
284
+ shell.send(socketUnsubscribedFor(id, "stream failed"));
285
+ }
286
+ };
287
+ const startSubscription = async (id, index) => {
288
+ let session;
289
+ try {
290
+ session = await resolve(id, index);
291
+ } catch {
292
+ if (!shell.torn()) shell.send(socketUnsubscribedFor(id, "stream failed"));
293
+ return;
294
+ }
295
+ if (shell.torn()) return;
296
+ if (session === null) {
297
+ shell.send(socketUnsubscribedFor(id, "subscribe rejected"));
298
+ return;
299
+ }
300
+ const target = session;
301
+ const existing = subscriptions.get(id);
302
+ if (existing) stopSubscription(existing);
303
+ const subscription = {
304
+ target,
305
+ iterator: (presence ? target.stream({
306
+ startAfter: index,
307
+ presence: true
308
+ }) : target.stream({ startAfter: index }))[Symbol.asyncIterator](),
309
+ stopped: false
310
+ };
311
+ subscriptions.set(id, subscription);
312
+ shell.send(socketSubscribedFor(id));
313
+ runSubscription(id, subscription);
314
+ };
315
+ let controlTail = Promise.resolve();
316
+ const control = (work) => {
317
+ controlTail = controlTail.then(work).catch(() => {});
318
+ };
319
+ const handlePush = async (frame, req, sessionId) => {
320
+ const subscription = subscriptions.get(sessionId);
321
+ if (subscription === void 0) {
322
+ shell.send(socketErrorAckFor(req, new A2Error("STORE_UNAVAILABLE", "session is not subscribed on this socket"), sessionId));
323
+ return;
324
+ }
325
+ try {
326
+ const events = parsePushEvents(frame["events"]);
327
+ const denial = await options?.gatePush?.(sessionId, events, void 0) ?? null;
328
+ if (denial !== null) {
329
+ shell.send(socketErrorAckFor(req, denial, sessionId));
330
+ return;
331
+ }
332
+ const appended = await subscription.target.append(...events);
333
+ shell.send(socketAckFor(req, appended, sessionId));
334
+ } catch (err) {
335
+ shell.send(socketErrorAckFor(req, asA2Error(err), sessionId));
336
+ }
337
+ };
338
+ const handlePresence = async (frame, sessionId) => {
339
+ const subscription = subscriptions.get(sessionId);
340
+ if (subscription === void 0) return;
341
+ if (typeof subscription.target.setPresence !== "function") {
342
+ shell.malformedFrame("presence frame on a presence-less session");
343
+ return;
344
+ }
345
+ let patch;
346
+ try {
347
+ patch = parsePresenceSibling(frame);
348
+ } catch {
349
+ shell.malformedFrame("invalid presence frame");
350
+ return;
351
+ }
352
+ if (!patch) return;
353
+ try {
354
+ if ((await options?.gatePush?.(sessionId, [], patch) ?? null) !== null) return;
355
+ await subscription.target.setPresence(patch);
356
+ } catch (error) {
357
+ const failure = asA2Error(error);
358
+ if (failure.code === "INVALID_PAYLOAD" || failure.code === "UNKNOWN_PRESENCE_FIELD") {
359
+ shell.malformedFrame("invalid presence frame");
360
+ return;
361
+ }
362
+ shell.shutdown(1011, "internal error");
363
+ }
364
+ };
365
+ const handleFrame = (frame) => {
366
+ switch (frame["kind"]) {
367
+ case "subscribe": {
368
+ const entries = parseSubscribeEntries(frame["sessions"]);
369
+ if (entries === null) {
370
+ shell.malformedFrame("malformed subscribe frame");
371
+ return;
372
+ }
373
+ for (const entry of entries) control(() => startSubscription(entry.id, entry.index));
374
+ return;
375
+ }
376
+ case "unsubscribe": {
377
+ const ids = frame["sessions"];
378
+ if (!Array.isArray(ids) || ids.length === 0 || !ids.every((id) => typeof id === "string")) {
379
+ shell.malformedFrame("malformed unsubscribe frame");
380
+ return;
381
+ }
382
+ for (const id of ids) control(() => {
383
+ const subscription = subscriptions.get(id);
384
+ if (subscription === void 0) return;
385
+ stopSubscription(subscription);
386
+ subscriptions.delete(id);
387
+ });
388
+ return;
389
+ }
390
+ case "push": {
391
+ const req = frame["req"];
392
+ const sessionId = frame["sessionId"];
393
+ if (shell.overCap()) {
394
+ if (typeof req === "number") shell.send(socketErrorAckFor(req, new A2Error("STORE_UNAVAILABLE", "push shed: too many in flight on this socket"), typeof sessionId === "string" ? sessionId : void 0));
395
+ else shell.malformedFrame("push frame without a numeric req");
396
+ return;
397
+ }
398
+ if (typeof req !== "number") {
399
+ shell.malformedFrame("push frame without a numeric req");
400
+ return;
401
+ }
402
+ if (typeof sessionId !== "string" || sessionId.length === 0) {
403
+ shell.send(socketErrorAckFor(req, new A2Error("INVALID_PAYLOAD", "push frame without a sessionId")));
404
+ return;
405
+ }
406
+ shell.track(handlePush(frame, req, sessionId));
407
+ return;
408
+ }
409
+ case "presence": {
410
+ if (shell.overCap()) return;
411
+ const sessionId = frame["sessionId"];
412
+ if (typeof sessionId !== "string" || sessionId.length === 0) {
413
+ shell.malformedFrame("presence frame without a sessionId");
414
+ return;
415
+ }
416
+ shell.track(handlePresence(frame, sessionId));
417
+ return;
418
+ }
419
+ default: return;
420
+ }
421
+ };
422
+ const shell = socketShell(socket, () => {
423
+ for (const subscription of subscriptions.values()) stopSubscription(subscription);
424
+ subscriptions.clear();
425
+ }, (frame) => handleFrame(frame), options?.deadline);
426
+ }
427
+ //#endregion
428
+ //#region src/sse.ts
429
+ /**
430
+ * The SSE response protocol — framing, heartbeat, deadline, and
431
+ * teardown for the live stream lane. Internal module: `server.fetch` is
432
+ * the sanctioned caller; the framing rules stay
433
+ * documented on `sseResponse` because the client's parser (and the
434
+ * stall watchdog) depend on them.
435
+ */
436
+ /**
437
+ * Pipe a live event iterable into an SSE `Response`. Each event is one
438
+ * frame — `id:` carries the event-log index, `data:` the JSON event. A
439
+ * `stream({ presence: true })` iterable also yields presence items,
440
+ * which ride as named frames old clients skip: `event:
441
+ * presence-snapshot` first (the map, ISO dates), then `event: presence`
442
+ * per patch — no `id:`, presence never advances the resume frontier. A
443
+ * disconnecting client cancels the stream, which closes the underlying
444
+ * subscription. Two kinds of comment frames ride along: a `: connected`
445
+ * prelude that flushes headers immediately, and a `: ping` heartbeat
446
+ * every 15 seconds so clients (and proxies) can tell a quiet stream
447
+ * from a dead connection — the session client's stall watchdog counts
448
+ * on it. When the platform exposes an invocation deadline, the response
449
+ * closes cleanly one second before it so the client reconnects without
450
+ * a platform-timeout failure.
451
+ */
452
+ function sseResponse(iterable) {
453
+ const iterator = iterable[Symbol.asyncIterator]();
454
+ const encoder = new TextEncoder();
455
+ let heartbeat;
456
+ let deadlineTimer;
457
+ let closed = false;
458
+ let iteratorReturned = false;
459
+ const stopTimers = () => {
460
+ if (heartbeat !== void 0) clearInterval(heartbeat);
461
+ if (deadlineTimer !== void 0) clearTimeout(deadlineTimer);
462
+ heartbeat = void 0;
463
+ deadlineTimer = void 0;
464
+ };
465
+ const returnIterator = async () => {
466
+ if (iteratorReturned) return;
467
+ iteratorReturned = true;
468
+ await iterator.return?.();
469
+ };
470
+ const stream = new ReadableStream({
471
+ start(controller) {
472
+ controller.enqueue(encoder.encode(": connected\n\n"));
473
+ heartbeat = setInterval(() => {
474
+ try {
475
+ controller.enqueue(encoder.encode(": ping\n\n"));
476
+ } catch {
477
+ stopTimers();
478
+ }
479
+ }, STREAM_TIMINGS.sseHeartbeatMs);
480
+ heartbeat.unref?.();
481
+ const invocationDeadline = invocationDeadlineMs();
482
+ if (invocationDeadline !== null) {
483
+ const delay = Math.max(0, invocationDeadline - Date.now() - STREAM_TIMINGS.sseDeadlineGraceMs);
484
+ deadlineTimer = setTimeout(() => {
485
+ if (closed) return;
486
+ closed = true;
487
+ stopTimers();
488
+ returnIterator().catch(() => {});
489
+ try {
490
+ controller.close();
491
+ } catch {}
492
+ }, delay);
493
+ deadlineTimer.unref?.();
494
+ }
495
+ },
496
+ async pull(controller) {
497
+ try {
498
+ const { value, done } = await iterator.next();
499
+ if (closed) return;
500
+ if (done) {
501
+ closed = true;
502
+ stopTimers();
503
+ controller.close();
504
+ return;
505
+ }
506
+ controller.enqueue(encoder.encode(frameFor(value)));
507
+ } catch (error) {
508
+ if (closed) return;
509
+ closed = true;
510
+ stopTimers();
511
+ returnIterator().catch(() => {});
512
+ throw error;
513
+ }
514
+ },
515
+ async cancel() {
516
+ if (closed) return;
517
+ closed = true;
518
+ stopTimers();
519
+ await returnIterator();
520
+ }
521
+ });
522
+ return new Response(stream, {
523
+ status: 200,
524
+ headers: {
525
+ "content-type": "text/event-stream",
526
+ "cache-control": "no-cache, no-transform",
527
+ connection: "keep-alive"
528
+ }
529
+ });
530
+ }
531
+ function frameFor(item) {
532
+ if ("snapshot" in item) return `event: presence-snapshot\ndata: ${JSON.stringify(presenceSnapshotToWire(item))}\n\n`;
533
+ if ("participant" in item) return `event: presence\ndata: ${JSON.stringify(presencePatchToWire(item))}\n\n`;
534
+ return `id: ${item.index}\ndata: ${JSON.stringify(eventToWire(item))}\n\n`;
535
+ }
536
+ //#endregion
537
+ //#region src/server-fetch.ts
538
+ const serverFetchHooks = /* @__PURE__ */ new WeakMap();
539
+ function setServerFetchHooks(server, hooks) {
540
+ serverFetchHooks.set(server.fetch, hooks);
541
+ }
542
+ const parseHistoryBounds = (gte, lte) => {
543
+ if (gte === null || lte === null) throw new A2Error("INVALID_PAYLOAD", "a history read takes both gte and lte");
544
+ const bounds = {
545
+ gte: Number(gte),
546
+ lte: Number(lte)
547
+ };
548
+ for (const [name, value] of Object.entries(bounds)) if (!Number.isSafeInteger(value) || value < 0) throw new A2Error("INVALID_PAYLOAD", `history ${name} must be a non-negative safe integer`);
549
+ if (bounds.gte > bounds.lte) throw new A2Error("INVALID_PAYLOAD", "history gte must be less than or equal to lte");
550
+ return bounds;
551
+ };
552
+ const parseResumeIndex = (raw) => {
553
+ if (raw === null) return 0;
554
+ const index = Number(raw);
555
+ if (!Number.isSafeInteger(index) || index < 0) throw new A2Error("INVALID_PAYLOAD", "stream index must be a non-negative safe integer");
556
+ return index;
557
+ };
558
+ const errorResponse = (error) => {
559
+ const a2error = asA2Error(error);
560
+ return Response.json(errorToWire(a2error), { status: errorStatus(a2error.code) });
561
+ };
562
+ const forbidden = () => new A2Error("FORBIDDEN", "the operation is not authorized");
563
+ const freezePush = (sessionId, events, presence, transport) => {
564
+ for (const event of events) Object.freeze(event);
565
+ Object.freeze(events);
566
+ if (presence !== void 0) {
567
+ Object.freeze(presence.values);
568
+ Object.freeze(presence);
569
+ }
570
+ return {
571
+ type: "push",
572
+ sessionId,
573
+ events,
574
+ ...presence === void 0 ? {} : { presence },
575
+ transport
576
+ };
577
+ };
578
+ function createServerFetch(server) {
579
+ const presence = Object.keys(server.contract.presence).length > 0;
580
+ const fetch = async (request, options) => {
581
+ const authorized = async (operation) => {
582
+ try {
583
+ return await options?.authorize?.(Object.freeze(operation)) !== false;
584
+ } catch (cause) {
585
+ throw new A2Error("STORE_UNAVAILABLE", "authorization failed", { cause });
586
+ }
587
+ };
588
+ const authorize = async (operation) => {
589
+ if (!await authorized(operation)) throw forbidden();
590
+ };
591
+ const validateIngress = async (context) => {
592
+ await serverFetchHooks.get(fetch)?.validateIngress?.(context);
593
+ };
594
+ const attach = (socket) => {
595
+ const deadline = invocationDeadlineMs();
596
+ sessionsSocket(async (sessionId, startAfter) => {
597
+ return await authorized({
598
+ type: "stream",
599
+ sessionId,
600
+ startAfter,
601
+ transport: "websocket"
602
+ }) ? server.session(sessionId) : null;
603
+ }, socket, {
604
+ presence,
605
+ ...deadline === null ? {} : { deadline },
606
+ gatePush: async (sessionId, events, pushedPresence) => {
607
+ try {
608
+ await authorize(freezePush(sessionId, events, pushedPresence, "websocket"));
609
+ await validateIngress({
610
+ sessionId,
611
+ events,
612
+ ...pushedPresence === void 0 ? {} : { presence: pushedPresence }
613
+ });
614
+ return null;
615
+ } catch (error) {
616
+ return asA2Error(error);
617
+ }
618
+ }
619
+ });
620
+ };
621
+ try {
622
+ if (request.method === "GET") {
623
+ if (request.headers.get("upgrade")?.toLowerCase() === "websocket") {
624
+ if (options?.upgradeWebSocket === void 0) return new Response("WebSocket upgrade requested, but server.fetch() has no upgrade implementation", { status: 426 });
625
+ return await options.upgradeWebSocket(attach);
626
+ }
627
+ const { searchParams } = new URL(request.url);
628
+ const sessionId = searchParams.get("sessionId");
629
+ if (sessionId === null || sessionId.length === 0) throw new A2Error("INVALID_PAYLOAD", "missing sessionId");
630
+ const gteRaw = searchParams.get("gte");
631
+ const lteRaw = searchParams.get("lte");
632
+ if (gteRaw !== null || lteRaw !== null) {
633
+ const bounds = parseHistoryBounds(gteRaw, lteRaw);
634
+ await authorize({
635
+ type: "history",
636
+ sessionId,
637
+ ...bounds,
638
+ transport: "http"
639
+ });
640
+ const events = await server.session(sessionId).history(bounds);
641
+ const covered = events.length === bounds.lte - bounds.gte + 1;
642
+ return Response.json(events.map(eventToWire), { headers: { "a2-history-covered": String(covered) } });
643
+ }
644
+ const startAfter = parseResumeIndex(searchParams.get("index"));
645
+ await authorize({
646
+ type: "stream",
647
+ sessionId,
648
+ startAfter,
649
+ transport: "http"
650
+ });
651
+ const target = server.session(sessionId);
652
+ return sseResponse(presence ? target.stream({
653
+ startAfter,
654
+ presence: true
655
+ }) : target.stream({ startAfter }));
656
+ }
657
+ if (request.method === "POST") {
658
+ const body = await parsePushBody(request);
659
+ await authorize(freezePush(body.sessionId, body.events, body.presence, "http"));
660
+ await validateIngress({
661
+ sessionId: body.sessionId,
662
+ events: body.events,
663
+ ...body.presence === void 0 ? {} : { presence: body.presence }
664
+ });
665
+ const session = server.session(body.sessionId);
666
+ if (body.presence !== void 0) {
667
+ const target = session;
668
+ if (typeof target.setPresence !== "function") throw invalidPushBody("presence is not declared by this contract");
669
+ await target.setPresence(body.presence);
670
+ }
671
+ const appended = body.events.length === 0 ? [] : await session.append(...body.events);
672
+ return Response.json(appended.map(eventToWire));
673
+ }
674
+ return new Response("Method Not Allowed", {
675
+ status: 405,
676
+ headers: { allow: "GET, POST" }
677
+ });
678
+ } catch (error) {
679
+ return errorResponse(error);
680
+ }
681
+ };
682
+ return fetch;
683
+ }
684
+ //#endregion
8
685
  //#region src/deterministic-id.ts
9
686
  /**
10
687
  * Deterministic default ids for handler `session.append` calls. A handler
@@ -446,21 +1123,45 @@ function createServer(options) {
446
1123
  throw asStoreUnavailable(err);
447
1124
  }
448
1125
  };
449
- const readCachedState = async (store, sessionId, reducerName) => {
1126
+ const readCachedState = async (store, sessionId, reducerName, throughIndex, snapshotThroughIndex) => {
450
1127
  try {
451
- return await store.readState(nsId(sessionId), reducerName);
1128
+ return await store.readState(nsId(sessionId), reducerName, throughIndex === void 0 && snapshotThroughIndex === void 0 ? void 0 : {
1129
+ ...throughIndex === void 0 ? {} : { throughIndex },
1130
+ ...snapshotThroughIndex === void 0 ? {} : { snapshotThroughIndex }
1131
+ });
452
1132
  } catch {
453
1133
  try {
454
1134
  return {
1135
+ headIndex: null,
455
1136
  snapshot: null,
456
- events: await store.read(nsId(sessionId))
1137
+ events: await store.read(nsId(sessionId), throughIndex === void 0 ? void 0 : { throughIndex })
457
1138
  };
458
1139
  } catch (err) {
459
1140
  throw asStoreUnavailable(err);
460
1141
  }
461
1142
  }
462
1143
  };
463
- const foldStateRead = async (store, sessionId, reducer, stateRead, span) => {
1144
+ const planStateReads = (batch) => {
1145
+ const bySession = /* @__PURE__ */ new Map();
1146
+ for (const entry of batch) {
1147
+ const entries = bySession.get(entry.sessionId);
1148
+ if (entries) entries.push(entry);
1149
+ else bySession.set(entry.sessionId, [entry]);
1150
+ }
1151
+ const plans = [];
1152
+ for (const [sessionId, entries] of bySession) {
1153
+ const boundaries = entries.flatMap((entry) => entry.throughIndex === void 0 ? [] : [entry.throughIndex]);
1154
+ const plan = {
1155
+ sessionId,
1156
+ entries
1157
+ };
1158
+ if (boundaries.length === entries.length) plan.throughIndex = Math.max(...boundaries);
1159
+ if (boundaries.length > 0) plan.snapshotThroughIndex = Math.min(...boundaries);
1160
+ plans.push(plan);
1161
+ }
1162
+ return plans;
1163
+ };
1164
+ const foldStatePlan = async (store, reducer, plan, stateRead) => {
464
1165
  let state = cloneInitial(reducer.initialState);
465
1166
  let index = 0;
466
1167
  let snapshotOutcome = "miss";
@@ -481,84 +1182,120 @@ function createServer(options) {
481
1182
  snapshotOutcome = "hit";
482
1183
  }
483
1184
  }
484
- span.setAttribute("a2.state.snapshot", snapshotOutcome);
485
1185
  if (snapshotOutcome === "rejected") try {
486
- rows = await store.read(nsId(sessionId));
1186
+ rows = await store.read(nsId(plan.sessionId), plan.throughIndex === void 0 ? void 0 : { throughIndex: plan.throughIndex });
487
1187
  } catch (err) {
488
1188
  throw asStoreUnavailable(err);
489
1189
  }
490
- for (const row of rows) {
491
- state = reducer.fold(state, toPublic(row));
492
- index = row.index;
1190
+ const ordered = plan.entries.toSorted((a, b) => (a.throughIndex ?? Number.POSITIVE_INFINITY) - (b.throughIndex ?? Number.POSITIVE_INFINITY));
1191
+ const writes = /* @__PURE__ */ new Map();
1192
+ let rowPosition = 0;
1193
+ for (const entry of ordered) {
1194
+ const target = entry.throughIndex ?? Number.POSITIVE_INFINITY;
1195
+ while (rowPosition < rows.length && rows[rowPosition].index <= target) {
1196
+ const row = rows[rowPosition];
1197
+ state = reducer.fold(state, toPublic(row));
1198
+ index = row.index;
1199
+ rowPosition += 1;
1200
+ }
1201
+ const resultState = cloneInitial(state);
1202
+ const folded = rowPosition;
1203
+ entry.span.setAttribute("a2.state.snapshot", snapshotOutcome);
1204
+ entry.span.setAttribute("a2.state.folded", folded);
1205
+ entry.span.setAttribute("a2.state.index", index);
1206
+ if (index > 0 && (folded > 0 || entry.pinEventIndex !== void 0)) {
1207
+ const advancesHead = stateRead.headIndex === null || index > stateRead.headIndex;
1208
+ const repairsHead = snapshotOutcome === "rejected" && index === stateRead.headIndex;
1209
+ if (!advancesHead && !repairsHead && entry.pinEventIndex === void 0) {
1210
+ entry.resolve({
1211
+ state: resultState,
1212
+ index
1213
+ });
1214
+ continue;
1215
+ }
1216
+ let write = writes.get(index);
1217
+ if (!write) {
1218
+ write = {
1219
+ state: cloneInitial(resultState),
1220
+ pinEventIndexes: /* @__PURE__ */ new Set()
1221
+ };
1222
+ writes.set(index, write);
1223
+ }
1224
+ if (entry.pinEventIndex !== void 0) write.pinEventIndexes.add(entry.pinEventIndex);
1225
+ }
1226
+ entry.resolve({
1227
+ state: resultState,
1228
+ index
1229
+ });
1230
+ }
1231
+ if (writes.size > 0) {
1232
+ const snapshots = [];
1233
+ for (const [snapshotIndex, write] of writes) {
1234
+ const snapshot = {
1235
+ index: snapshotIndex,
1236
+ state: write.state
1237
+ };
1238
+ if (write.pinEventIndexes.size > 0) snapshot.pinEventIndexes = [...write.pinEventIndexes];
1239
+ snapshots.push(snapshot);
1240
+ }
1241
+ const persistence = Promise.resolve().then(() => store.putSnapshots(nsId(plan.sessionId), reducer.name, snapshots)).catch(() => {});
1242
+ track(persistence);
1243
+ platformWaitUntil(persistence);
493
1244
  }
494
- span.setAttribute("a2.state.folded", rows.length);
495
- span.setAttribute("a2.state.index", index);
496
- return {
497
- state,
498
- index,
499
- folded: rows.length
500
- };
501
1245
  };
502
1246
  const pendingStateReads = /* @__PURE__ */ new Map();
503
- const flushStateReads = async (store, reducerName, batch) => {
1247
+ const flushStateReads = async (store, reducer, batch) => {
1248
+ const plans = planStateReads(batch);
504
1249
  let reads;
505
1250
  try {
506
- reads = store.readStates ? await store.readStates(batch.map((entry) => nsId(entry.sessionId)), reducerName) : null;
1251
+ reads = store.readStates ? await store.readStates(plans.map((plan) => ({
1252
+ sessionId: nsId(plan.sessionId),
1253
+ ...plan.throughIndex === void 0 ? {} : { throughIndex: plan.throughIndex },
1254
+ ...plan.snapshotThroughIndex === void 0 ? {} : { snapshotThroughIndex: plan.snapshotThroughIndex }
1255
+ })), reducer.name) : await Promise.all(plans.map((plan) => readCachedState(store, plan.sessionId, reducer.name, plan.throughIndex, plan.snapshotThroughIndex)));
507
1256
  } catch {
508
1257
  reads = null;
509
1258
  }
510
- if (!reads || reads.length !== batch.length) {
511
- for (const entry of batch) entry.resolve(readCachedState(store, entry.sessionId, reducerName));
1259
+ if (!reads || reads.length !== plans.length) try {
1260
+ reads = await Promise.all(plans.map((plan) => readCachedState(store, plan.sessionId, reducer.name, plan.throughIndex, plan.snapshotThroughIndex)));
1261
+ } catch (err) {
1262
+ for (const entry of batch) entry.reject(err);
512
1263
  return;
513
1264
  }
514
- const seen = /* @__PURE__ */ new Set();
515
- for (const [position, entry] of batch.entries()) {
516
- const shared = seen.has(entry.sessionId);
517
- seen.add(entry.sessionId);
518
- try {
519
- const read = reads[position];
520
- entry.resolve(shared ? structuredClone(read) : read);
521
- } catch {
522
- entry.resolve(readCachedState(store, entry.sessionId, reducerName));
523
- }
1265
+ for (const [position, plan] of plans.entries()) try {
1266
+ await foldStatePlan(store, reducer, plan, reads[position]);
1267
+ } catch (err) {
1268
+ for (const entry of plan.entries) entry.reject(err);
524
1269
  }
525
1270
  };
526
- const enqueueStateRead = (store, sessionId, reducerName) => new Promise((resolve) => {
1271
+ const enqueueStateRead = (store, sessionId, reducer, throughIndex, pinEventIndex, span) => new Promise((resolve, reject) => {
1272
+ const reducerName = reducer.name;
527
1273
  let batch = pendingStateReads.get(reducerName);
528
1274
  if (!batch) {
529
1275
  const opened = [];
530
1276
  pendingStateReads.set(reducerName, opened);
531
1277
  queueMicrotask(() => {
532
1278
  pendingStateReads.delete(reducerName);
533
- flushStateReads(store, reducerName, opened);
1279
+ flushStateReads(store, reducer, opened);
534
1280
  });
535
1281
  batch = opened;
536
1282
  }
537
1283
  batch.push({
538
1284
  sessionId,
539
- resolve
1285
+ ...throughIndex === void 0 ? {} : { throughIndex },
1286
+ ...pinEventIndex === void 0 ? {} : { pinEventIndex },
1287
+ span,
1288
+ resolve,
1289
+ reject
540
1290
  });
541
1291
  });
542
- const readState = async (sessionId, reducer) => {
1292
+ const readState = async (sessionId, reducer, throughIndex, pinEventIndex) => {
543
1293
  const store = await resolveStore();
544
1294
  return telemetry.span("a2.state", {
545
1295
  "a2.contract": name,
546
1296
  "a2.session_id": sessionId,
547
1297
  "a2.state.reducer": reducer.name
548
- }, async (span) => {
549
- const stateRead = store.readStates ? await enqueueStateRead(store, sessionId, reducer.name) : await readCachedState(store, sessionId, reducer.name);
550
- const { state, index, folded } = await foldStateRead(store, sessionId, reducer, stateRead, span);
551
- if (folded > 0) {
552
- const snapshotState = cloneInitial(state);
553
- const write = Promise.resolve().then(() => store.putSnapshot(nsId(sessionId), reducer.name, index, snapshotState)).catch(() => {});
554
- track(write);
555
- platformWaitUntil(write);
556
- }
557
- return {
558
- state,
559
- index
560
- };
561
- });
1298
+ }, (span) => enqueueStateRead(store, sessionId, reducer, throughIndex, pinEventIndex, span));
562
1299
  };
563
1300
  const makeSchedule = (sessionId, trigger) => {
564
1301
  const triggerEventId = trigger?.id ?? null;
@@ -569,11 +1306,14 @@ function createServer(options) {
569
1306
  if (events.length === 0) throw new TypeError("schedule requires at least one event");
570
1307
  const dueAt = scheduleDueAt(timing, triggerCreatedAt ?? Date.now());
571
1308
  const payloads = events.map((event) => cloneScheduledPayload(event.payload));
572
- const snapshottedEvents = events.map((event, index) => ({
1309
+ const snapshottedEvents = events.map((event, index) => event.id === void 0 ? {
1310
+ type: event.type,
1311
+ payload: payloads[index]
1312
+ } : {
573
1313
  type: event.type,
574
1314
  payload: payloads[index],
575
- ...event.id === void 0 ? {} : { id: event.id }
576
- }));
1315
+ id: event.id
1316
+ });
577
1317
  const validated = validateEvents(sessionId, structuredClone(snapshottedEvents), false);
578
1318
  const scheduleId = await deterministicScheduleId(name, sessionId, triggerEventId, scheduleName);
579
1319
  const scheduledEvents = await Promise.all(validated.map(async (event, item) => ({
@@ -618,14 +1358,6 @@ function createServer(options) {
618
1358
  return store.presence;
619
1359
  };
620
1360
  const setPresence = async (sessionId, patch, fallbackSeen) => {
621
- if (Reflect.get(patch, "~a2.pushed") === true) {
622
- Object.freeze(patch.values);
623
- await options.validatePush?.({
624
- sessionId,
625
- events: [],
626
- presence: Object.freeze(patch)
627
- });
628
- }
629
1361
  if (typeof patch.participant !== "string" || patch.participant.length === 0) throw new TypeError("presence participant must be a non-empty string");
630
1362
  if (RESERVED_PARTICIPANT_IDS.has(patch.participant)) throw new TypeError(`presence participant must not be '${patch.participant}'`);
631
1363
  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");
@@ -843,7 +1575,11 @@ function createServer(options) {
843
1575
  append,
844
1576
  schedule: makeSchedule(id, trigger),
845
1577
  history: async (bounds) => (await readHistory(id, bounds)).map(toPublic),
846
- state: (reducer) => readState(id, reducer),
1578
+ state: (reducer, stateOptions) => {
1579
+ const through = stateOptions?.through ?? trigger?.index;
1580
+ if (through !== void 0 && through !== "latest") assertStateIndex(through);
1581
+ return readState(id, reducer, through === "latest" ? void 0 : through, trigger && through !== "latest" ? trigger.index : void 0);
1582
+ },
847
1583
  stream: (opts) => {
848
1584
  const startAfter = opts?.startAfter ?? 0;
849
1585
  assertStreamIndex(startAfter);
@@ -1230,11 +1966,22 @@ function createServer(options) {
1230
1966
  shouldClaim = false;
1231
1967
  }
1232
1968
  }
1969
+ if (claim?.outcome === "claimed" && [...active.values()].some((entry) => entry.lapsed)) {
1970
+ shouldClaim = true;
1971
+ continue;
1972
+ }
1233
1973
  if (active.size > 0) {
1234
1974
  const wake = signal?.wait(signalVersion);
1235
- await Promise.race([...[...active.values()].map((entry) => entry.execution), ...wake ? [wake.promise] : []]);
1975
+ const hasLapsed = [...active.values()].some((entry) => entry.lapsed);
1976
+ const retry = claim?.outcome === "busy" && hasLapsed ? defaultSleep(Math.max(0, claim.retryAt.getTime() - Date.now())) : void 0;
1977
+ const retryDue = await Promise.race([
1978
+ ...[...active.values()].map((entry) => entry.execution.then(() => false)),
1979
+ ...wake ? [wake.promise.then(() => false)] : [],
1980
+ ...retry ? [retry.promise.then(() => true)] : []
1981
+ ]);
1236
1982
  wake?.cancel();
1237
- shouldClaim = claimAgain || active.size === 0 || signal !== void 0 && signal.version !== signalVersion;
1983
+ retry?.cancel();
1984
+ shouldClaim = retryDue || claimAgain || active.size === 0 || hasLapsed || signal !== void 0 && signal.version !== signalVersion;
1238
1985
  claimAgain = false;
1239
1986
  continue;
1240
1987
  }
@@ -1270,22 +2017,22 @@ function createServer(options) {
1270
2017
  };
1271
2018
  });
1272
2019
  const appendExternal = async (sessionId, mode, events, requireDurableInitialArm = false) => {
1273
- const pushed = events.filter((event) => Reflect.get(event, "~a2.pushed") === true);
1274
- if (pushed.length > 0) await options.validatePush?.({
1275
- sessionId,
1276
- events: pushed
1277
- });
1278
2020
  return appendCore(sessionId, events, "external", mode, (_rows, watchdogDueAt) => scheduleDrain(sessionId, watchdogDueAt), void 0, void 0, requireDurableInitialArm);
1279
2021
  };
2022
+ const session = (id) => {
2023
+ assertSessionId(id);
2024
+ const appendInline = async (...events) => appendExternal(id, "inline", events);
2025
+ const appendDispatch = async (...events) => appendExternal(id, "dispatch", events);
2026
+ const append = Object.assign(appendInline, { dispatch: appendDispatch });
2027
+ return makeSession(id, append, null);
2028
+ };
1280
2029
  const self = {
1281
2030
  contract: serverContract,
1282
- session(id) {
1283
- assertSessionId(id);
1284
- const appendInline = async (...events) => appendExternal(id, "inline", events);
1285
- const appendDispatch = async (...events) => appendExternal(id, "dispatch", events);
1286
- const append = Object.assign(appendInline, { dispatch: appendDispatch });
1287
- return makeSession(id, append, null);
1288
- },
2031
+ fetch: createServerFetch({
2032
+ contract: serverContract,
2033
+ session
2034
+ }),
2035
+ session,
1289
2036
  async drain(sessionId) {
1290
2037
  assertSessionId(sessionId);
1291
2038
  const { settled } = await drainSession(sessionId);
@@ -1306,57 +2053,6 @@ function createServer(options) {
1306
2053
  await appendExternal(sessionId, "inline", [...events], true);
1307
2054
  }
1308
2055
  });
1309
- serverSchedulerBindings.set(self, { scheduler });
1310
- serverInspection.set(self, {
1311
- async listSessions(inspectionOptions) {
1312
- const store = await resolveStore();
1313
- if (!store.inspect) throw new InspectionUnsupportedError("this store backend does not support inspection");
1314
- const page = await store.inspect.listSessions({
1315
- prefix: `${name}${NS}`,
1316
- limit: inspectionOptions.limit,
1317
- ...inspectionOptions.cursor !== void 0 ? { cursor: inspectionOptions.cursor } : {}
1318
- });
1319
- return {
1320
- cursor: page.cursor,
1321
- sessions: page.sessions.map((session) => {
1322
- const publicSession = { ...session };
1323
- publicSession.sessionId = stripNs(session.sessionId);
1324
- return publicSession;
1325
- })
1326
- };
1327
- },
1328
- async readSessionPage(sessionId, inspectionOptions) {
1329
- assertSessionId(sessionId);
1330
- 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");
1331
- const store = await resolveStore();
1332
- if (!store.inspect) throw new InspectionUnsupportedError("this store backend does not support inspection");
1333
- const ns = nsId(sessionId);
1334
- const snapshots = inspectionOptions.afterIndex === 0 ? await store.inspect.listSnapshots(ns) : [];
1335
- const { events, throughIndex } = store.inspect.readEvents ? await store.inspect.readEvents(ns, inspectionOptions) : await (async () => {
1336
- const all = await store.read(ns);
1337
- const currentFrontier = all.at(-1)?.index ?? 0;
1338
- const throughIndex = Math.min(inspectionOptions.throughIndex ?? currentFrontier, currentFrontier);
1339
- return {
1340
- events: all.filter((event) => event.index > inspectionOptions.afterIndex && event.index <= throughIndex).slice(0, inspectionOptions.limit),
1341
- throughIndex
1342
- };
1343
- })();
1344
- 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`);
1345
- 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`);
1346
- if (inspectionOptions.afterIndex < throughIndex && events.length === 0) throw new A2Error("STORE_UNAVAILABLE", `inspection event page for session '${sessionId}' ended before index ${throughIndex}`);
1347
- const lastIndex = events.at(-1)?.index ?? inspectionOptions.afterIndex;
1348
- return {
1349
- events: events.map((event) => {
1350
- const publicEvent = { ...event };
1351
- publicEvent.sessionId = sessionId;
1352
- return publicEvent;
1353
- }),
1354
- snapshots: snapshots.filter((snapshot) => snapshot.index <= throughIndex),
1355
- throughIndex,
1356
- nextIndex: lastIndex < throughIndex ? lastIndex : null
1357
- };
1358
- }
1359
- });
1360
2056
  return self;
1361
2057
  }
1362
2058
  function assertSessionId(id) {
@@ -1369,6 +2065,9 @@ function assertHistoryIndex(name, value) {
1369
2065
  function assertStreamIndex(value) {
1370
2066
  if (!Number.isSafeInteger(value) || value < 0) throw new TypeError("stream.startAfter must be a non-negative safe integer");
1371
2067
  }
2068
+ function assertStateIndex(value) {
2069
+ if (!Number.isSafeInteger(value) || value < 0) throw new TypeError("state.through must be a non-negative safe integer");
2070
+ }
1372
2071
  function assertAppendName(name) {
1373
2072
  if (typeof name !== "string" || name.length === 0) throw new TypeError("handler append name must be a non-empty string");
1374
2073
  }
@@ -1458,6 +2157,6 @@ function cloneInitial(initial) {
1458
2157
  }
1459
2158
  }
1460
2159
  //#endregion
1461
- export { deliverSchedulerAppend as n, createServer as t };
2160
+ export { platformVercelOidcToken as i, deliverSchedulerAppend as n, setServerFetchHooks as r, createServer as t };
1462
2161
 
1463
- //# sourceMappingURL=server-Duw6MVlB.js.map
2162
+ //# sourceMappingURL=server-B2XNevQA.js.map