experimental-a2 0.4.0 → 0.5.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 (182) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/dist/{ai-B4YhEnfw.d.ts → ai-CFNeCrRl.d.ts} +3 -2
  3. package/dist/ai-CFNeCrRl.d.ts.map +1 -0
  4. package/dist/ai-server.browser.js +2 -0
  5. package/dist/ai-server.browser.js.map +1 -0
  6. package/dist/ai-server.d.ts +4 -3
  7. package/dist/ai-server.d.ts.map +1 -0
  8. package/dist/ai-server.js +4 -2
  9. package/dist/ai-server.js.map +1 -0
  10. package/dist/ai.d.ts +1 -1
  11. package/dist/ai.js +3 -1
  12. package/dist/ai.js.map +1 -0
  13. package/dist/cli-B3VuxoDe.js +2 -0
  14. package/dist/cli-B3VuxoDe.js.map +1 -0
  15. package/dist/cli-bin.js +2 -0
  16. package/dist/cli-bin.js.map +1 -0
  17. package/dist/cli.d.ts +2 -1
  18. package/dist/cli.d.ts.map +1 -0
  19. package/dist/{client-Bt4tAKi9.js → client-BKlyLiOU.js} +295 -85
  20. package/dist/client-BKlyLiOU.js.map +1 -0
  21. package/dist/{client-BrfDXQ8A.d.ts → client-D7mvIXrF.d.ts} +40 -4
  22. package/dist/client-D7mvIXrF.d.ts.map +1 -0
  23. package/dist/client.d.ts +2 -2
  24. package/dist/client.js +1 -1
  25. package/dist/contract-48bUMgcL.js +2 -0
  26. package/dist/contract-48bUMgcL.js.map +1 -0
  27. package/dist/contract-jIfaR085.d.ts +2 -1
  28. package/dist/contract-jIfaR085.d.ts.map +1 -0
  29. package/dist/devtools-J_jZ2vQf.d.ts +2 -1
  30. package/dist/devtools-J_jZ2vQf.d.ts.map +1 -0
  31. package/dist/devtools-kJJaORn-.js +2 -0
  32. package/dist/devtools-kJJaORn-.js.map +1 -0
  33. package/dist/devtools-server.browser.js +2 -0
  34. package/dist/devtools-server.browser.js.map +1 -0
  35. package/dist/devtools-server.d.ts +2 -1
  36. package/dist/devtools-server.d.ts.map +1 -0
  37. package/dist/devtools-server.js +2 -0
  38. package/dist/devtools-server.js.map +1 -0
  39. package/dist/errors-BQuJpe82.js +2 -0
  40. package/dist/errors-BQuJpe82.js.map +1 -0
  41. package/dist/errors-W6nwJ-fm.d.ts +2 -1
  42. package/dist/errors-W6nwJ-fm.d.ts.map +1 -0
  43. package/dist/http.d.ts +121 -72
  44. package/dist/http.d.ts.map +1 -0
  45. package/dist/http.js +503 -178
  46. package/dist/http.js.map +1 -0
  47. package/dist/idempotent-replay-DuqEkYA7.js +2 -0
  48. package/dist/idempotent-replay-DuqEkYA7.js.map +1 -0
  49. package/dist/index.d.ts +1 -1
  50. package/dist/inspection-DaxB5jM2.js +2 -0
  51. package/dist/inspection-DaxB5jM2.js.map +1 -0
  52. package/dist/{internal-aEotMzu_.js → internal-DstsI6Re.js} +3 -1
  53. package/dist/internal-DstsI6Re.js.map +1 -0
  54. package/dist/otel.d.ts +2 -1
  55. package/dist/otel.d.ts.map +1 -0
  56. package/dist/otel.js +2 -0
  57. package/dist/otel.js.map +1 -0
  58. package/dist/platform-B4TnJtWu.js +2 -0
  59. package/dist/platform-B4TnJtWu.js.map +1 -0
  60. package/dist/react.d.ts +12 -3
  61. package/dist/react.d.ts.map +1 -0
  62. package/dist/react.js +5 -1
  63. package/dist/react.js.map +1 -0
  64. package/dist/retryable-lazy-DZWmHpii.js +2 -0
  65. package/dist/retryable-lazy-DZWmHpii.js.map +1 -0
  66. package/dist/scheduler-qstash.d.ts +4 -3
  67. package/dist/scheduler-qstash.d.ts.map +1 -0
  68. package/dist/scheduler-qstash.js +4 -2
  69. package/dist/scheduler-qstash.js.map +1 -0
  70. package/dist/scheduler-task-BpzhPnRS.js +2 -0
  71. package/dist/scheduler-task-BpzhPnRS.js.map +1 -0
  72. package/dist/scheduler-vercel.d.ts +4 -3
  73. package/dist/scheduler-vercel.d.ts.map +1 -0
  74. package/dist/scheduler-vercel.js +4 -2
  75. package/dist/scheduler-vercel.js.map +1 -0
  76. package/dist/{server-CcNnFnoW.js → server-C72KOw51.js} +100 -48
  77. package/dist/server-C72KOw51.js.map +1 -0
  78. package/dist/{server-YtPq7hjw.d.ts → server-DUF9pjsx.d.ts} +5 -4
  79. package/dist/server-DUF9pjsx.d.ts.map +1 -0
  80. package/dist/server.browser.js +2 -0
  81. package/dist/server.browser.js.map +1 -0
  82. package/dist/server.d.ts +2 -2
  83. package/dist/server.js +1 -1
  84. package/dist/{store-C3sNAaBT.d.ts → store-DysUkTH3.d.ts} +10 -1
  85. package/dist/store-DysUkTH3.d.ts.map +1 -0
  86. package/dist/store-N8PXxDAS.js +2 -0
  87. package/dist/store-N8PXxDAS.js.map +1 -0
  88. package/dist/store-codec-DTG0Ftek.js +2 -0
  89. package/dist/store-codec-DTG0Ftek.js.map +1 -0
  90. package/dist/store-memory.d.ts +3 -2
  91. package/dist/store-memory.d.ts.map +1 -0
  92. package/dist/store-memory.js +19 -11
  93. package/dist/store-memory.js.map +1 -0
  94. package/dist/{store-polling-DgrrAE3d.js → store-polling-dSeLxzfb.js} +3 -1
  95. package/dist/store-polling-dSeLxzfb.js.map +1 -0
  96. package/dist/store-postgres.d.ts +3 -2
  97. package/dist/store-postgres.d.ts.map +1 -0
  98. package/dist/store-postgres.js +57 -1
  99. package/dist/store-postgres.js.map +1 -0
  100. package/dist/{store-redis-core-DWqx3F47.js → store-redis-core-BFLwz0Wj.js} +3 -1
  101. package/dist/store-redis-core-BFLwz0Wj.js.map +1 -0
  102. package/dist/store-redis-http.d.ts +3 -2
  103. package/dist/store-redis-http.d.ts.map +1 -0
  104. package/dist/store-redis-http.js +4 -2
  105. package/dist/store-redis-http.js.map +1 -0
  106. package/dist/store-redis.d.ts +3 -2
  107. package/dist/store-redis.d.ts.map +1 -0
  108. package/dist/store-redis.js +5 -3
  109. package/dist/store-redis.js.map +1 -0
  110. package/dist/store-sqlite.d.ts +3 -2
  111. package/dist/store-sqlite.d.ts.map +1 -0
  112. package/dist/store-sqlite.js +3 -1
  113. package/dist/store-sqlite.js.map +1 -0
  114. package/dist/telemetry-BjYHTfh2.d.ts +2 -1
  115. package/dist/telemetry-BjYHTfh2.d.ts.map +1 -0
  116. package/dist/testing.browser.js +2 -0
  117. package/dist/testing.browser.js.map +1 -0
  118. package/dist/testing.d.ts +2 -1
  119. package/dist/testing.d.ts.map +1 -0
  120. package/dist/testing.js +2 -0
  121. package/dist/testing.js.map +1 -0
  122. package/dist/validate-XKT4FSNn.js +2 -0
  123. package/dist/validate-XKT4FSNn.js.map +1 -0
  124. package/dist/{wire-DCUZBUlT.js → wire-BFQmSJ-9.js} +77 -15
  125. package/dist/wire-BFQmSJ-9.js.map +1 -0
  126. package/docs/guides/03-react.mdx +59 -39
  127. package/docs/guides/06-ai-agents.mdx +5 -27
  128. package/docs/guides/09-presence.mdx +19 -40
  129. package/docs/guides/10-transports.mdx +49 -40
  130. package/docs/reference/01-api.mdx +107 -26
  131. package/docs/reference/02-errors.mdx +4 -2
  132. package/package.json +2 -1
  133. package/src/ai-coordinator.ts +358 -0
  134. package/src/ai-projector.ts +524 -0
  135. package/src/ai-sdk-step.ts +261 -0
  136. package/src/ai-server.browser.ts +5 -0
  137. package/src/ai-server.ts +1719 -0
  138. package/src/ai.ts +2155 -0
  139. package/src/cache-indexeddb.ts +10 -0
  140. package/src/cli-bin.ts +5 -0
  141. package/src/cli.ts +1046 -0
  142. package/src/client.ts +1826 -0
  143. package/src/contract.ts +206 -0
  144. package/src/deterministic-id.ts +72 -0
  145. package/src/devtools-app.ts +989 -0
  146. package/src/devtools-server.browser.ts +5 -0
  147. package/src/devtools-server.ts +604 -0
  148. package/src/devtools.ts +716 -0
  149. package/src/errors.ts +50 -0
  150. package/src/http.ts +394 -0
  151. package/src/idempotent-replay.ts +53 -0
  152. package/src/index.ts +37 -0
  153. package/src/inspection.ts +39 -0
  154. package/src/internal.ts +426 -0
  155. package/src/otel.ts +59 -0
  156. package/src/platform.ts +60 -0
  157. package/src/push-envelope.ts +137 -0
  158. package/src/react.ts +284 -0
  159. package/src/reducer.ts +108 -0
  160. package/src/retryable-lazy.ts +27 -0
  161. package/src/scheduler-qstash.ts +915 -0
  162. package/src/scheduler-task.ts +106 -0
  163. package/src/scheduler-vercel.ts +437 -0
  164. package/src/server.browser.ts +12 -0
  165. package/src/server.ts +2700 -0
  166. package/src/session-socket.ts +548 -0
  167. package/src/sse.ts +141 -0
  168. package/src/standard-schema.ts +77 -0
  169. package/src/store-codec.ts +10 -0
  170. package/src/store-memory.ts +788 -0
  171. package/src/store-polling.ts +102 -0
  172. package/src/store-postgres.ts +1212 -0
  173. package/src/store-redis-core.ts +1494 -0
  174. package/src/store-redis-http.ts +116 -0
  175. package/src/store-redis.ts +458 -0
  176. package/src/store-sqlite.ts +1108 -0
  177. package/src/store.ts +385 -0
  178. package/src/telemetry.ts +47 -0
  179. package/src/testing.browser.ts +5 -0
  180. package/src/testing.ts +185 -0
  181. package/src/validate.ts +39 -0
  182. package/src/wire.ts +454 -0
package/dist/http.js CHANGED
@@ -1,102 +1,15 @@
1
1
  import { t as A2Error } from "./errors-BQuJpe82.js";
2
- import { _ as schedulerForServer, c as STREAM_TIMINGS, d as assertServerSchedulerBindings, o as RESERVED_PARTICIPANT_IDS, s as SOCKET_TIMINGS, u as assertSchedulerTargets } from "./internal-aEotMzu_.js";
2
+ import { _ as schedulerForServer, c as STREAM_TIMINGS, d as assertServerSchedulerBindings, o as RESERVED_PARTICIPANT_IDS, s as SOCKET_TIMINGS, u as assertSchedulerTargets } from "./internal-DstsI6Re.js";
3
3
  import { t as invocationDeadlineMs } from "./platform-B4TnJtWu.js";
4
- import { _ as socketFrameFor, f as presencePatchToWire, g as socketErrorAckFor, h as socketAckFor, i as errorToWire, m as presenceSnapshotToWire, n as errorFromWire, o as eventToWire, r as errorStatus, t as SOCKET_PING_FRAME } from "./wire-DCUZBUlT.js";
5
- //#region src/http.ts
4
+ import { _ as socketErrorAckFor, a as errorToWire, b as socketUnsubscribedFor, g as socketAckFor, h as presenceSnapshotToWire, i as errorStatus, n as asA2Error, p as presencePatchToWire, r as errorFromWire, s as eventToWire, t as SOCKET_PING_FRAME, v as socketFrameFor, y as socketSubscribedFor } from "./wire-BFQmSJ-9.js";
5
+ //#region src/sse.ts
6
6
  /**
7
- * experimental-a2/http route-side transport helpers (specs/a2-api.md §9–10, §13).
8
- *
9
- * `sseResponse` pipes a `session.stream()` iterable into a server-sent
10
- * events Response; `schedulerHandler` mounts servers on their configured
11
- * scheduler callback; `parsePushBody` validates the push envelope;
12
- * `errorResponse`/`deserializeError` are the A2Error wire pair the push
13
- * route and the client's `push` share; `sessionSocket` speaks the same
14
- * lanes — stream down, push and presence up — over one WebSocket.
15
- */
16
- const invalid = (message) => new A2Error("INVALID_PAYLOAD", `malformed push body: ${message}`);
17
- /**
18
- * Create one scheduler callback route for servers sharing the same configured
19
- * scheduler. Configuration mistakes throw immediately while the route module
20
- * is evaluated, before a delivery can be acknowledged by the wrong adapter.
7
+ * The SSE response protocol framing, heartbeat, deadline, and
8
+ * teardown for the live stream lane. Internal module: `handle`'s
9
+ * stream branch is the one sanctioned caller; the framing rules stay
10
+ * documented on `sseResponse` because the client's parser (and the
11
+ * stall watchdog) depend on them.
21
12
  */
22
- function schedulerHandler(firstServer, ...servers) {
23
- if (firstServer === void 0) throw new TypeError("schedulerHandler() requires at least one A2 server");
24
- const scheduler = schedulerForServer(firstServer);
25
- const targets = [firstServer, ...servers];
26
- assertSchedulerTargets(targets, { allowStructural: false });
27
- assertServerSchedulerBindings(scheduler, targets);
28
- return scheduler.handler(...targets);
29
- }
30
- function parsePresenceSibling(value) {
31
- if (value === void 0) return void 0;
32
- if (value === null || typeof value !== "object" || Array.isArray(value)) throw invalid("presence must be an object when present");
33
- const { participant, values, seen, at } = value;
34
- if (typeof participant !== "string" || participant.length === 0) throw invalid("presence.participant must be a non-empty string");
35
- if (RESERVED_PARTICIPANT_IDS.has(participant)) throw invalid(`presence.participant must not be '${participant}'`);
36
- if (values === null || typeof values !== "object" || Array.isArray(values)) throw invalid("presence.values must be an object");
37
- if (seen !== void 0 && (typeof seen !== "number" || !Number.isFinite(seen) || seen < 0)) throw invalid("presence.seen must be a non-negative number when present");
38
- 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");
39
- const out = {
40
- participant,
41
- values
42
- };
43
- if (seen !== void 0) out.seen = seen;
44
- if (at !== void 0) out.at = at;
45
- Object.defineProperty(out, "~a2.pushed", { value: true });
46
- return out;
47
- }
48
- /** The events half of the envelope — shared verbatim by the POST body
49
- * and the socket's `push` frames, so the provenance brand and the
50
- * INVALID_PAYLOAD shapes are one implementation. */
51
- function parsePushEvents(events) {
52
- if (!Array.isArray(events) || events.length === 0) throw invalid("events must be a non-empty array");
53
- return events.map((event, i) => {
54
- if (event === null || typeof event !== "object") throw invalid(`events[${i}] must be an object`);
55
- const { type, payload, id } = event;
56
- if (typeof type !== "string" || type.length === 0) throw invalid(`events[${i}].type must be a non-empty string`);
57
- if (id !== void 0 && typeof id !== "string") throw invalid(`events[${i}].id must be a string when present`);
58
- const out = {
59
- type,
60
- payload
61
- };
62
- if (id !== void 0) out.id = id;
63
- Object.defineProperty(out, "~a2.pushed", { value: true });
64
- return out;
65
- });
66
- }
67
- /**
68
- * Validate the push envelope — `{ sessionId, events, presence? }` —
69
- * throwing `INVALID_PAYLOAD` on a malformed body. Payload validation
70
- * against the machine's schemas happens in `append` and `setPresence`,
71
- * not here. Either plane may appear alone: a presence-only push has no
72
- * (or empty) `events`.
73
- */
74
- async function parsePushBody(req) {
75
- let body;
76
- try {
77
- body = await req.json();
78
- } catch (cause) {
79
- throw new A2Error("INVALID_PAYLOAD", "push body is not valid JSON", { cause });
80
- }
81
- if (body === null || typeof body !== "object") throw invalid("expected an object");
82
- const { sessionId, events, presence } = body;
83
- if (typeof sessionId !== "string" || sessionId.length === 0) throw invalid("sessionId must be a non-empty string");
84
- const parsedPresence = parsePresenceSibling(presence);
85
- if (parsedPresence !== void 0 && (events === void 0 || Array.isArray(events) && events.length === 0)) return {
86
- sessionId,
87
- events: [],
88
- presence: parsedPresence
89
- };
90
- const parsed = parsePushEvents(events);
91
- return parsedPresence !== void 0 ? {
92
- sessionId,
93
- events: parsed,
94
- presence: parsedPresence
95
- } : {
96
- sessionId,
97
- events: parsed
98
- };
99
- }
100
13
  /**
101
14
  * Pipe a live event iterable into an SSE `Response`. Each event is one
102
15
  * frame — `id:` carries the event-log index, `data:` the JSON event. A
@@ -197,26 +110,97 @@ function frameFor(item) {
197
110
  if ("participant" in item) return `event: presence\ndata: ${JSON.stringify(presencePatchToWire(item))}\n\n`;
198
111
  return `id: ${item.index}\ndata: ${JSON.stringify(eventToWire(item))}\n\n`;
199
112
  }
113
+ //#endregion
114
+ //#region src/push-envelope.ts
200
115
  /**
201
- * Serialize an error into the documented wire shape,
202
- * `{ error: { code, message, details } }`, with the mapped status
203
- * (400 for caller bugs, 503 for STORE_UNAVAILABLE). Non-A2Errors become a
204
- * 503 STORE_UNAVAILABLE from the client's perspective an unknown server
205
- * failure is retryable-once, not a protocol contract.
116
+ * The push envelope's trust boundary parsing and provenance-branding
117
+ * for `{ sessionId, events, presence? }`, shared verbatim by the HTTP
118
+ * POST lane and the socket's push/presence frames so INVALID_PAYLOAD
119
+ * shapes are one implementation. Internal module `handle` (and the
120
+ * socket handler) compose it; it is not part of the public surface.
206
121
  */
207
- function errorResponse(error) {
208
- const a2error = asA2Error(error);
209
- return Response.json(errorToWire(a2error), { status: errorStatus(a2error.code) });
122
+ const invalidPushBody = (message) => new A2Error("INVALID_PAYLOAD", `malformed push body: ${message}`);
123
+ const invalid = invalidPushBody;
124
+ function parsePresenceSibling(value) {
125
+ if (value === void 0) return void 0;
126
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw invalid("presence must be an object when present");
127
+ const { participant, values, seen, at } = value;
128
+ if (typeof participant !== "string" || participant.length === 0) throw invalid("presence.participant must be a non-empty string");
129
+ if (RESERVED_PARTICIPANT_IDS.has(participant)) throw invalid(`presence.participant must not be '${participant}'`);
130
+ if (values === null || typeof values !== "object" || Array.isArray(values)) throw invalid("presence.values must be an object");
131
+ if (seen !== void 0 && (typeof seen !== "number" || !Number.isFinite(seen) || seen < 0)) throw invalid("presence.seen must be a non-negative number when present");
132
+ 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");
133
+ const out = {
134
+ participant,
135
+ values
136
+ };
137
+ if (seen !== void 0) out.seen = seen;
138
+ if (at !== void 0) out.at = at;
139
+ Object.defineProperty(out, "~a2.pushed", { value: true });
140
+ return out;
141
+ }
142
+ /** The events half of the envelope — shared verbatim by the POST body
143
+ * and the socket's `push` frames, so the provenance brand and the
144
+ * INVALID_PAYLOAD shapes are one implementation. */
145
+ function parsePushEvents(events) {
146
+ if (!Array.isArray(events) || events.length === 0) throw invalid("events must be a non-empty array");
147
+ return events.map((event, i) => {
148
+ if (event === null || typeof event !== "object") throw invalid(`events[${i}] must be an object`);
149
+ const { type, payload, id } = event;
150
+ if (typeof type !== "string" || type.length === 0) throw invalid(`events[${i}].type must be a non-empty string`);
151
+ if (id !== void 0 && typeof id !== "string") throw invalid(`events[${i}].id must be a string when present`);
152
+ const out = {
153
+ type,
154
+ payload
155
+ };
156
+ if (id !== void 0) out.id = id;
157
+ Object.defineProperty(out, "~a2.pushed", { value: true });
158
+ return out;
159
+ });
210
160
  }
211
- const asA2Error = (error) => error instanceof A2Error ? error : new A2Error("STORE_UNAVAILABLE", "internal error", { cause: error });
212
161
  /**
213
- * The other half of the wire pair: rebuild an `A2Error` from a response
214
- * body, or null if the body isn't one. Client `push` uses it so both
215
- * sides branch on identical codes.
162
+ * Validate the push envelope `{ sessionId, events, presence? }`
163
+ * throwing `INVALID_PAYLOAD` on a malformed body. Payload validation
164
+ * against the machine's schemas happens in `append` and `setPresence`,
165
+ * not here. Either plane may appear alone: a presence-only push has no
166
+ * (or empty) `events`.
216
167
  */
217
- function deserializeError(body) {
218
- return errorFromWire(body);
168
+ async function parsePushBody(req) {
169
+ let body;
170
+ try {
171
+ body = await req.json();
172
+ } catch (cause) {
173
+ throw new A2Error("INVALID_PAYLOAD", "push body is not valid JSON", { cause });
174
+ }
175
+ if (body === null || typeof body !== "object") throw invalid("expected an object");
176
+ const { sessionId, events, presence } = body;
177
+ if (typeof sessionId !== "string" || sessionId.length === 0) throw invalid("sessionId must be a non-empty string");
178
+ const parsedPresence = parsePresenceSibling(presence);
179
+ if (parsedPresence !== void 0 && (events === void 0 || Array.isArray(events) && events.length === 0)) return {
180
+ sessionId,
181
+ events: [],
182
+ presence: parsedPresence
183
+ };
184
+ const parsed = parsePushEvents(events);
185
+ return parsedPresence !== void 0 ? {
186
+ sessionId,
187
+ events: parsed,
188
+ presence: parsedPresence
189
+ } : {
190
+ sessionId,
191
+ events: parsed
192
+ };
219
193
  }
194
+ //#endregion
195
+ //#region src/session-socket.ts
196
+ /**
197
+ * The A2 socket protocol's server half (specs/a2-api.md §13): the
198
+ * per-socket shell — teardown, backpressure, heartbeat, the malformed
199
+ * budget, the in-flight bound — and `sessionsSocket`, the multiplexed
200
+ * frame handler riding it. Internal module: `handle`'s upgrade lane is
201
+ * the sanctioned mount; only `A2Socket` (the structural socket the
202
+ * platform hands over) is re-exported as public surface.
203
+ */
220
204
  const messageText = (data) => {
221
205
  if (typeof data === "string") return data;
222
206
  if (data instanceof ArrayBuffer) return new TextDecoder().decode(data);
@@ -237,23 +221,7 @@ const messageText = (data) => {
237
221
  * error acks, exactly like the POST path's 4xx bodies.
238
222
  */
239
223
  const MAX_MALFORMED_FRAMES = 5;
240
- /**
241
- * Speak the A2 socket protocol (specs/a2-api.md §13) against one
242
- * session: the live stream pumps down as `kind`-framed JSON with a
243
- * `ping` heartbeat on the SSE cadence; `push` frames come up through
244
- * the same validation seams as `parsePushBody` (same provenance brand,
245
- * same `validatePush` calls, once per plane) and are answered with an
246
- * ack; `presence` frames are fire-and-forget. Socket close or error
247
- * tears the pump down and releases the store subscription; the stream
248
- * ending closes the socket.
249
- */
250
- function sessionSocket(session, socket, options) {
251
- const target = session;
252
- const startAfter = options?.startAfter ?? 0;
253
- const iterator = (options?.presence === true ? target.stream({
254
- startAfter,
255
- presence: true
256
- }) : target.stream({ startAfter }))[Symbol.asyncIterator]();
224
+ function socketShell(socket, release, onFrame, deadlineAt) {
257
225
  let torn = false;
258
226
  let malformed = 0;
259
227
  let heartbeat;
@@ -263,7 +231,7 @@ function sessionSocket(session, socket, options) {
263
231
  torn = true;
264
232
  if (heartbeat !== void 0) clearInterval(heartbeat);
265
233
  if (deadline !== void 0) clearTimeout(deadline);
266
- iterator.return?.()?.catch(() => {});
234
+ release();
267
235
  };
268
236
  const shutdown = (code, reason) => {
269
237
  if (torn) return;
@@ -288,32 +256,6 @@ function sessionSocket(session, socket, options) {
288
256
  malformed += 1;
289
257
  if (malformed >= MAX_MALFORMED_FRAMES) shutdown(1008, `too many malformed frames (last: ${reason})`);
290
258
  };
291
- const handlePush = async (frame) => {
292
- const req = frame["req"];
293
- if (typeof req !== "number") {
294
- malformedFrame("push frame without a numeric req");
295
- return;
296
- }
297
- try {
298
- const events = parsePushEvents(frame["events"]);
299
- const appended = await target.append(...events);
300
- send(socketAckFor(req, appended));
301
- } catch (err) {
302
- send(socketErrorAckFor(req, asA2Error(err)));
303
- }
304
- };
305
- const handlePresence = async (frame) => {
306
- if (typeof target.setPresence !== "function") {
307
- malformedFrame("presence frame on a presence-less session");
308
- return;
309
- }
310
- try {
311
- const patch = parsePresenceSibling(frame);
312
- if (patch) await target.setPresence(patch);
313
- } catch {
314
- malformedFrame("invalid presence frame");
315
- }
316
- };
317
259
  let inFlight = 0;
318
260
  const track = (work) => {
319
261
  inFlight += 1;
@@ -339,43 +281,426 @@ function sessionSocket(session, socket, options) {
339
281
  malformedFrame("frame is not an object");
340
282
  return;
341
283
  }
342
- const frame = parsed;
343
- if (frame["kind"] === "push") {
344
- if (inFlight >= SOCKET_TIMINGS.maxInFlightMessages) {
345
- const req = frame["req"];
346
- if (typeof req === "number") send(socketErrorAckFor(req, new A2Error("STORE_UNAVAILABLE", "push shed: too many in flight on this socket")));
347
- else malformedFrame("push frame without a numeric req");
348
- return;
349
- }
350
- track(handlePush(frame));
351
- } else if (frame["kind"] === "presence") {
352
- if (inFlight >= SOCKET_TIMINGS.maxInFlightMessages) return;
353
- track(handlePresence(frame));
354
- }
284
+ onFrame(parsed);
355
285
  };
356
286
  socket.on("message", handleMessage);
357
287
  socket.on("close", teardown);
358
288
  socket.on("error", teardown);
359
289
  heartbeat = setInterval(() => send(SOCKET_PING_FRAME), STREAM_TIMINGS.sseHeartbeatMs);
360
290
  heartbeat.unref?.();
361
- if (options?.deadline !== void 0) {
362
- deadline = setTimeout(() => shutdown(1e3, "deadline"), Math.max(0, options.deadline - Date.now()));
291
+ if (deadlineAt !== void 0) {
292
+ deadline = setTimeout(() => shutdown(1e3, "deadline"), Math.max(0, deadlineAt - Date.now()));
363
293
  deadline.unref?.();
364
294
  }
365
- const buffered = () => typeof socket.bufferedAmount === "number" ? socket.bufferedAmount : 0;
366
- const pump = async () => {
295
+ return {
296
+ torn: () => torn,
297
+ send,
298
+ shutdown,
299
+ malformedFrame,
300
+ buffered: () => typeof socket.bufferedAmount === "number" ? socket.bufferedAmount : 0,
301
+ overCap: () => inFlight >= SOCKET_TIMINGS.maxInFlightMessages,
302
+ track
303
+ };
304
+ }
305
+ const stopSubscription = (subscription) => {
306
+ subscription.stopped = true;
307
+ subscription.iterator.return?.()?.catch(() => {});
308
+ };
309
+ const parseSubscribeEntries = (value) => {
310
+ if (!Array.isArray(value) || value.length === 0) return null;
311
+ const entries = [];
312
+ for (const entry of value) {
313
+ if (entry === null || typeof entry !== "object") return null;
314
+ const { id, index } = entry;
315
+ if (typeof id !== "string" || id.length === 0) return null;
316
+ if (typeof index !== "number" || !Number.isInteger(index) || index < 0) return null;
317
+ entries.push({
318
+ id,
319
+ index
320
+ });
321
+ }
322
+ return entries;
323
+ };
324
+ /**
325
+ * Speak the multiplexed A2 socket protocol (specs/a2-api.md §13)
326
+ * against many sessions on one socket. `subscribe` frames open a
327
+ * per-session pump at that session's own resume frontier; push and
328
+ * presence frames route by `sessionId` through the same validation
329
+ * seams as the POST lane; every down frame carries the `sessionId` it
330
+ * belongs to. One heartbeat, one
331
+ * malformed budget, one in-flight bound, one backpressure gauge — per
332
+ * socket, shared by all sessions. A session's stream ending or failing
333
+ * answers `unsubscribed` for that session; only the peer, the
334
+ * deadline, the malformed budget, or backpressure closes the socket.
335
+ */
336
+ function sessionsSocket(resolve, socket, options) {
337
+ const presence = options?.presence === true;
338
+ const subscriptions = /* @__PURE__ */ new Map();
339
+ const retire = (id, subscription) => {
340
+ if (subscriptions.get(id) === subscription) subscriptions.delete(id);
341
+ };
342
+ const pump = async (id, subscription) => {
367
343
  for (;;) {
368
344
  for (;;) {
369
- if (torn || buffered() <= SOCKET_TIMINGS.highWaterMarkBytes) break;
370
- await new Promise((resolve) => setTimeout(resolve, SOCKET_TIMINGS.resumePollMs));
345
+ if (shell.torn() || subscription.stopped || shell.buffered() <= SOCKET_TIMINGS.highWaterMarkBytes) break;
346
+ await new Promise((wake) => setTimeout(wake, SOCKET_TIMINGS.resumePollMs));
347
+ }
348
+ if (shell.torn() || subscription.stopped) return;
349
+ const { value, done } = await subscription.iterator.next();
350
+ if (shell.torn() || subscription.stopped) return;
351
+ if (done) return;
352
+ shell.send(socketFrameFor(value, id));
353
+ }
354
+ };
355
+ const runSubscription = async (id, subscription) => {
356
+ try {
357
+ await pump(id, subscription);
358
+ if (shell.torn() || subscription.stopped) return;
359
+ retire(id, subscription);
360
+ shell.send(socketUnsubscribedFor(id));
361
+ } catch {
362
+ if (shell.torn() || subscription.stopped) return;
363
+ retire(id, subscription);
364
+ shell.send(socketUnsubscribedFor(id, "stream failed"));
365
+ }
366
+ };
367
+ const startSubscription = async (id, index) => {
368
+ let session;
369
+ try {
370
+ session = await resolve(id, index);
371
+ } catch {
372
+ session = null;
373
+ }
374
+ if (shell.torn()) return;
375
+ if (session === null) {
376
+ shell.send(socketUnsubscribedFor(id, "subscribe rejected"));
377
+ return;
378
+ }
379
+ const target = session;
380
+ const existing = subscriptions.get(id);
381
+ if (existing) stopSubscription(existing);
382
+ const subscription = {
383
+ target,
384
+ iterator: (presence ? target.stream({
385
+ startAfter: index,
386
+ presence: true
387
+ }) : target.stream({ startAfter: index }))[Symbol.asyncIterator](),
388
+ stopped: false
389
+ };
390
+ subscriptions.set(id, subscription);
391
+ shell.send(socketSubscribedFor(id));
392
+ runSubscription(id, subscription);
393
+ };
394
+ let controlTail = Promise.resolve();
395
+ const control = (work) => {
396
+ controlTail = controlTail.then(work).catch(() => {});
397
+ };
398
+ const handlePush = async (frame, req, sessionId) => {
399
+ const subscription = subscriptions.get(sessionId);
400
+ if (subscription === void 0) {
401
+ shell.send(socketErrorAckFor(req, new A2Error("STORE_UNAVAILABLE", "session is not subscribed on this socket"), sessionId));
402
+ return;
403
+ }
404
+ try {
405
+ const events = parsePushEvents(frame["events"]);
406
+ const denial = await options?.gatePush?.(sessionId, events) ?? null;
407
+ if (denial !== null) {
408
+ shell.send(socketErrorAckFor(req, denial, sessionId));
409
+ return;
410
+ }
411
+ const appended = await subscription.target.append(...events);
412
+ shell.send(socketAckFor(req, appended, sessionId));
413
+ } catch (err) {
414
+ shell.send(socketErrorAckFor(req, asA2Error(err), sessionId));
415
+ }
416
+ };
417
+ const handlePresence = async (frame, sessionId) => {
418
+ const subscription = subscriptions.get(sessionId);
419
+ if (subscription === void 0) return;
420
+ if (typeof subscription.target.setPresence !== "function") {
421
+ shell.malformedFrame("presence frame on a presence-less session");
422
+ return;
423
+ }
424
+ try {
425
+ const patch = parsePresenceSibling(frame);
426
+ if (patch) await subscription.target.setPresence(patch);
427
+ } catch {
428
+ shell.malformedFrame("invalid presence frame");
429
+ }
430
+ };
431
+ const handleFrame = (frame) => {
432
+ switch (frame["kind"]) {
433
+ case "subscribe": {
434
+ const entries = parseSubscribeEntries(frame["sessions"]);
435
+ if (entries === null) {
436
+ shell.malformedFrame("malformed subscribe frame");
437
+ return;
438
+ }
439
+ for (const entry of entries) control(() => startSubscription(entry.id, entry.index));
440
+ return;
441
+ }
442
+ case "unsubscribe": {
443
+ const ids = frame["sessions"];
444
+ if (!Array.isArray(ids) || ids.length === 0 || !ids.every((id) => typeof id === "string")) {
445
+ shell.malformedFrame("malformed unsubscribe frame");
446
+ return;
447
+ }
448
+ for (const id of ids) control(() => {
449
+ const subscription = subscriptions.get(id);
450
+ if (subscription === void 0) return;
451
+ stopSubscription(subscription);
452
+ subscriptions.delete(id);
453
+ });
454
+ return;
455
+ }
456
+ case "push": {
457
+ const req = frame["req"];
458
+ const sessionId = frame["sessionId"];
459
+ if (shell.overCap()) {
460
+ 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));
461
+ else shell.malformedFrame("push frame without a numeric req");
462
+ return;
463
+ }
464
+ if (typeof req !== "number") {
465
+ shell.malformedFrame("push frame without a numeric req");
466
+ return;
467
+ }
468
+ if (typeof sessionId !== "string" || sessionId.length === 0) {
469
+ shell.send(socketErrorAckFor(req, new A2Error("INVALID_PAYLOAD", "push frame without a sessionId")));
470
+ return;
471
+ }
472
+ shell.track(handlePush(frame, req, sessionId));
473
+ return;
474
+ }
475
+ case "presence": {
476
+ if (shell.overCap()) return;
477
+ const sessionId = frame["sessionId"];
478
+ if (typeof sessionId !== "string" || sessionId.length === 0) {
479
+ shell.malformedFrame("presence frame without a sessionId");
480
+ return;
481
+ }
482
+ shell.track(handlePresence(frame, sessionId));
483
+ return;
484
+ }
485
+ default: return;
486
+ }
487
+ };
488
+ const shell = socketShell(socket, () => {
489
+ for (const subscription of subscriptions.values()) stopSubscription(subscription);
490
+ subscriptions.clear();
491
+ }, (frame) => handleFrame(frame), options?.deadline);
492
+ }
493
+ //#endregion
494
+ //#region src/http.ts
495
+ /**
496
+ * experimental-a2/http — the wire's server half (specs/a2-api.md §9–10, §13).
497
+ *
498
+ * A thin public assembly. The surface: `handle` — GET, POST, and the
499
+ * socket's frames parsed into intents (stream, history, push,
500
+ * ws-upgrade) with `before`/`after` hooks around them for policy —
501
+ * plus `A2Intent`/`A2Outcome`/`UpgradeFn`/`A2Socket` (its argument
502
+ * types), `errorResponse`/`deserializeError` (the A2Error wire pair
503
+ * both sides branch on), and `schedulerHandler` (the scheduler
504
+ * callback mount). The machinery lives in internal modules — the SSE
505
+ * framing in sse.ts, the push-envelope trust boundary in
506
+ * push-envelope.ts, the socket shell and multiplexed handler in
507
+ * session-socket.ts — because nothing outside `handle` is sanctioned
508
+ * to compose them. Policy — auth, caching, quotas — never enters this
509
+ * module: the hooks own it.
510
+ */
511
+ /**
512
+ * Create one scheduler callback route for servers sharing the same configured
513
+ * scheduler. Configuration mistakes throw immediately while the route module
514
+ * is evaluated, before a delivery can be acknowledged by the wrong adapter.
515
+ */
516
+ function schedulerHandler(firstServer, ...servers) {
517
+ if (firstServer === void 0) throw new TypeError("schedulerHandler() requires at least one A2 server");
518
+ const scheduler = schedulerForServer(firstServer);
519
+ const targets = [firstServer, ...servers];
520
+ assertSchedulerTargets(targets, { allowStructural: false });
521
+ assertServerSchedulerBindings(scheduler, targets);
522
+ return scheduler.handler(...targets);
523
+ }
524
+ /**
525
+ * Serialize an error into the documented wire shape,
526
+ * `{ error: { code, message, details } }`, with the mapped status
527
+ * (400 for caller bugs, 503 for STORE_UNAVAILABLE). Non-A2Errors become a
528
+ * 503 STORE_UNAVAILABLE — from the client's perspective an unknown server
529
+ * failure is retryable-once, not a protocol contract. The natural
530
+ * return value of a `before` hook that wants the wire's own error
531
+ * shapes.
532
+ */
533
+ function errorResponse(error) {
534
+ const a2error = asA2Error(error);
535
+ return Response.json(errorToWire(a2error), { status: errorStatus(a2error.code) });
536
+ }
537
+ /**
538
+ * The other half of the wire pair: rebuild an `A2Error` from a response
539
+ * body, or null if the body isn't one. Client `push` uses it so both
540
+ * sides branch on identical codes.
541
+ */
542
+ function deserializeError(body) {
543
+ return errorFromWire(body);
544
+ }
545
+ const parseHistoryBounds = (gte, lte) => {
546
+ if (gte === null || lte === null) throw new A2Error("INVALID_PAYLOAD", "a history read takes both gte and lte");
547
+ const bounds = {
548
+ gte: Number(gte),
549
+ lte: Number(lte)
550
+ };
551
+ 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`);
552
+ if (bounds.gte > bounds.lte) throw new A2Error("INVALID_PAYLOAD", "history gte must be less than or equal to lte");
553
+ return bounds;
554
+ };
555
+ const parseResumeIndex = (raw) => {
556
+ if (raw === null) return 0;
557
+ const index = Number(raw);
558
+ if (!Number.isSafeInteger(index) || index < 0) throw new A2Error("INVALID_PAYLOAD", "stream index must be a non-negative safe integer");
559
+ return index;
560
+ };
561
+ /**
562
+ * The session route pair as one call: `export const { GET, POST } =
563
+ * handle(server, options?)` serves the live stream (SSE GET, resumed
564
+ * at `index`), the history slice (GET with `gte`/`lte`), the push
565
+ * envelope (POST), and — with `options.upgrade` — the multiplexed
566
+ * WebSocket, all through one pair of policy hooks.
567
+ *
568
+ * Parsing is protocol, hooks are policy: a request that fails to parse
569
+ * (missing sessionId, malformed bounds, bad push envelope) answers
570
+ * INVALID_PAYLOAD on the wire before any hook runs. `before` sees
571
+ * every parsed intent — HTTP requests and socket frames alike, with
572
+ * `request` always the original (for socket frames: upgrade) Request —
573
+ * and short-circuits by returning a Response: over HTTP it is the
574
+ * response verbatim; over the socket it is translated to the wire's
575
+ * own vocabulary (subscribe → `unsubscribed { reason: 'subscribe
576
+ * rejected' }`, push → an INVALID_PAYLOAD error ack), because a
577
+ * Response body cannot cross a socket. `after` runs only where the
578
+ * library produced an HTTP response — never after a short-circuit,
579
+ * never for socket frames — and may mutate `response.headers` in place
580
+ * or return a replacement (history caching policy lives here, on
581
+ * `outcome.covered`). `options.deadline` (epoch ms) closes sockets
582
+ * cleanly ahead of a known platform deadline, so clients reconnect on
583
+ * our schedule.
584
+ */
585
+ function handle(server, options) {
586
+ const presence = options?.presence === true;
587
+ const before = async (request, intent) => {
588
+ const result = await options?.before?.({
589
+ request,
590
+ intent
591
+ });
592
+ return result instanceof Response ? result : null;
593
+ };
594
+ const decorate = async (request, intent, outcome, response) => {
595
+ const replaced = await options?.after?.({
596
+ request,
597
+ intent,
598
+ outcome,
599
+ response
600
+ });
601
+ return replaced instanceof Response ? replaced : response;
602
+ };
603
+ const attach = (request) => (socket) => {
604
+ sessionsSocket(async (sessionId, startAfter) => {
605
+ return await before(request, {
606
+ type: "stream",
607
+ sessionId,
608
+ startAfter,
609
+ transport: "ws"
610
+ }) === null ? server.session(sessionId) : null;
611
+ }, socket, {
612
+ presence,
613
+ ...options?.deadline === void 0 ? {} : { deadline: options.deadline },
614
+ gatePush: async (sessionId, events) => {
615
+ return await before(request, {
616
+ type: "push",
617
+ sessionId,
618
+ events,
619
+ transport: "ws"
620
+ }) === null ? null : new A2Error("INVALID_PAYLOAD", "push rejected");
621
+ }
622
+ });
623
+ };
624
+ return {
625
+ async GET(req) {
626
+ try {
627
+ if (req.headers.get("upgrade")?.toLowerCase() === "websocket") {
628
+ const short = await before(req, { type: "ws-upgrade" });
629
+ if (short !== null) return short;
630
+ if (options?.upgrade === void 0) return new Response("WebSocket upgrade requested, but handle() has no upgrade implementation — pass options.upgrade, e.g. { upgrade: (attach) => experimental_upgradeWebSocket(attach) } from '@vercel/functions'", { status: 426 });
631
+ return await options.upgrade(attach(req));
632
+ }
633
+ const { searchParams } = new URL(req.url);
634
+ const sessionId = searchParams.get("sessionId");
635
+ if (sessionId === null || sessionId.length === 0) throw new A2Error("INVALID_PAYLOAD", "missing sessionId");
636
+ const gteRaw = searchParams.get("gte");
637
+ const lteRaw = searchParams.get("lte");
638
+ if (gteRaw !== null || lteRaw !== null) {
639
+ const bounds = parseHistoryBounds(gteRaw, lteRaw);
640
+ const intent = {
641
+ type: "history",
642
+ sessionId,
643
+ ...bounds
644
+ };
645
+ const short = await before(req, intent);
646
+ if (short !== null) return short;
647
+ const events = await server.session(sessionId).history(bounds);
648
+ const covered = events.length === bounds.lte - bounds.gte + 1;
649
+ return await decorate(req, intent, {
650
+ type: "history",
651
+ covered,
652
+ events
653
+ }, Response.json(events.map(eventToWire)));
654
+ }
655
+ const startAfter = parseResumeIndex(searchParams.get("index"));
656
+ const intent = {
657
+ type: "stream",
658
+ sessionId,
659
+ startAfter,
660
+ transport: "sse"
661
+ };
662
+ const short = await before(req, intent);
663
+ if (short !== null) return short;
664
+ const target = server.session(sessionId);
665
+ const response = sseResponse(presence ? target.stream({
666
+ startAfter,
667
+ presence: true
668
+ }) : target.stream({ startAfter }));
669
+ return await decorate(req, intent, { type: "stream" }, response);
670
+ } catch (err) {
671
+ return errorResponse(err);
672
+ }
673
+ },
674
+ async POST(req) {
675
+ try {
676
+ const body = await parsePushBody(req);
677
+ const intent = {
678
+ type: "push",
679
+ sessionId: body.sessionId,
680
+ events: body.events,
681
+ ...body.presence === void 0 ? {} : { presence: body.presence },
682
+ transport: "http"
683
+ };
684
+ const short = await before(req, intent);
685
+ if (short !== null) return short;
686
+ const session = server.session(body.sessionId);
687
+ if (body.presence !== void 0) {
688
+ const target = session;
689
+ if (typeof target.setPresence !== "function") throw invalidPushBody("presence is not declared by this contract");
690
+ await target.setPresence(body.presence);
691
+ }
692
+ const appended = body.events.length === 0 ? [] : await session.append(...body.events);
693
+ return await decorate(req, intent, {
694
+ type: "push",
695
+ appended
696
+ }, Response.json(appended));
697
+ } catch (err) {
698
+ return errorResponse(err);
371
699
  }
372
- if (torn) return;
373
- const { value, done } = await iterator.next();
374
- if (done || torn) return;
375
- send(socketFrameFor(value));
376
700
  }
377
701
  };
378
- pump().then(() => shutdown(1e3), () => shutdown(1011, "stream failed"));
379
702
  }
380
703
  //#endregion
381
- export { deserializeError, errorResponse, parsePushBody, schedulerHandler, sessionSocket, sseResponse };
704
+ export { deserializeError, errorResponse, handle, schedulerHandler };
705
+
706
+ //# sourceMappingURL=http.js.map