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.
- package/CHANGELOG.md +14 -0
- package/dist/actor-client.d.ts +1 -1
- package/dist/actor-react.d.ts +1 -1
- package/dist/{actor-shared-BACubf4x.d.ts → actor-shared-USo5MyuF.d.ts} +2 -2
- package/dist/{actor-shared-BACubf4x.d.ts.map → actor-shared-USo5MyuF.d.ts.map} +1 -1
- package/dist/actor.d.ts +2 -2
- package/dist/actor.js +1 -1
- package/dist/ai-server.d.ts +1 -1
- package/dist/ai-server.js +1 -1
- package/dist/ai.d.ts +1 -1
- package/dist/client.d.ts +13 -7
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +234 -54
- package/dist/client.js.map +1 -1
- package/dist/scheduler-qstash.d.ts +1 -1
- package/dist/scheduler-qstash.js +1 -1
- package/dist/scheduler-vercel.d.ts +1 -1
- package/dist/scheduler-vercel.js +1 -1
- package/dist/{server-CKY3_lbw.d.ts → server-DgCrSuhB.d.ts} +3 -1
- package/dist/server-DgCrSuhB.d.ts.map +1 -0
- package/dist/{server-CBET-jSz.js → server-DlLyvaSH.js} +135 -76
- package/dist/server-DlLyvaSH.js.map +1 -0
- package/dist/server.d.ts +1 -1
- package/dist/server.js +1 -1
- package/docs/guides/10-transports.mdx +72 -22
- package/docs/reference/01-api.mdx +30 -12
- package/examples/playground/package.json +1 -1
- package/package.json +1 -1
- package/src/client.ts +332 -87
- package/src/server-fetch.ts +51 -23
- package/src/session-socket.ts +216 -81
- package/dist/server-CBET-jSz.js.map +0 -1
- package/dist/server-CKY3_lbw.d.ts.map +0 -1
|
@@ -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
|
|
120
|
-
*
|
|
121
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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-
|
|
2221
|
+
//# sourceMappingURL=server-DlLyvaSH.js.map
|