statewire 0.16.0 → 0.17.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/README.md +12 -4
- package/dist/client/connection.d.ts +5 -1
- package/dist/client/connection.d.ts.map +1 -1
- package/dist/client/connection.js +19 -4
- package/dist/client/connection.js.map +1 -1
- package/dist/client/core.d.ts.map +1 -1
- package/dist/client/core.js +2 -0
- package/dist/client/core.js.map +1 -1
- package/dist/client/session.d.ts +15 -4
- package/dist/client/session.d.ts.map +1 -1
- package/dist/client/session.js +232 -50
- package/dist/client/session.js.map +1 -1
- package/dist/client/sessionStore.d.ts +5 -1
- package/dist/client/sessionStore.d.ts.map +1 -1
- package/dist/client/sessionStore.js +3 -1
- package/dist/client/sessionStore.js.map +1 -1
- package/dist/host/host.d.ts +1 -0
- package/dist/host/host.d.ts.map +1 -1
- package/dist/host/host.js.map +1 -1
- package/dist/host/http.d.ts.map +1 -1
- package/dist/host/http.js +27 -9
- package/dist/host/http.js.map +1 -1
- package/dist/host/socket.d.ts +10 -3
- package/dist/host/socket.d.ts.map +1 -1
- package/dist/host/socket.js +103 -39
- package/dist/host/socket.js.map +1 -1
- package/dist/host-internal.d.ts +9 -1
- package/dist/host-internal.d.ts.map +1 -1
- package/dist/host-internal.js +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/statewire.d.ts +33 -2
- package/dist/statewire.d.ts.map +1 -1
- package/dist/statewire.js +34 -3
- package/dist/statewire.js.map +1 -1
- package/dist/transports/fixtures.js +1 -0
- package/dist/transports/fixtures.js.map +1 -1
- package/dist/transports/remote.d.ts.map +1 -1
- package/dist/transports/remote.js +15 -8
- package/dist/transports/remote.js.map +1 -1
- package/dist/transports/sse.d.ts.map +1 -1
- package/dist/transports/sse.js +8 -8
- package/dist/transports/sse.js.map +1 -1
- package/dist/transports/testing.d.ts +1 -1
- package/dist/transports/testing.d.ts.map +1 -1
- package/dist/transports/testing.js +12 -4
- package/dist/transports/testing.js.map +1 -1
- package/dist/transports/ws.js +2 -2
- package/dist/transports/ws.js.map +1 -1
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/wire/documents.d.ts +25 -2
- package/dist/wire/documents.d.ts.map +1 -1
- package/dist/wire/documents.js +27 -7
- package/dist/wire/documents.js.map +1 -1
- package/dist/wire/protocol.d.ts +55 -0
- package/dist/wire/protocol.d.ts.map +1 -0
- package/dist/wire/protocol.js +221 -0
- package/dist/wire/protocol.js.map +1 -0
- package/dist/wire/version.d.ts +1 -3
- package/dist/wire/version.d.ts.map +1 -1
- package/dist/wire/version.js +1 -3
- package/dist/wire/version.js.map +1 -1
- package/package.json +1 -1
- package/src/client/connection.ts +43 -13
- package/src/client/core.ts +6 -0
- package/src/client/session.ts +338 -39
- package/src/client/sessionStore.ts +8 -0
- package/src/host/host.ts +1 -0
- package/src/host/http.ts +36 -7
- package/src/host/socket.ts +141 -50
- package/src/host-internal.ts +12 -1
- package/src/index.ts +3 -0
- package/src/statewire.ts +109 -6
- package/src/transports/fixtures.ts +1 -1
- package/src/transports/remote.ts +16 -7
- package/src/transports/sse.ts +18 -8
- package/src/transports/testing.ts +10 -2
- package/src/transports/ws.ts +1 -1
- package/src/types.ts +11 -0
- package/src/wire/documents.ts +126 -8
- package/src/wire/protocol.ts +327 -0
- package/src/wire/version.ts +0 -2
package/dist/host/http.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { negotiateProtocols } from "../wire/protocol.js";
|
|
1
2
|
import { MAX_FRAME_BODY_BYTES } from "../wire/limits.js";
|
|
2
|
-
import { VERSION_HEADER } from "../wire/version.js";
|
|
3
3
|
import { CLIENT_ID_RE, PRINCIPAL_MISMATCH_MESSAGE, SUBSCRIBER_LIMIT_MESSAGE, admitFrame, atSubscriberLimit, authEquals, closeConn, flush, parseFrame, parseFrameValue, register, sendTo } from "./socket.js";
|
|
4
4
|
//#region src/host/http.ts
|
|
5
5
|
const CLIENT_ID_HEADER = "Statewire-Client-Id";
|
|
@@ -66,7 +66,7 @@ const mintLease = () => {
|
|
|
66
66
|
const bytes = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16));
|
|
67
67
|
return btoa(String.fromCharCode(...bytes)).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
|
|
68
68
|
};
|
|
69
|
-
const attachSse = (h, clientId, auth) => {
|
|
69
|
+
const attachSse = (h, clientId, auth, selection) => {
|
|
70
70
|
let controller;
|
|
71
71
|
let connection = null;
|
|
72
72
|
let open = true;
|
|
@@ -109,7 +109,7 @@ const attachSse = (h, clientId, auth) => {
|
|
|
109
109
|
return;
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
|
-
}, false, auth);
|
|
112
|
+
}, false, auth, selection);
|
|
113
113
|
connection = registration?.connection ?? null;
|
|
114
114
|
if (!open) connection?.close();
|
|
115
115
|
return {
|
|
@@ -126,14 +126,13 @@ const streamResponse = (attachment, lease) => {
|
|
|
126
126
|
status: 200,
|
|
127
127
|
headers: {
|
|
128
128
|
"Content-Type": "text/event-stream",
|
|
129
|
-
[VERSION_HEADER]: "1",
|
|
130
129
|
...lease !== null && { [LEASE_HEADER]: lease }
|
|
131
130
|
}
|
|
132
131
|
});
|
|
133
132
|
};
|
|
134
133
|
const admitAttachFrame = (h, registration, frame) => {
|
|
135
134
|
if (frame.ctx.length === 0 && frame.cmd.length === 0) return;
|
|
136
|
-
const admission = admitFrame(h, registration.rec, frame, registration.conn.auth);
|
|
135
|
+
const admission = admitFrame(h, registration.rec, frame, registration.conn.auth, registration.conn.selection);
|
|
137
136
|
if (admission.type === "refused") sendTo(h, registration.conn, JSON.stringify(admission.syn));
|
|
138
137
|
};
|
|
139
138
|
const getStream = async (h, request) => {
|
|
@@ -144,7 +143,9 @@ const getStream = async (h, request) => {
|
|
|
144
143
|
const mismatch = principalMismatchResponse(h, client.clientId, authorized.auth);
|
|
145
144
|
if (mismatch !== null) return mismatch;
|
|
146
145
|
if (atSubscriberLimit(h)) return subscriberLimitResponse();
|
|
147
|
-
const
|
|
146
|
+
const selection = negotiateProtocols(request.headers, h.wireVersion, [h.protocol]);
|
|
147
|
+
if ("fin" in selection) return Response.json(selection, { status: selection.fin.status });
|
|
148
|
+
const attachment = attachSse(h, client.clientId, authorized.auth, selection);
|
|
148
149
|
const lease = attachment.registration === null ? null : mintLease();
|
|
149
150
|
if (attachment.registration !== null) attachment.registration.conn.lease = lease;
|
|
150
151
|
return streamResponse(attachment, lease);
|
|
@@ -175,11 +176,22 @@ const postStream = async (h, request) => {
|
|
|
175
176
|
const mismatch = principalMismatchResponse(h, client.clientId, authorized.auth);
|
|
176
177
|
if (mismatch !== null) return mismatch;
|
|
177
178
|
if (atSubscriberLimit(h)) return subscriberLimitResponse();
|
|
178
|
-
const
|
|
179
|
+
const selection = negotiateProtocols(request.headers, h.wireVersion, [h.protocol]);
|
|
180
|
+
if ("fin" in selection) return Response.json(selection, { status: selection.fin.status });
|
|
181
|
+
const attachment = attachSse(h, client.clientId, authorized.auth, selection);
|
|
179
182
|
if (attachment.registration === null) return streamResponse(attachment, null);
|
|
180
183
|
const lease = mintLease();
|
|
181
184
|
attachment.registration.conn.lease = lease;
|
|
182
|
-
|
|
185
|
+
try {
|
|
186
|
+
admitAttachFrame(h, attachment.registration, frame);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
closeConn(h, attachment.registration.conn, 1008, "invalid-context");
|
|
189
|
+
if (!(error instanceof Error) || error.name !== "StatewireViolation") throw error;
|
|
190
|
+
return Response.json({ fin: {
|
|
191
|
+
reason: "protocol-error",
|
|
192
|
+
message: error.message
|
|
193
|
+
} }, { status: 400 });
|
|
194
|
+
}
|
|
183
195
|
return streamResponse(attachment, lease);
|
|
184
196
|
};
|
|
185
197
|
const stream = async (h, request) => {
|
|
@@ -221,7 +233,13 @@ const frames = async (h, request) => {
|
|
|
221
233
|
return violationResponse(h, clientId, error.message);
|
|
222
234
|
}
|
|
223
235
|
if (!leased.open || h.disposed || h.finished !== null) return Response.json({ error: "stale-lease" }, { status: 423 });
|
|
224
|
-
|
|
236
|
+
let admission;
|
|
237
|
+
try {
|
|
238
|
+
admission = admitFrame(h, rec, frame, leased.auth, leased.selection);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
if (!(error instanceof Error) || error.name !== "StatewireViolation") throw error;
|
|
241
|
+
return violationResponse(h, clientId, error.message);
|
|
242
|
+
}
|
|
225
243
|
if (admission.type === "refused") {
|
|
226
244
|
const retry = admission.syn.syn.retry;
|
|
227
245
|
return Response.json(admission.syn, {
|
package/dist/host/http.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.js","names":["StatewireHost","CLIENT_ID_RE","PRINCIPAL_MISMATCH_MESSAGE","SUBSCRIBER_LIMIT_MESSAGE","admitFrame","atSubscriberLimit","authEquals","closeConn","flush","parseFrame","parseFrameValue","register","sendTo","ParsedFrame","Registration","SocketHostState","MAX_FRAME_BODY_BYTES","PROTOCOL_VERSION","VERSION_HEADER","CLIENT_ID_HEADER","LEASE_HEADER","encoder","TextEncoder","laggardFin","encode","readFrameBody","request","Request","Promise","declared","headers","get","Number","body","cancel","catch","reader","getReader","chunks","Uint8Array","size","done","value","read","length","push","offset","chunk","set","TextDecoder","decode","invalidClientIdReason","clientId","test","clientIdResponse","response","Response","json","detail","status","reason","headersOf","Record","Object","fromEntries","authorize","h","auth","refused","error","subscriberLimitResponse","principalMismatchResponse","rec","clients","undefined","principal","mintLease","bytes","crypto","getRandomValues","btoa","String","fromCharCode","replaceAll","replace","SseAttachment","ReadableStream","registration","activateBackpressure","attachSse","controller","ReadableStreamDefaultController","connection","open","backpressureActive","start","next","close","highWaterMark","streamQueueSize","socket","send","data","desiredSize","enqueue","frame","streamResponse","attachment","lease","admitAttachFrame","ctx","cmd","admission","conn","type","JSON","stringify","syn","getStream","authorized","client","mismatch","postStream","raw","trim","parse","Error","name","message","stream","method","Allow","finClient","fin","Fin","conns","violationResponse","frames","leased","find","disposed","finished","retry","Math","ceil"],"sources":["../../src/host/http.ts"],"sourcesContent":["import type { StatewireHost } from \"../host\";\nimport {\n CLIENT_ID_RE,\n PRINCIPAL_MISMATCH_MESSAGE,\n SUBSCRIBER_LIMIT_MESSAGE,\n admitFrame,\n atSubscriberLimit,\n authEquals,\n closeConn,\n flush,\n parseFrame,\n parseFrameValue,\n register,\n sendTo,\n} from \"./socket\";\nimport type { ParsedFrame, Registration, SocketHostState } from \"./socket\";\nimport { MAX_FRAME_BODY_BYTES } from \"../wire/limits\";\nimport { PROTOCOL_VERSION, VERSION_HEADER } from \"../wire/version\";\n\nexport { MAX_FRAME_BODY_BYTES } from \"../wire/limits\";\n\nconst CLIENT_ID_HEADER = \"Statewire-Client-Id\";\nconst LEASE_HEADER = \"Statewire-Lease\";\nconst encoder = new TextEncoder();\nconst laggardFin = encoder.encode(\n 'data: {\"fin\":{\"reason\":\"reconnect\",\"code\":\"client-fell-behind\"}}\\n\\n',\n);\nconst readFrameBody = async (request: Request): Promise<string | null> => {\n const declared = request.headers.get(\"content-length\");\n if (declared !== null && Number(declared) > MAX_FRAME_BODY_BYTES) {\n await request.body?.cancel(\"frame body too large\").catch(() => {});\n return null;\n }\n if (request.body === null) return \"\";\n const reader = request.body.getReader();\n const chunks: Uint8Array[] = [];\n let size = 0;\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n size += value.length;\n if (size > MAX_FRAME_BODY_BYTES) {\n await reader.cancel(\"frame body too large\").catch(() => {});\n return null;\n }\n chunks.push(value);\n }\n const body = new Uint8Array(size);\n let offset = 0;\n for (const chunk of chunks) {\n body.set(chunk, offset);\n offset += chunk.length;\n }\n return new TextDecoder().decode(body);\n};\n\nconst invalidClientIdReason = (clientId: string): string | null => {\n if (clientId === \"\") return \"id must be non-empty\";\n if (clientId.length > 256) return \"id must be at most 256 characters\";\n if (!CLIENT_ID_RE.test(clientId)) {\n return \"id must contain only [A-Za-z0-9._~-]\";\n }\n return null;\n};\n\nconst clientIdResponse = (\n request: Request,\n): { clientId: string } | { response: Response } => {\n const clientId = request.headers.get(CLIENT_ID_HEADER);\n if (clientId === null) {\n return {\n response: Response.json(\n { detail: `missing ${CLIENT_ID_HEADER} header` },\n { status: 400 },\n ),\n };\n }\n const reason = invalidClientIdReason(clientId);\n if (reason !== null) {\n return {\n response: Response.json(\n { detail: `invalid ${CLIENT_ID_HEADER}: ${reason}` },\n { status: 400 },\n ),\n };\n }\n return { clientId };\n};\n\nconst headersOf = (request: Request): Record<string, string> =>\n Object.fromEntries(request.headers);\n\nconst authorize = async (\n h: SocketHostState,\n headers: Record<string, string>,\n): Promise<{ auth: unknown } | { refused: Response }> => {\n try {\n return { auth: await h.authorize?.({ headers }) };\n } catch (error) {\n if (error instanceof Response) return { refused: error };\n throw error;\n }\n};\n\nconst subscriberLimitResponse = (): Response =>\n Response.json({ detail: SUBSCRIBER_LIMIT_MESSAGE }, { status: 503 });\n\n// No awaits between this check and register: the binding cannot race.\nconst principalMismatchResponse = (\n h: SocketHostState,\n clientId: string,\n auth: unknown,\n): Response | null => {\n const rec = h.clients.get(clientId);\n if (rec !== undefined && !authEquals(rec.principal, auth)) {\n return Response.json(\n { detail: PRINCIPAL_MISMATCH_MESSAGE },\n { status: 403 },\n );\n }\n return null;\n};\n\nconst mintLease = (): string => {\n const bytes = crypto.getRandomValues(new Uint8Array(16));\n return btoa(String.fromCharCode(...bytes))\n .replaceAll(\"+\", \"-\")\n .replaceAll(\"/\", \"_\")\n .replace(/=+$/, \"\");\n};\n\ntype SseAttachment = {\n body: ReadableStream<Uint8Array>;\n registration: Registration | null;\n activateBackpressure: () => void;\n};\n\nconst attachSse = (\n h: SocketHostState,\n clientId: string,\n auth: unknown,\n): SseAttachment => {\n let controller: ReadableStreamDefaultController<Uint8Array>;\n let connection: Registration[\"connection\"] | null = null;\n let open = true;\n let backpressureActive = false;\n const body = new ReadableStream(\n {\n start: (next) => {\n controller = next;\n },\n cancel: () => {\n open = false;\n connection?.close();\n },\n },\n { highWaterMark: h.streamQueueSize },\n );\n const socket = {\n send: (data: string) => {\n if (!open) return;\n if (\n backpressureActive &&\n controller.desiredSize !== null &&\n controller.desiredSize <= 0\n ) {\n open = false;\n try {\n controller.enqueue(laggardFin);\n controller.close();\n } catch {\n /* stream already gone */\n }\n connection?.close();\n return;\n }\n const frame = data === \"{}\" ? \": ping\\n\\n\" : `data: ${data}\\n\\n`;\n try {\n controller.enqueue(encoder.encode(frame));\n } catch {\n open = false;\n connection?.close();\n }\n },\n close: () => {\n if (!open) return;\n open = false;\n try {\n controller.close();\n } catch {\n return;\n }\n },\n };\n const registration = register(h, clientId, socket, false, auth);\n connection = registration?.connection ?? null;\n if (!open) connection?.close();\n return {\n body,\n registration,\n activateBackpressure: () => {\n backpressureActive = true;\n },\n };\n};\n\nconst streamResponse = (\n attachment: SseAttachment,\n lease: string | null,\n): Response => {\n attachment.activateBackpressure();\n return new Response(attachment.body, {\n status: 200,\n headers: {\n \"Content-Type\": \"text/event-stream\",\n [VERSION_HEADER]: PROTOCOL_VERSION,\n ...(lease !== null && { [LEASE_HEADER]: lease }),\n },\n });\n};\n\nconst admitAttachFrame = (\n h: SocketHostState,\n registration: Registration,\n frame: ParsedFrame,\n) => {\n if (frame.ctx.length === 0 && frame.cmd.length === 0) return;\n const admission = admitFrame(\n h,\n registration.rec,\n frame,\n registration.conn.auth,\n );\n if (admission.type === \"refused\") {\n sendTo(h, registration.conn, JSON.stringify(admission.syn));\n }\n};\n\nconst getStream = async (\n h: SocketHostState,\n request: Request,\n): Promise<Response> => {\n const authorized = await authorize(h, headersOf(request));\n if (\"refused\" in authorized) return authorized.refused;\n const client = clientIdResponse(request);\n if (\"response\" in client) return client.response;\n const mismatch = principalMismatchResponse(\n h,\n client.clientId,\n authorized.auth,\n );\n if (mismatch !== null) return mismatch;\n if (atSubscriberLimit(h)) return subscriberLimitResponse();\n const attachment = attachSse(h, client.clientId, authorized.auth);\n const lease = attachment.registration === null ? null : mintLease();\n if (attachment.registration !== null)\n attachment.registration.conn.lease = lease;\n return streamResponse(attachment, lease);\n};\n\nconst postStream = async (\n h: SocketHostState,\n request: Request,\n): Promise<Response> => {\n const client = clientIdResponse(request);\n if (\"response\" in client) return client.response;\n const raw = await readFrameBody(request);\n if (raw === null) {\n return Response.json({ detail: \"frame body too large\" }, { status: 413 });\n }\n let body: unknown;\n try {\n body = raw.trim() ? JSON.parse(raw) : null;\n } catch {\n return Response.json({ detail: \"body must be JSON\" }, { status: 400 });\n }\n let frame: ParsedFrame;\n try {\n frame =\n body === null && !raw.trim()\n ? { ctx: [], cmd: [] }\n : parseFrameValue(body);\n } catch (error) {\n if (!(error instanceof Error) || error.name !== \"StatewireViolation\") {\n throw error;\n }\n return Response.json({ detail: error.message }, { status: 400 });\n }\n const authorized = await authorize(h, headersOf(request));\n if (\"refused\" in authorized) return authorized.refused;\n const mismatch = principalMismatchResponse(\n h,\n client.clientId,\n authorized.auth,\n );\n if (mismatch !== null) return mismatch;\n if (atSubscriberLimit(h)) return subscriberLimitResponse();\n const attachment = attachSse(h, client.clientId, authorized.auth);\n if (attachment.registration === null) return streamResponse(attachment, null);\n const lease = mintLease();\n attachment.registration.conn.lease = lease;\n admitAttachFrame(h, attachment.registration, frame);\n return streamResponse(attachment, lease);\n};\n\nexport const stream = async (\n h: SocketHostState,\n request: Request,\n): Promise<Response> => {\n if (request.method === \"GET\") return getStream(h, request);\n if (request.method === \"POST\") return postStream(h, request);\n return new Response(null, {\n status: 405,\n headers: { Allow: \"GET, POST\" },\n });\n};\n\nconst finClient = (\n h: SocketHostState,\n clientId: string,\n fin: StatewireHost.Fin,\n) => {\n flush(h);\n for (const conn of [...h.conns]) {\n if (conn.clientId !== clientId) continue;\n sendTo(h, conn, JSON.stringify({ fin }));\n closeConn(h, conn);\n }\n};\n\nconst violationResponse = (\n h: SocketHostState,\n clientId: string,\n reason: string,\n) => {\n finClient(h, clientId, {\n reason: \"protocol-error\",\n message: reason,\n });\n return Response.json({ error: reason }, { status: 400 });\n};\n\nexport const frames = async (\n h: SocketHostState,\n request: Request,\n): Promise<Response> => {\n const lease = request.headers.get(LEASE_HEADER);\n // A lease lives and dies with its attach: any live attach of the id accepts.\n const leased =\n lease === null\n ? undefined\n : [...h.conns].find((conn) => conn.open && conn.lease === lease);\n if (h.disposed || h.finished !== null || leased === undefined) {\n return Response.json({ error: \"stale-lease\" }, { status: 423 });\n }\n const clientId = leased.clientId;\n const rec = h.clients.get(clientId)!;\n const raw = await readFrameBody(request);\n if (raw === null) {\n return Response.json({ error: \"frame body too large\" }, { status: 413 });\n }\n let frame: ParsedFrame;\n try {\n frame = parseFrame(raw);\n } catch (error) {\n if (!(error instanceof Error) || error.name !== \"StatewireViolation\") {\n throw error;\n }\n return violationResponse(h, clientId, error.message);\n }\n if (!leased.open || h.disposed || h.finished !== null) {\n return Response.json({ error: \"stale-lease\" }, { status: 423 });\n }\n const admission = admitFrame(h, rec, frame, leased.auth);\n if (admission.type === \"refused\") {\n const retry = admission.syn.syn.retry;\n return Response.json(admission.syn, {\n status: retry === undefined ? 409 : 429,\n ...(retry !== undefined && {\n headers: { \"Retry-After\": String(Math.ceil(retry / 1000)) },\n }),\n });\n }\n return Response.json({});\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"http.js","names":["StatewireHost","CLIENT_ID_RE","PRINCIPAL_MISMATCH_MESSAGE","SUBSCRIBER_LIMIT_MESSAGE","admitFrame","atSubscriberLimit","authEquals","closeConn","flush","parseFrame","parseFrameValue","register","sendTo","ParsedFrame","Registration","SocketHostState","MAX_FRAME_BODY_BYTES","negotiateProtocols","StatewireProtocol","CLIENT_ID_HEADER","LEASE_HEADER","encoder","TextEncoder","laggardFin","encode","readFrameBody","request","Request","Promise","declared","headers","get","Number","body","cancel","catch","reader","getReader","chunks","Uint8Array","size","done","value","read","length","push","offset","chunk","set","TextDecoder","decode","invalidClientIdReason","clientId","test","clientIdResponse","response","Response","json","detail","status","reason","headersOf","Record","Object","fromEntries","authorize","h","auth","refused","error","subscriberLimitResponse","principalMismatchResponse","rec","clients","undefined","principal","mintLease","bytes","crypto","getRandomValues","btoa","String","fromCharCode","replaceAll","replace","SseAttachment","ReadableStream","registration","activateBackpressure","attachSse","selection","Selection","controller","ReadableStreamDefaultController","connection","open","backpressureActive","start","next","close","highWaterMark","streamQueueSize","socket","send","data","desiredSize","enqueue","frame","streamResponse","attachment","lease","admitAttachFrame","ctx","cmd","admission","conn","type","JSON","stringify","syn","getStream","authorized","client","mismatch","wireVersion","protocol","fin","postStream","raw","trim","parse","Error","name","message","stream","method","Allow","finClient","Fin","conns","violationResponse","frames","leased","find","disposed","finished","ReturnType","retry","Math","ceil"],"sources":["../../src/host/http.ts"],"sourcesContent":["import type { StatewireHost } from \"../host\";\nimport {\n CLIENT_ID_RE,\n PRINCIPAL_MISMATCH_MESSAGE,\n SUBSCRIBER_LIMIT_MESSAGE,\n admitFrame,\n atSubscriberLimit,\n authEquals,\n closeConn,\n flush,\n parseFrame,\n parseFrameValue,\n register,\n sendTo,\n} from \"./socket\";\nimport type { ParsedFrame, Registration, SocketHostState } from \"./socket\";\nimport { MAX_FRAME_BODY_BYTES } from \"../wire/limits\";\nimport { negotiateProtocols } from \"../wire/protocol\";\nimport type { StatewireProtocol } from \"../wire/protocol\";\n\nexport { MAX_FRAME_BODY_BYTES } from \"../wire/limits\";\n\nconst CLIENT_ID_HEADER = \"Statewire-Client-Id\";\nconst LEASE_HEADER = \"Statewire-Lease\";\nconst encoder = new TextEncoder();\nconst laggardFin = encoder.encode(\n 'data: {\"fin\":{\"reason\":\"reconnect\",\"code\":\"client-fell-behind\"}}\\n\\n',\n);\nconst readFrameBody = async (request: Request): Promise<string | null> => {\n const declared = request.headers.get(\"content-length\");\n if (declared !== null && Number(declared) > MAX_FRAME_BODY_BYTES) {\n await request.body?.cancel(\"frame body too large\").catch(() => {});\n return null;\n }\n if (request.body === null) return \"\";\n const reader = request.body.getReader();\n const chunks: Uint8Array[] = [];\n let size = 0;\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n size += value.length;\n if (size > MAX_FRAME_BODY_BYTES) {\n await reader.cancel(\"frame body too large\").catch(() => {});\n return null;\n }\n chunks.push(value);\n }\n const body = new Uint8Array(size);\n let offset = 0;\n for (const chunk of chunks) {\n body.set(chunk, offset);\n offset += chunk.length;\n }\n return new TextDecoder().decode(body);\n};\n\nconst invalidClientIdReason = (clientId: string): string | null => {\n if (clientId === \"\") return \"id must be non-empty\";\n if (clientId.length > 256) return \"id must be at most 256 characters\";\n if (!CLIENT_ID_RE.test(clientId)) {\n return \"id must contain only [A-Za-z0-9._~-]\";\n }\n return null;\n};\n\nconst clientIdResponse = (\n request: Request,\n): { clientId: string } | { response: Response } => {\n const clientId = request.headers.get(CLIENT_ID_HEADER);\n if (clientId === null) {\n return {\n response: Response.json(\n { detail: `missing ${CLIENT_ID_HEADER} header` },\n { status: 400 },\n ),\n };\n }\n const reason = invalidClientIdReason(clientId);\n if (reason !== null) {\n return {\n response: Response.json(\n { detail: `invalid ${CLIENT_ID_HEADER}: ${reason}` },\n { status: 400 },\n ),\n };\n }\n return { clientId };\n};\n\nconst headersOf = (request: Request): Record<string, string> =>\n Object.fromEntries(request.headers);\n\nconst authorize = async (\n h: SocketHostState,\n headers: Record<string, string>,\n): Promise<{ auth: unknown } | { refused: Response }> => {\n try {\n return { auth: await h.authorize?.({ headers }) };\n } catch (error) {\n if (error instanceof Response) return { refused: error };\n throw error;\n }\n};\n\nconst subscriberLimitResponse = (): Response =>\n Response.json({ detail: SUBSCRIBER_LIMIT_MESSAGE }, { status: 503 });\n\n// No awaits between this check and register: the binding cannot race.\nconst principalMismatchResponse = (\n h: SocketHostState,\n clientId: string,\n auth: unknown,\n): Response | null => {\n const rec = h.clients.get(clientId);\n if (rec !== undefined && !authEquals(rec.principal, auth)) {\n return Response.json(\n { detail: PRINCIPAL_MISMATCH_MESSAGE },\n { status: 403 },\n );\n }\n return null;\n};\n\nconst mintLease = (): string => {\n const bytes = crypto.getRandomValues(new Uint8Array(16));\n return btoa(String.fromCharCode(...bytes))\n .replaceAll(\"+\", \"-\")\n .replaceAll(\"/\", \"_\")\n .replace(/=+$/, \"\");\n};\n\ntype SseAttachment = {\n body: ReadableStream<Uint8Array>;\n registration: Registration | null;\n activateBackpressure: () => void;\n};\n\nconst attachSse = (\n h: SocketHostState,\n clientId: string,\n auth: unknown,\n selection: StatewireProtocol.Selection,\n): SseAttachment => {\n let controller: ReadableStreamDefaultController<Uint8Array>;\n let connection: Registration[\"connection\"] | null = null;\n let open = true;\n let backpressureActive = false;\n const body = new ReadableStream(\n {\n start: (next) => {\n controller = next;\n },\n cancel: () => {\n open = false;\n connection?.close();\n },\n },\n { highWaterMark: h.streamQueueSize },\n );\n const socket = {\n send: (data: string) => {\n if (!open) return;\n if (\n backpressureActive &&\n controller.desiredSize !== null &&\n controller.desiredSize <= 0\n ) {\n open = false;\n try {\n controller.enqueue(laggardFin);\n controller.close();\n } catch {\n /* stream already gone */\n }\n connection?.close();\n return;\n }\n const frame = data === \"{}\" ? \": ping\\n\\n\" : `data: ${data}\\n\\n`;\n try {\n controller.enqueue(encoder.encode(frame));\n } catch {\n open = false;\n connection?.close();\n }\n },\n close: () => {\n if (!open) return;\n open = false;\n try {\n controller.close();\n } catch {\n return;\n }\n },\n };\n const registration = register(h, clientId, socket, false, auth, selection);\n connection = registration?.connection ?? null;\n if (!open) connection?.close();\n return {\n body,\n registration,\n activateBackpressure: () => {\n backpressureActive = true;\n },\n };\n};\n\nconst streamResponse = (\n attachment: SseAttachment,\n lease: string | null,\n): Response => {\n attachment.activateBackpressure();\n return new Response(attachment.body, {\n status: 200,\n headers: {\n \"Content-Type\": \"text/event-stream\",\n ...(lease !== null && { [LEASE_HEADER]: lease }),\n },\n });\n};\n\nconst admitAttachFrame = (\n h: SocketHostState,\n registration: Registration,\n frame: ParsedFrame,\n) => {\n if (frame.ctx.length === 0 && frame.cmd.length === 0) return;\n const admission = admitFrame(\n h,\n registration.rec,\n frame,\n registration.conn.auth,\n registration.conn.selection,\n );\n if (admission.type === \"refused\") {\n sendTo(h, registration.conn, JSON.stringify(admission.syn));\n }\n};\n\nconst getStream = async (\n h: SocketHostState,\n request: Request,\n): Promise<Response> => {\n const authorized = await authorize(h, headersOf(request));\n if (\"refused\" in authorized) return authorized.refused;\n const client = clientIdResponse(request);\n if (\"response\" in client) return client.response;\n const mismatch = principalMismatchResponse(\n h,\n client.clientId,\n authorized.auth,\n );\n if (mismatch !== null) return mismatch;\n if (atSubscriberLimit(h)) return subscriberLimitResponse();\n const selection = negotiateProtocols(request.headers, h.wireVersion, [\n h.protocol,\n ]);\n if (\"fin\" in selection)\n return Response.json(selection, { status: selection.fin.status });\n const attachment = attachSse(h, client.clientId, authorized.auth, selection);\n const lease = attachment.registration === null ? null : mintLease();\n if (attachment.registration !== null)\n attachment.registration.conn.lease = lease;\n return streamResponse(attachment, lease);\n};\n\nconst postStream = async (\n h: SocketHostState,\n request: Request,\n): Promise<Response> => {\n const client = clientIdResponse(request);\n if (\"response\" in client) return client.response;\n const raw = await readFrameBody(request);\n if (raw === null) {\n return Response.json({ detail: \"frame body too large\" }, { status: 413 });\n }\n let body: unknown;\n try {\n body = raw.trim() ? JSON.parse(raw) : null;\n } catch {\n return Response.json({ detail: \"body must be JSON\" }, { status: 400 });\n }\n let frame: ParsedFrame;\n try {\n frame =\n body === null && !raw.trim()\n ? { ctx: [], cmd: [] }\n : parseFrameValue(body);\n } catch (error) {\n if (!(error instanceof Error) || error.name !== \"StatewireViolation\") {\n throw error;\n }\n return Response.json({ detail: error.message }, { status: 400 });\n }\n const authorized = await authorize(h, headersOf(request));\n if (\"refused\" in authorized) return authorized.refused;\n const mismatch = principalMismatchResponse(\n h,\n client.clientId,\n authorized.auth,\n );\n if (mismatch !== null) return mismatch;\n if (atSubscriberLimit(h)) return subscriberLimitResponse();\n const selection = negotiateProtocols(request.headers, h.wireVersion, [\n h.protocol,\n ]);\n if (\"fin\" in selection)\n return Response.json(selection, { status: selection.fin.status });\n const attachment = attachSse(h, client.clientId, authorized.auth, selection);\n if (attachment.registration === null) return streamResponse(attachment, null);\n const lease = mintLease();\n attachment.registration.conn.lease = lease;\n try {\n admitAttachFrame(h, attachment.registration, frame);\n } catch (error) {\n closeConn(h, attachment.registration.conn, 1008, \"invalid-context\");\n if (!(error instanceof Error) || error.name !== \"StatewireViolation\")\n throw error;\n return Response.json(\n { fin: { reason: \"protocol-error\", message: error.message } },\n { status: 400 },\n );\n }\n return streamResponse(attachment, lease);\n};\n\nexport const stream = async (\n h: SocketHostState,\n request: Request,\n): Promise<Response> => {\n if (request.method === \"GET\") return getStream(h, request);\n if (request.method === \"POST\") return postStream(h, request);\n return new Response(null, {\n status: 405,\n headers: { Allow: \"GET, POST\" },\n });\n};\n\nconst finClient = (\n h: SocketHostState,\n clientId: string,\n fin: StatewireHost.Fin,\n) => {\n flush(h);\n for (const conn of [...h.conns]) {\n if (conn.clientId !== clientId) continue;\n sendTo(h, conn, JSON.stringify({ fin }));\n closeConn(h, conn);\n }\n};\n\nconst violationResponse = (\n h: SocketHostState,\n clientId: string,\n reason: string,\n) => {\n finClient(h, clientId, {\n reason: \"protocol-error\",\n message: reason,\n });\n return Response.json({ error: reason }, { status: 400 });\n};\n\nexport const frames = async (\n h: SocketHostState,\n request: Request,\n): Promise<Response> => {\n const lease = request.headers.get(LEASE_HEADER);\n // A lease lives and dies with its attach: any live attach of the id accepts.\n const leased =\n lease === null\n ? undefined\n : [...h.conns].find((conn) => conn.open && conn.lease === lease);\n if (h.disposed || h.finished !== null || leased === undefined) {\n return Response.json({ error: \"stale-lease\" }, { status: 423 });\n }\n const clientId = leased.clientId;\n const rec = h.clients.get(clientId)!;\n const raw = await readFrameBody(request);\n if (raw === null) {\n return Response.json({ error: \"frame body too large\" }, { status: 413 });\n }\n let frame: ParsedFrame;\n try {\n frame = parseFrame(raw);\n } catch (error) {\n if (!(error instanceof Error) || error.name !== \"StatewireViolation\") {\n throw error;\n }\n return violationResponse(h, clientId, error.message);\n }\n if (!leased.open || h.disposed || h.finished !== null) {\n return Response.json({ error: \"stale-lease\" }, { status: 423 });\n }\n let admission: ReturnType<typeof admitFrame>;\n try {\n admission = admitFrame(h, rec, frame, leased.auth, leased.selection);\n } catch (error) {\n if (!(error instanceof Error) || error.name !== \"StatewireViolation\")\n throw error;\n return violationResponse(h, clientId, error.message);\n }\n if (admission.type === \"refused\") {\n const retry = admission.syn.syn.retry;\n return Response.json(admission.syn, {\n status: retry === undefined ? 409 : 429,\n ...(retry !== undefined && {\n headers: { \"Retry-After\": String(Math.ceil(retry / 1000)) },\n }),\n });\n }\n return Response.json({});\n};\n"],"mappings":"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"}
|
package/dist/host/socket.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { StatewireDocuments } from "../wire/documents.js";
|
|
2
|
+
import { StatewireProtocol } from "../wire/protocol.js";
|
|
1
3
|
import { CommandsHost } from "./host.js";
|
|
2
4
|
import { StatewireHost } from "../host.js";
|
|
3
5
|
import { StatewireCmd } from "../client/connection.js";
|
|
@@ -32,6 +34,7 @@ type ClientRec = {
|
|
|
32
34
|
ctxSeq: number;
|
|
33
35
|
touched: number;
|
|
34
36
|
context: Record<string, unknown> | null;
|
|
37
|
+
contextDocuments: StatewireDocuments.State;
|
|
35
38
|
results: Map<number, {
|
|
36
39
|
ts: number;
|
|
37
40
|
cmd: StatewireCmd;
|
|
@@ -41,6 +44,7 @@ type ClientRec = {
|
|
|
41
44
|
principal: unknown;
|
|
42
45
|
};
|
|
43
46
|
type Conn = {
|
|
47
|
+
selection: StatewireProtocol.Selection;
|
|
44
48
|
clientId: string;
|
|
45
49
|
socket: StatewireHostInternal.Socket;
|
|
46
50
|
open: boolean;
|
|
@@ -60,9 +64,10 @@ type PendingConn = {
|
|
|
60
64
|
};
|
|
61
65
|
type CtxStatement = {
|
|
62
66
|
seq: number;
|
|
63
|
-
|
|
67
|
+
ops: unknown[];
|
|
64
68
|
};
|
|
65
69
|
type CmdStatement = {
|
|
70
|
+
protocol: string;
|
|
66
71
|
seq: number;
|
|
67
72
|
method: string;
|
|
68
73
|
params: unknown;
|
|
@@ -75,6 +80,8 @@ type ParsedFrame = {
|
|
|
75
80
|
declare const parseFrameValue: (frame: unknown) => ParsedFrame;
|
|
76
81
|
declare const parseFrame: (raw: string) => ParsedFrame;
|
|
77
82
|
type SocketHostState = {
|
|
83
|
+
protocol: StatewireProtocol.Definition;
|
|
84
|
+
wireVersion: StatewireProtocol.Range;
|
|
78
85
|
heartbeatMs: number;
|
|
79
86
|
wsInitTimeoutMs: number;
|
|
80
87
|
maxSubscribers: number;
|
|
@@ -121,13 +128,13 @@ type FrameAdmission = {
|
|
|
121
128
|
};
|
|
122
129
|
};
|
|
123
130
|
};
|
|
124
|
-
declare const admitFrame: (h: SocketHostState, rec: ClientRec, frame: ParsedFrame, auth: unknown) => FrameAdmission;
|
|
131
|
+
declare const admitFrame: (h: SocketHostState, rec: ClientRec, frame: ParsedFrame, auth: unknown, selection?: StatewireProtocol.Selection) => FrameAdmission;
|
|
125
132
|
type Registration = {
|
|
126
133
|
rec: ClientRec;
|
|
127
134
|
conn: Conn;
|
|
128
135
|
connection: StatewireHostInternal.SocketConnection;
|
|
129
136
|
};
|
|
130
|
-
declare const register: (h: SocketHostState, clientId: string, socket: StatewireHostInternal.Socket, sawFrame?: boolean, auth?: unknown) => Registration | null;
|
|
137
|
+
declare const register: (h: SocketHostState, clientId: string, socket: StatewireHostInternal.Socket, sawFrame?: boolean, auth?: unknown, selection?: StatewireProtocol.Selection) => Registration | null;
|
|
131
138
|
/**
|
|
132
139
|
* Transport-agnostic WS binding over a statewire/host transport:
|
|
133
140
|
* feed it sockets (anything with `send`/`close`) and raw text frames, and it
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"socket.d.ts","names":[],"sources":["../../src/host/socket.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"socket.d.ts","names":[],"sources":["../../src/host/socket.ts"],"mappings":";;;;;;;;;;;cAgCa,cAAY;cACZ;;;cAKA,aAAc,YAAY;cAoC1B;KAGR;EACH;EACA,OAAO;EACP;EACA,OAAO;;KAGJ;EACH;EACA;EACA;EACA;EACA;;KAGU;EACV;EACA;EACA;EACA;EACA,SAAS;EACT,kBAAkB,mBAAmB;EACrC,SAAS;IAAc;IAAY,KAAK;;EACxC,UAAU,YAAY;;EAEtB;;KAGU;EACV,WAAW,kBAAkB;EAC7B;EACA,QAAQ,sBAAsB;EAC9B;EACA;EACA;EACA;;EAEA;;KAGG;EACH,QAAQ,sBAAsB;EAC9B;EACA;EACA;EACA,OAAO,kBAAkB;EACzB,YAAY,sBAAsB;;KAc/B;EAAiB;EAAa;;KAC9B;EACH;EACA;EACA;EACA;EACA;;KAGU;EACV,KAAK;EACL,KAAK;;cA0BM,kBAAmB,mBAAiB;cAoDpC,aAAc,gBAAc;KAiG7B;EACV,UAAU,kBAAkB;EAC5B,aAAa,kBAAkB;EAC/B;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,WAAW,sBAAsB;EACjC,SAAS,YAAY;EACrB,OAAO,IAAI;EACX,cAAc,IAAI;EAClB,YAAY;EACZ,gBAAgB;EAChB,aAAa,YAAY;EACzB;EACA,UAAU,cAAc;EACxB;EACA;EACA,gBAAgB,kBAAkB;EAClC,WAAW,cAAc;EACzB,cAAc;EACd,SAAS;EACT,aAAa,sBAAsB;EACnC;EACA,UACE,kBACA,QAAQ,sBAAsB,QAC9B,UAAU,sBAAsB,yBAC7B,sBAAsB;EAC3B,SAAS,KAAK,cAAc;EAC5B,SAAS,SAAS,YAAY,QAAQ;EACtC,SAAS,SAAS,YAAY,QAAQ;;cA+C3B,YACX,GAAG,iBACH,MAAM,MACN,eACA;cAgBW,SAAU,GAAG,iBAAiB,MAAM,MAAM;cAU1C,oBAAqB,GAAG;cAkCxB,QAAS,GAAG,iBAAiB,MAAM,cAAc;KAsRlD;EACN;;EAEA;EACA;IACE;MAAO;MAAa;MAAgB;;;;cAI/B,aACX,GAAG,iBACH,KAAK,WACL,OAAO,aACP,eACA,YAAW,kBAAkB,cAI5B;KAsbS;EACV,KAAK;EACL,MAAM;EACN,YAAY,sBAAsB;;cAGvB,WACX,GAAG,iBACH,kBACA,QAAQ,sBAAsB,QAC9B,oBACA,gBACA,YAAW,kBAAkB,cAI5B;;;;;;;;;cA0QU,sBACX,WAAW,gBAAgB,YAC3B,UAAU,sBAAsB,mBAChC,cAAc,sBAAsB,0BACnC,sBAAsB"}
|
package/dist/host/socket.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { DEFAULT_PROTOCOL, WIRE_VERSION, negotiateProtocols } from "../wire/protocol.js";
|
|
2
|
+
import { encodeValue, reviveValue } from "../wire/meta.js";
|
|
3
|
+
import { applyDocumentOps, mainDocument } from "../wire/documents.js";
|
|
1
4
|
import { StatewireSendError } from "../types.js";
|
|
2
5
|
import { getCommandsHost, getStateHost } from "./host.js";
|
|
3
|
-
import { encodeValue, reviveValue } from "../wire/meta.js";
|
|
4
6
|
import { COMMAND_CAPACITY_RETRY_MS, MAX_INFLIGHT_COMMANDS_PER_HOST } from "../wire/limits.js";
|
|
5
7
|
import { createMacrotaskPoster } from "../util/macrotask.js";
|
|
6
8
|
import { frames, stream } from "./http.js";
|
|
@@ -49,15 +51,6 @@ const parseLane = (value) => {
|
|
|
49
51
|
return statements;
|
|
50
52
|
};
|
|
51
53
|
const frameEncoder = new TextEncoder();
|
|
52
|
-
/** v1 ctx statements state the context whole: one root replace with an object value. */
|
|
53
|
-
const contextValue = (ops) => {
|
|
54
|
-
if (!Array.isArray(ops) || ops.length !== 1) throw new Error("context must be a single-op array");
|
|
55
|
-
const op = ops[0];
|
|
56
|
-
if (!isPlainObject(op) || op["op"] !== "replace" || !Array.isArray(op["path"]) || op["path"].length !== 0) throw new Error("context op must be a root replace");
|
|
57
|
-
const value = op["meta"] !== void 0 ? reviveValue(op["value"], op["meta"], { rejectUnknown: true }) : op["value"];
|
|
58
|
-
if (!isPlainObject(value)) throw new Error("context value must be an object");
|
|
59
|
-
return value;
|
|
60
|
-
};
|
|
61
54
|
const parseFrameValue = (frame) => {
|
|
62
55
|
if (!isPlainObject(frame)) return violation("malformed frame");
|
|
63
56
|
const keys = Object.keys(frame);
|
|
@@ -69,16 +62,12 @@ const parseFrameValue = (frame) => {
|
|
|
69
62
|
const lane = frame["ctx"];
|
|
70
63
|
if (Array.isArray(lane) && lane.length > 256) return violation("too many context statements");
|
|
71
64
|
for (const statement of parseLane(lane)) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
} catch {
|
|
76
|
-
return violation("invalid-context");
|
|
77
|
-
}
|
|
78
|
-
if (frameEncoder.encode(JSON.stringify(value)).byteLength > 65536) return violation("oversized-context");
|
|
65
|
+
const ops = statement["ops"];
|
|
66
|
+
if (!Array.isArray(ops) || !ops.length) return violation("invalid-context");
|
|
67
|
+
if (frameEncoder.encode(JSON.stringify(ops)).byteLength > 65536) return violation("oversized-context");
|
|
79
68
|
ctx.push({
|
|
80
69
|
seq: statement["seq"],
|
|
81
|
-
|
|
70
|
+
ops
|
|
82
71
|
});
|
|
83
72
|
}
|
|
84
73
|
}
|
|
@@ -88,8 +77,9 @@ const parseFrameValue = (frame) => {
|
|
|
88
77
|
if (Array.isArray(lane) && lane.length > 256) return violation("too many commands");
|
|
89
78
|
for (const statement of parseLane(lane)) {
|
|
90
79
|
const method = statement["method"];
|
|
91
|
-
if (typeof method !== "string" || !("params" in statement)) return violation("malformed statement");
|
|
80
|
+
if (typeof method !== "string" || typeof statement["protocol"] !== "string" || !("params" in statement)) return violation("malformed statement");
|
|
92
81
|
cmd.push({
|
|
82
|
+
protocol: statement["protocol"],
|
|
93
83
|
seq: statement["seq"],
|
|
94
84
|
method,
|
|
95
85
|
params: statement["params"],
|
|
@@ -277,13 +267,16 @@ const flush = (h, fin) => {
|
|
|
277
267
|
}));
|
|
278
268
|
if (ops.length > 0) {
|
|
279
269
|
const packet = {
|
|
280
|
-
ops,
|
|
270
|
+
ops: conn.selection.protocols.some((p) => p.name === h.protocol.name) ? ops : [],
|
|
281
271
|
...appliedEntries.length > 0 && { cmd: appliedEntries },
|
|
282
272
|
...edge && { idle: level }
|
|
283
273
|
};
|
|
284
274
|
sendTo(h, conn, JSON.stringify(packet));
|
|
285
275
|
} else if (appliedEntries.length > 0) sendTo(h, conn, JSON.stringify({ cmd: appliedEntries }));
|
|
286
|
-
const verdicts = cmds.get(conn.clientId) ?? []
|
|
276
|
+
const verdicts = (cmds.get(conn.clientId) ?? []).map((cmd) => conn.selection.protocols.some((p) => p.name === h.protocol.name) ? cmd : {
|
|
277
|
+
seq: cmd.seq,
|
|
278
|
+
...cmd.dur === void 0 ? {} : { dur: cmd.dur }
|
|
279
|
+
});
|
|
287
280
|
if (verdicts.length > 0) sendTo(h, conn, JSON.stringify({ cmd: verdicts }));
|
|
288
281
|
if (edge && ops.length === 0) sendTo(h, conn, JSON.stringify({ idle: level }));
|
|
289
282
|
if (fin !== void 0) {
|
|
@@ -311,6 +304,7 @@ const notifyClientRecords = (h) => {
|
|
|
311
304
|
ctxSeq: rec.ctxSeq,
|
|
312
305
|
touched: rec.touched,
|
|
313
306
|
context: rec.context,
|
|
307
|
+
contextDocuments: [...rec.contextDocuments.documents],
|
|
314
308
|
results: [...rec.results].map(([seq, entry]) => ({
|
|
315
309
|
seq,
|
|
316
310
|
ts: entry.ts,
|
|
@@ -380,7 +374,7 @@ const rejectStatement = (h, rec, adm, message) => settleCmd(h, rec, adm, {
|
|
|
380
374
|
code: "invalid-params",
|
|
381
375
|
message
|
|
382
376
|
});
|
|
383
|
-
const admitCmd = (h, rec, statement, auth) => {
|
|
377
|
+
const admitCmd = (h, rec, statement, auth, selection) => {
|
|
384
378
|
const adm = {
|
|
385
379
|
seq: statement.seq,
|
|
386
380
|
method: statement.method,
|
|
@@ -390,6 +384,15 @@ const admitCmd = (h, rec, statement, auth) => {
|
|
|
390
384
|
};
|
|
391
385
|
rec.inflight.set(statement.seq, adm);
|
|
392
386
|
notifyClientRecords(h);
|
|
387
|
+
if (statement.protocol !== h.protocol.name || !selection.protocols.some((p) => p.name === statement.protocol)) {
|
|
388
|
+
settleCmd(h, rec, adm, {
|
|
389
|
+
seq: adm.seq,
|
|
390
|
+
type: "rejected",
|
|
391
|
+
code: "unsupported-protocol",
|
|
392
|
+
message: "command protocol was not negotiated"
|
|
393
|
+
});
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
393
396
|
if (!Array.isArray(statement.params)) {
|
|
394
397
|
rejectStatement(h, rec, adm, "params must be an array");
|
|
395
398
|
return;
|
|
@@ -434,7 +437,13 @@ const admitCmd = (h, rec, statement, auth) => {
|
|
|
434
437
|
});
|
|
435
438
|
});
|
|
436
439
|
};
|
|
437
|
-
const admitFrame = (h, rec, frame, auth
|
|
440
|
+
const admitFrame = (h, rec, frame, auth, selection = {
|
|
441
|
+
version: h.wireVersion.version,
|
|
442
|
+
protocols: [{
|
|
443
|
+
name: h.protocol.name,
|
|
444
|
+
version: h.protocol.version
|
|
445
|
+
}]
|
|
446
|
+
}) => {
|
|
438
447
|
rec.touched = Date.now();
|
|
439
448
|
const through = admittedThrough(rec);
|
|
440
449
|
for (const statement of frame.cmd) if (statement.seq <= through) reemit(h, rec, statement.seq);
|
|
@@ -476,11 +485,17 @@ const admitFrame = (h, rec, frame, auth) => {
|
|
|
476
485
|
};
|
|
477
486
|
}
|
|
478
487
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
488
|
+
let contextDocuments = rec.contextDocuments;
|
|
489
|
+
try {
|
|
490
|
+
for (const statement of ctxNew) contextDocuments = applyDocumentOps(contextDocuments, statement.ops, selection.protocols, true);
|
|
491
|
+
if (frameEncoder.encode(JSON.stringify([...contextDocuments.documents])).byteLength > 65536) return violation("oversized-context");
|
|
492
|
+
} catch {
|
|
493
|
+
return violation("invalid-context");
|
|
482
494
|
}
|
|
483
|
-
|
|
495
|
+
rec.contextDocuments = contextDocuments;
|
|
496
|
+
rec.context = mainDocument(contextDocuments, h.protocol.name)?.value ?? null;
|
|
497
|
+
if (ctxNew.length) rec.ctxSeq = ctxNew.at(-1).seq;
|
|
498
|
+
for (const statement of cmdNew) admitCmd(h, rec, statement, auth, selection);
|
|
484
499
|
if (cmdNew.length > 0) queueCmd(h, rec.id, {
|
|
485
500
|
seq: cmdNew.at(-1).seq,
|
|
486
501
|
dur: true
|
|
@@ -516,7 +531,19 @@ const message = (h, conn, data) => {
|
|
|
516
531
|
}
|
|
517
532
|
if (frame.ctx.length === 0 && frame.cmd.length === 0) return;
|
|
518
533
|
const rec = h.clients.get(conn.clientId);
|
|
519
|
-
|
|
534
|
+
let admission;
|
|
535
|
+
try {
|
|
536
|
+
admission = admitFrame(h, rec, frame, conn.auth, conn.selection);
|
|
537
|
+
} catch (error) {
|
|
538
|
+
const reason = violationReason(error);
|
|
539
|
+
if (reason === null) throw error;
|
|
540
|
+
sendTo(h, conn, JSON.stringify({ fin: {
|
|
541
|
+
reason: "protocol-error",
|
|
542
|
+
message: reason
|
|
543
|
+
} }));
|
|
544
|
+
closeConn(h, conn, 1008, reason);
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
520
547
|
if (admission.type === "refused") sendTo(h, conn, JSON.stringify(admission.syn));
|
|
521
548
|
};
|
|
522
549
|
const closePending = (h, pending, code, reason) => {
|
|
@@ -573,7 +600,7 @@ const normalizedHeaders = (headers) => {
|
|
|
573
600
|
};
|
|
574
601
|
const connectAuthorized = (h, clientId, socket, options) => {
|
|
575
602
|
const authorize = h.authorize;
|
|
576
|
-
if (options.authorized === true) return register(h, clientId, socket, options.authorized === true, options.auth)?.connection ?? null;
|
|
603
|
+
if (options.authorized === true) return register(h, clientId, socket, options.authorized === true, options.auth, options.selection)?.connection ?? null;
|
|
577
604
|
const refuse = (code, reason) => {
|
|
578
605
|
try {
|
|
579
606
|
socket.close(code, reason);
|
|
@@ -640,7 +667,10 @@ const connectAuthorized = (h, clientId, socket, options) => {
|
|
|
640
667
|
const frame = initial.frame;
|
|
641
668
|
let headers;
|
|
642
669
|
try {
|
|
643
|
-
headers =
|
|
670
|
+
headers = {
|
|
671
|
+
...normalizedHeaders(initial.headers),
|
|
672
|
+
...normalizedHeaders(options.headers ?? {})
|
|
673
|
+
};
|
|
644
674
|
} catch {
|
|
645
675
|
sendPending(pending, JSON.stringify({ fin: {
|
|
646
676
|
reason: "protocol-error",
|
|
@@ -661,17 +691,23 @@ const connectAuthorized = (h, clientId, socket, options) => {
|
|
|
661
691
|
pending.authorizing = true;
|
|
662
692
|
Promise.resolve().then(() => authorize?.({ headers })).then((auth) => {
|
|
663
693
|
if (!pending.open || h.disposed || h.finished !== null) return;
|
|
664
|
-
|
|
694
|
+
const selection = negotiateProtocols(new Headers(headers), h.wireVersion, [h.protocol]);
|
|
695
|
+
if ("fin" in selection) {
|
|
696
|
+
sendPending(pending, JSON.stringify(selection));
|
|
697
|
+
closePending(h, pending, 1008, selection.fin.message);
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
options.onAuthorized?.(auth, selection);
|
|
665
701
|
pending.open = false;
|
|
666
702
|
h.pendingConns.delete(pending);
|
|
667
703
|
if (pending.timer !== null) clearTimeout(pending.timer);
|
|
668
|
-
const registration = register(h, clientId, socket, true, auth);
|
|
704
|
+
const registration = register(h, clientId, socket, true, auth, selection);
|
|
669
705
|
if (registration === null) return;
|
|
670
706
|
pending.connection = registration.connection;
|
|
671
|
-
if (frame.ctx.length || frame.cmd.length) {
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
}
|
|
707
|
+
if (frame.ctx.length || frame.cmd.length) message(h, registration.conn, JSON.stringify({
|
|
708
|
+
...frame.ctx.length ? { ctx: frame.ctx } : {},
|
|
709
|
+
...frame.cmd.length ? { cmd: frame.cmd } : {}
|
|
710
|
+
}));
|
|
675
711
|
for (const data of pending.queued.splice(0)) registration.connection.message(data);
|
|
676
712
|
}).catch((error) => {
|
|
677
713
|
if (pending.open) rejectPendingAuthorization(h, pending, error);
|
|
@@ -698,7 +734,13 @@ const gcClients = (h) => {
|
|
|
698
734
|
}
|
|
699
735
|
if (changed) notifyClientRecords(h);
|
|
700
736
|
};
|
|
701
|
-
const register = (h, clientId, socket, sawFrame = false, auth = void 0
|
|
737
|
+
const register = (h, clientId, socket, sawFrame = false, auth = void 0, selection = {
|
|
738
|
+
version: h.wireVersion.version,
|
|
739
|
+
protocols: [{
|
|
740
|
+
name: h.protocol.name,
|
|
741
|
+
version: h.protocol.version
|
|
742
|
+
}]
|
|
743
|
+
}) => {
|
|
702
744
|
const refuse = (code, reason) => {
|
|
703
745
|
try {
|
|
704
746
|
socket.close(code, reason);
|
|
@@ -734,6 +776,10 @@ const register = (h, clientId, socket, sawFrame = false, auth = void 0) => {
|
|
|
734
776
|
ctxSeq: 0,
|
|
735
777
|
touched: Date.now(),
|
|
736
778
|
context: null,
|
|
779
|
+
contextDocuments: {
|
|
780
|
+
mode: "mounted",
|
|
781
|
+
documents: /* @__PURE__ */ new Map()
|
|
782
|
+
},
|
|
737
783
|
results: /* @__PURE__ */ new Map(),
|
|
738
784
|
inflight: /* @__PURE__ */ new Map(),
|
|
739
785
|
principal: auth
|
|
@@ -743,6 +789,7 @@ const register = (h, clientId, socket, sawFrame = false, auth = void 0) => {
|
|
|
743
789
|
})();
|
|
744
790
|
rec.touched = Date.now();
|
|
745
791
|
const conn = {
|
|
792
|
+
selection,
|
|
746
793
|
clientId,
|
|
747
794
|
socket,
|
|
748
795
|
open: true,
|
|
@@ -758,17 +805,20 @@ const register = (h, clientId, socket, sawFrame = false, auth = void 0) => {
|
|
|
758
805
|
const { value, meta } = encodeValue(h.stateHost.snapshot());
|
|
759
806
|
const hello = {
|
|
760
807
|
ops: [{
|
|
761
|
-
op: "
|
|
762
|
-
|
|
808
|
+
op: "mount",
|
|
809
|
+
protocol: h.protocol.name,
|
|
810
|
+
main: true,
|
|
763
811
|
value,
|
|
764
812
|
...meta !== void 0 && { meta }
|
|
765
813
|
}],
|
|
766
814
|
syn: {
|
|
815
|
+
...selection,
|
|
767
816
|
seq: admittedThrough(rec),
|
|
768
817
|
ctxSeq: rec.ctxSeq,
|
|
769
818
|
...h.idleWire && { idle: true }
|
|
770
819
|
}
|
|
771
820
|
};
|
|
821
|
+
if (!selection.protocols.some((p) => p.name === h.protocol.name)) hello.ops = [];
|
|
772
822
|
sendTo(h, conn, JSON.stringify(hello));
|
|
773
823
|
return {
|
|
774
824
|
rec,
|
|
@@ -808,6 +858,10 @@ const suspend = (h) => {
|
|
|
808
858
|
};
|
|
809
859
|
const restoreClientRecords = (records = []) => new Map(records.map((record) => [record.id, {
|
|
810
860
|
...record,
|
|
861
|
+
contextDocuments: {
|
|
862
|
+
mode: "mounted",
|
|
863
|
+
documents: new Map(record.contextDocuments ?? [])
|
|
864
|
+
},
|
|
811
865
|
results: new Map(record.results.map(({ seq, ts, cmd }) => [seq, {
|
|
812
866
|
ts,
|
|
813
867
|
cmd
|
|
@@ -817,6 +871,11 @@ const restoreClientRecords = (records = []) => new Map(records.map((record) => [
|
|
|
817
871
|
const useSocketHost = (transport, options, persistence) => {
|
|
818
872
|
const [h] = useState(() => {
|
|
819
873
|
const host = {
|
|
874
|
+
protocol: {
|
|
875
|
+
name: DEFAULT_PROTOCOL,
|
|
876
|
+
version: WIRE_VERSION
|
|
877
|
+
},
|
|
878
|
+
wireVersion: { version: WIRE_VERSION },
|
|
820
879
|
heartbeatMs: 15e3,
|
|
821
880
|
wsInitTimeoutMs: 5e3,
|
|
822
881
|
maxSubscribers: MAX_SUBSCRIBERS,
|
|
@@ -852,6 +911,11 @@ const useSocketHost = (transport, options, persistence) => {
|
|
|
852
911
|
};
|
|
853
912
|
return host;
|
|
854
913
|
});
|
|
914
|
+
h.protocol = options?.protocol ?? {
|
|
915
|
+
name: "default",
|
|
916
|
+
version: "2026-09-07"
|
|
917
|
+
};
|
|
918
|
+
h.wireVersion = options?.wireVersion ?? { version: "2026-09-07" };
|
|
855
919
|
setHeartbeatMs(h, options?.heartbeatIntervalMs ?? 15e3);
|
|
856
920
|
h.pushback = options?.pushback;
|
|
857
921
|
h.wsInitTimeoutMs = options?.wsInitTimeoutMs ?? 5e3;
|