experimental-a2 0.9.0 → 0.10.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.
@@ -116,10 +116,9 @@ async function parsePushBody(req) {
116
116
  /**
117
117
  * The A2 socket protocol's server half (specs/a2-api.md §13): the
118
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.
119
+ * budget, and the in-flight bound. `sessionSocket` binds one session to
120
+ * one socket; `sessionsSocket` is the opt-in multiplexed handler. Only
121
+ * `A2Socket`, the structural socket the platform hands over, is public.
123
122
  */
124
123
  const messageText = (data) => {
125
124
  if (typeof data === "string") return data;
@@ -222,10 +221,101 @@ function socketShell(socket, release, onFrame, deadlineAt) {
222
221
  track
223
222
  };
224
223
  }
224
+ const socketSubscription = (target, startAfter, presence) => ({
225
+ target,
226
+ iterator: (presence ? target.stream({
227
+ startAfter,
228
+ presence: true
229
+ }) : target.stream({ startAfter }))[Symbol.asyncIterator](),
230
+ stopped: false
231
+ });
225
232
  const stopSubscription = (subscription) => {
226
233
  subscription.stopped = true;
227
234
  subscription.iterator.return?.()?.catch(() => {});
228
235
  };
236
+ const pumpSubscription = async (shell, subscription, frameFor) => {
237
+ for (;;) {
238
+ while (!shell.torn() && !subscription.stopped && shell.buffered() > SOCKET_TIMINGS.highWaterMarkBytes) await new Promise((wake) => setTimeout(wake, SOCKET_TIMINGS.resumePollMs));
239
+ if (shell.torn() || subscription.stopped) return;
240
+ const { value, done } = await subscription.iterator.next();
241
+ if (shell.torn() || subscription.stopped || done) return;
242
+ shell.send(frameFor(value));
243
+ }
244
+ };
245
+ const handleSocketPush = async (shell, target, sessionId, frame, req, ackSessionId, gatePush) => {
246
+ try {
247
+ const events = parsePushEvents(frame["events"]);
248
+ const denial = await gatePush?.(sessionId, events, void 0) ?? null;
249
+ if (denial !== null) {
250
+ shell.send(socketErrorAckFor(req, denial, ackSessionId));
251
+ return;
252
+ }
253
+ const appended = await target.append(...events);
254
+ shell.send(socketAckFor(req, appended, ackSessionId));
255
+ } catch (error) {
256
+ shell.send(socketErrorAckFor(req, asA2Error(error), ackSessionId));
257
+ }
258
+ };
259
+ const applySocketPresence = async (shell, target, sessionId, frame, gatePush) => {
260
+ if (typeof target.setPresence !== "function") {
261
+ shell.malformedFrame("presence frame on a presence-less session");
262
+ return;
263
+ }
264
+ let patch;
265
+ try {
266
+ patch = parsePresenceSibling(frame);
267
+ } catch {
268
+ shell.malformedFrame("invalid presence frame");
269
+ return;
270
+ }
271
+ if (!patch) return;
272
+ try {
273
+ if ((await gatePush?.(sessionId, [], patch) ?? null) !== null) return;
274
+ await target.setPresence(patch);
275
+ } catch (error) {
276
+ const failure = asA2Error(error);
277
+ if (failure.code === "INVALID_PAYLOAD" || failure.code === "UNKNOWN_PRESENCE_FIELD") {
278
+ shell.malformedFrame("invalid presence frame");
279
+ return;
280
+ }
281
+ shell.shutdown(1011, "internal error");
282
+ }
283
+ };
284
+ function sessionSocket(sessionId, session, socket, options) {
285
+ const target = session;
286
+ const subscription = socketSubscription(target, options?.startAfter ?? 0, options?.presence === true);
287
+ const handleFrame = (frame) => {
288
+ switch (frame["kind"]) {
289
+ case "push": {
290
+ const req = frame["req"];
291
+ if (typeof req !== "number") {
292
+ shell.malformedFrame("push frame without a numeric req");
293
+ return;
294
+ }
295
+ if (frame["sessionId"] !== void 0) {
296
+ shell.send(socketErrorAckFor(req, new A2Error("INVALID_PAYLOAD", "sessionId is not allowed on a single-session socket")));
297
+ return;
298
+ }
299
+ if (shell.overCap()) {
300
+ shell.send(socketErrorAckFor(req, new A2Error("STORE_UNAVAILABLE", "push shed: too many in flight on this socket")));
301
+ return;
302
+ }
303
+ shell.track(handleSocketPush(shell, target, sessionId, frame, req, void 0, options?.gatePush));
304
+ return;
305
+ }
306
+ case "presence":
307
+ if (frame["sessionId"] !== void 0) {
308
+ shell.malformedFrame("sessionId is not allowed on a single-session socket");
309
+ return;
310
+ }
311
+ if (!shell.overCap()) shell.track(applySocketPresence(shell, target, sessionId, frame, options?.gatePush));
312
+ return;
313
+ default: return;
314
+ }
315
+ };
316
+ const shell = socketShell(socket, () => stopSubscription(subscription), handleFrame, options?.deadline);
317
+ pumpSubscription(shell, subscription, socketFrameFor).then(() => shell.shutdown(1e3), () => shell.shutdown(1011, "stream failed"));
318
+ }
229
319
  const parseSubscribeEntries = (value) => {
230
320
  if (!Array.isArray(value) || value.length === 0) return null;
231
321
  const entries = [];
@@ -259,22 +349,9 @@ function sessionsSocket(resolve, socket, options) {
259
349
  const retire = (id, subscription) => {
260
350
  if (subscriptions.get(id) === subscription) subscriptions.delete(id);
261
351
  };
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
352
  const runSubscription = async (id, subscription) => {
276
353
  try {
277
- await pump(id, subscription);
354
+ await pumpSubscription(shell, subscription, (item) => socketFrameFor(item, id));
278
355
  if (shell.torn() || subscription.stopped) return;
279
356
  retire(id, subscription);
280
357
  shell.send(socketUnsubscribedFor(id));
@@ -300,14 +377,7 @@ function sessionsSocket(resolve, socket, options) {
300
377
  const target = session;
301
378
  const existing = subscriptions.get(id);
302
379
  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
- };
380
+ const subscription = socketSubscription(target, index, presence);
311
381
  subscriptions.set(id, subscription);
312
382
  shell.send(socketSubscribedFor(id));
313
383
  runSubscription(id, subscription);
@@ -322,45 +392,12 @@ function sessionsSocket(resolve, socket, options) {
322
392
  shell.send(socketErrorAckFor(req, new A2Error("STORE_UNAVAILABLE", "session is not subscribed on this socket"), sessionId));
323
393
  return;
324
394
  }
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
- }
395
+ await handleSocketPush(shell, subscription.target, sessionId, frame, req, sessionId, options?.gatePush);
337
396
  };
338
397
  const handlePresence = async (frame, sessionId) => {
339
398
  const subscription = subscriptions.get(sessionId);
340
399
  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
- }
400
+ await applySocketPresence(shell, subscription.target, sessionId, frame, options?.gatePush);
364
401
  };
365
402
  const handleFrame = (frame) => {
366
403
  switch (frame["kind"]) {
@@ -591,7 +628,20 @@ function createServerFetch(server) {
591
628
  const validateIngress = async (context) => {
592
629
  await serverFetchHooks.get(fetch)?.validateIngress?.(context);
593
630
  };
594
- const attach = (socket) => {
631
+ const gateSocketPush = async (sessionId, events, pushedPresence) => {
632
+ try {
633
+ await authorize(freezePush(sessionId, events, pushedPresence, "websocket"));
634
+ await validateIngress({
635
+ sessionId,
636
+ events,
637
+ ...pushedPresence === void 0 ? {} : { presence: pushedPresence }
638
+ });
639
+ return null;
640
+ } catch (error) {
641
+ return asA2Error(error);
642
+ }
643
+ };
644
+ const attachMultiplexed = (socket) => {
595
645
  const deadline = invocationDeadlineMs();
596
646
  sessionsSocket(async (sessionId, startAfter) => {
597
647
  return await authorized({
@@ -603,25 +653,34 @@ function createServerFetch(server) {
603
653
  }, socket, {
604
654
  presence,
605
655
  ...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
- }
656
+ gatePush: gateSocketPush
619
657
  });
620
658
  };
621
659
  try {
622
660
  if (request.method === "GET") {
623
661
  if (request.headers.get("upgrade")?.toLowerCase() === "websocket") {
624
662
  if (options?.upgradeWebSocket === void 0) return new Response("WebSocket upgrade requested, but server.fetch() has no upgrade implementation", { status: 426 });
663
+ if (options.multiplexWebSocket === true) return await options.upgradeWebSocket(attachMultiplexed);
664
+ const { searchParams } = new URL(request.url);
665
+ const sessionId = searchParams.get("sessionId");
666
+ if (sessionId === null || sessionId.length === 0) throw new A2Error("INVALID_PAYLOAD", "missing sessionId");
667
+ const startAfter = parseResumeIndex(searchParams.get("index"));
668
+ await authorize({
669
+ type: "stream",
670
+ sessionId,
671
+ startAfter,
672
+ transport: "websocket"
673
+ });
674
+ const target = server.session(sessionId);
675
+ const attach = (socket) => {
676
+ const deadline = invocationDeadlineMs();
677
+ sessionSocket(sessionId, target, socket, {
678
+ startAfter,
679
+ presence,
680
+ ...deadline === null ? {} : { deadline },
681
+ gatePush: gateSocketPush
682
+ });
683
+ };
625
684
  return await options.upgradeWebSocket(attach);
626
685
  }
627
686
  const { searchParams } = new URL(request.url);
@@ -2159,4 +2218,4 @@ function cloneInitial(initial) {
2159
2218
  //#endregion
2160
2219
  export { parsePresenceSibling as a, sseResponse as i, deliverSchedulerAppend as n, platformVercelOidcToken as o, setServerFetchHooks as r, createServer as t };
2161
2220
 
2162
- //# sourceMappingURL=server-CBET-jSz.js.map
2221
+ //# sourceMappingURL=server-DlLyvaSH.js.map