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