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.
- package/CHANGELOG.md +51 -0
- package/dist/{ai-B4YhEnfw.d.ts → ai-CFNeCrRl.d.ts} +3 -2
- package/dist/ai-CFNeCrRl.d.ts.map +1 -0
- package/dist/ai-server.browser.js +2 -0
- package/dist/ai-server.browser.js.map +1 -0
- package/dist/ai-server.d.ts +4 -3
- package/dist/ai-server.d.ts.map +1 -0
- package/dist/ai-server.js +4 -2
- package/dist/ai-server.js.map +1 -0
- package/dist/ai.d.ts +1 -1
- package/dist/ai.js +3 -1
- package/dist/ai.js.map +1 -0
- package/dist/cli-B3VuxoDe.js +2 -0
- package/dist/cli-B3VuxoDe.js.map +1 -0
- package/dist/cli-bin.js +2 -0
- package/dist/cli-bin.js.map +1 -0
- package/dist/cli.d.ts +2 -1
- package/dist/cli.d.ts.map +1 -0
- package/dist/{client-Bt4tAKi9.js → client-BKlyLiOU.js} +295 -85
- package/dist/client-BKlyLiOU.js.map +1 -0
- package/dist/{client-BrfDXQ8A.d.ts → client-D7mvIXrF.d.ts} +40 -4
- package/dist/client-D7mvIXrF.d.ts.map +1 -0
- package/dist/client.d.ts +2 -2
- package/dist/client.js +1 -1
- package/dist/contract-48bUMgcL.js +2 -0
- package/dist/contract-48bUMgcL.js.map +1 -0
- package/dist/contract-jIfaR085.d.ts +2 -1
- package/dist/contract-jIfaR085.d.ts.map +1 -0
- package/dist/devtools-J_jZ2vQf.d.ts +2 -1
- package/dist/devtools-J_jZ2vQf.d.ts.map +1 -0
- package/dist/devtools-kJJaORn-.js +2 -0
- package/dist/devtools-kJJaORn-.js.map +1 -0
- package/dist/devtools-server.browser.js +2 -0
- package/dist/devtools-server.browser.js.map +1 -0
- package/dist/devtools-server.d.ts +2 -1
- package/dist/devtools-server.d.ts.map +1 -0
- package/dist/devtools-server.js +2 -0
- package/dist/devtools-server.js.map +1 -0
- package/dist/errors-BQuJpe82.js +2 -0
- package/dist/errors-BQuJpe82.js.map +1 -0
- package/dist/errors-W6nwJ-fm.d.ts +2 -1
- package/dist/errors-W6nwJ-fm.d.ts.map +1 -0
- package/dist/http.d.ts +121 -72
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +503 -178
- package/dist/http.js.map +1 -0
- package/dist/idempotent-replay-DuqEkYA7.js +2 -0
- package/dist/idempotent-replay-DuqEkYA7.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/inspection-DaxB5jM2.js +2 -0
- package/dist/inspection-DaxB5jM2.js.map +1 -0
- package/dist/{internal-aEotMzu_.js → internal-DstsI6Re.js} +3 -1
- package/dist/internal-DstsI6Re.js.map +1 -0
- package/dist/otel.d.ts +2 -1
- package/dist/otel.d.ts.map +1 -0
- package/dist/otel.js +2 -0
- package/dist/otel.js.map +1 -0
- package/dist/platform-B4TnJtWu.js +2 -0
- package/dist/platform-B4TnJtWu.js.map +1 -0
- package/dist/react.d.ts +12 -3
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +5 -1
- package/dist/react.js.map +1 -0
- package/dist/retryable-lazy-DZWmHpii.js +2 -0
- package/dist/retryable-lazy-DZWmHpii.js.map +1 -0
- package/dist/scheduler-qstash.d.ts +4 -3
- package/dist/scheduler-qstash.d.ts.map +1 -0
- package/dist/scheduler-qstash.js +4 -2
- package/dist/scheduler-qstash.js.map +1 -0
- package/dist/scheduler-task-BpzhPnRS.js +2 -0
- package/dist/scheduler-task-BpzhPnRS.js.map +1 -0
- package/dist/scheduler-vercel.d.ts +4 -3
- package/dist/scheduler-vercel.d.ts.map +1 -0
- package/dist/scheduler-vercel.js +4 -2
- package/dist/scheduler-vercel.js.map +1 -0
- package/dist/{server-CcNnFnoW.js → server-C72KOw51.js} +100 -48
- package/dist/server-C72KOw51.js.map +1 -0
- package/dist/{server-YtPq7hjw.d.ts → server-DUF9pjsx.d.ts} +5 -4
- package/dist/server-DUF9pjsx.d.ts.map +1 -0
- package/dist/server.browser.js +2 -0
- package/dist/server.browser.js.map +1 -0
- package/dist/server.d.ts +2 -2
- package/dist/server.js +1 -1
- package/dist/{store-C3sNAaBT.d.ts → store-DysUkTH3.d.ts} +10 -1
- package/dist/store-DysUkTH3.d.ts.map +1 -0
- package/dist/store-N8PXxDAS.js +2 -0
- package/dist/store-N8PXxDAS.js.map +1 -0
- package/dist/store-codec-DTG0Ftek.js +2 -0
- package/dist/store-codec-DTG0Ftek.js.map +1 -0
- package/dist/store-memory.d.ts +3 -2
- package/dist/store-memory.d.ts.map +1 -0
- package/dist/store-memory.js +19 -11
- package/dist/store-memory.js.map +1 -0
- package/dist/{store-polling-DgrrAE3d.js → store-polling-dSeLxzfb.js} +3 -1
- package/dist/store-polling-dSeLxzfb.js.map +1 -0
- package/dist/store-postgres.d.ts +3 -2
- package/dist/store-postgres.d.ts.map +1 -0
- package/dist/store-postgres.js +57 -1
- package/dist/store-postgres.js.map +1 -0
- package/dist/{store-redis-core-DWqx3F47.js → store-redis-core-BFLwz0Wj.js} +3 -1
- package/dist/store-redis-core-BFLwz0Wj.js.map +1 -0
- package/dist/store-redis-http.d.ts +3 -2
- package/dist/store-redis-http.d.ts.map +1 -0
- package/dist/store-redis-http.js +4 -2
- package/dist/store-redis-http.js.map +1 -0
- package/dist/store-redis.d.ts +3 -2
- package/dist/store-redis.d.ts.map +1 -0
- package/dist/store-redis.js +5 -3
- package/dist/store-redis.js.map +1 -0
- package/dist/store-sqlite.d.ts +3 -2
- package/dist/store-sqlite.d.ts.map +1 -0
- package/dist/store-sqlite.js +3 -1
- package/dist/store-sqlite.js.map +1 -0
- package/dist/telemetry-BjYHTfh2.d.ts +2 -1
- package/dist/telemetry-BjYHTfh2.d.ts.map +1 -0
- package/dist/testing.browser.js +2 -0
- package/dist/testing.browser.js.map +1 -0
- package/dist/testing.d.ts +2 -1
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +2 -0
- package/dist/testing.js.map +1 -0
- package/dist/validate-XKT4FSNn.js +2 -0
- package/dist/validate-XKT4FSNn.js.map +1 -0
- package/dist/{wire-DCUZBUlT.js → wire-BFQmSJ-9.js} +77 -15
- package/dist/wire-BFQmSJ-9.js.map +1 -0
- package/docs/guides/03-react.mdx +59 -39
- package/docs/guides/06-ai-agents.mdx +5 -27
- package/docs/guides/09-presence.mdx +19 -40
- package/docs/guides/10-transports.mdx +49 -40
- package/docs/reference/01-api.mdx +107 -26
- package/docs/reference/02-errors.mdx +4 -2
- package/package.json +2 -1
- package/src/ai-coordinator.ts +358 -0
- package/src/ai-projector.ts +524 -0
- package/src/ai-sdk-step.ts +261 -0
- package/src/ai-server.browser.ts +5 -0
- package/src/ai-server.ts +1719 -0
- package/src/ai.ts +2155 -0
- package/src/cache-indexeddb.ts +10 -0
- package/src/cli-bin.ts +5 -0
- package/src/cli.ts +1046 -0
- package/src/client.ts +1826 -0
- package/src/contract.ts +206 -0
- package/src/deterministic-id.ts +72 -0
- package/src/devtools-app.ts +989 -0
- package/src/devtools-server.browser.ts +5 -0
- package/src/devtools-server.ts +604 -0
- package/src/devtools.ts +716 -0
- package/src/errors.ts +50 -0
- package/src/http.ts +394 -0
- package/src/idempotent-replay.ts +53 -0
- package/src/index.ts +37 -0
- package/src/inspection.ts +39 -0
- package/src/internal.ts +426 -0
- package/src/otel.ts +59 -0
- package/src/platform.ts +60 -0
- package/src/push-envelope.ts +137 -0
- package/src/react.ts +284 -0
- package/src/reducer.ts +108 -0
- package/src/retryable-lazy.ts +27 -0
- package/src/scheduler-qstash.ts +915 -0
- package/src/scheduler-task.ts +106 -0
- package/src/scheduler-vercel.ts +437 -0
- package/src/server.browser.ts +12 -0
- package/src/server.ts +2700 -0
- package/src/session-socket.ts +548 -0
- package/src/sse.ts +141 -0
- package/src/standard-schema.ts +77 -0
- package/src/store-codec.ts +10 -0
- package/src/store-memory.ts +788 -0
- package/src/store-polling.ts +102 -0
- package/src/store-postgres.ts +1212 -0
- package/src/store-redis-core.ts +1494 -0
- package/src/store-redis-http.ts +116 -0
- package/src/store-redis.ts +458 -0
- package/src/store-sqlite.ts +1108 -0
- package/src/store.ts +385 -0
- package/src/telemetry.ts +47 -0
- package/src/testing.browser.ts +5 -0
- package/src/testing.ts +185 -0
- package/src/validate.ts +39 -0
- 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-
|
|
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
|
|
5
|
-
//#region src/
|
|
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
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
-
*
|
|
202
|
-
* `{
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
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
|
|
218
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
362
|
-
deadline = setTimeout(() => shutdown(1e3, "deadline"), Math.max(0,
|
|
291
|
+
if (deadlineAt !== void 0) {
|
|
292
|
+
deadline = setTimeout(() => shutdown(1e3, "deadline"), Math.max(0, deadlineAt - Date.now()));
|
|
363
293
|
deadline.unref?.();
|
|
364
294
|
}
|
|
365
|
-
|
|
366
|
-
|
|
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((
|
|
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,
|
|
704
|
+
export { deserializeError, errorResponse, handle, schedulerHandler };
|
|
705
|
+
|
|
706
|
+
//# sourceMappingURL=http.js.map
|