statewire 0.15.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.
Files changed (90) hide show
  1. package/README.md +14 -6
  2. package/dist/client/connection.d.ts +5 -1
  3. package/dist/client/connection.d.ts.map +1 -1
  4. package/dist/client/connection.js +19 -4
  5. package/dist/client/connection.js.map +1 -1
  6. package/dist/client/core.d.ts.map +1 -1
  7. package/dist/client/core.js +2 -0
  8. package/dist/client/core.js.map +1 -1
  9. package/dist/client/session.d.ts +15 -4
  10. package/dist/client/session.d.ts.map +1 -1
  11. package/dist/client/session.js +233 -52
  12. package/dist/client/session.js.map +1 -1
  13. package/dist/client/sessionStore.d.ts +5 -1
  14. package/dist/client/sessionStore.d.ts.map +1 -1
  15. package/dist/client/sessionStore.js +3 -1
  16. package/dist/client/sessionStore.js.map +1 -1
  17. package/dist/host/host.d.ts +1 -0
  18. package/dist/host/host.d.ts.map +1 -1
  19. package/dist/host/host.js.map +1 -1
  20. package/dist/host/http.d.ts.map +1 -1
  21. package/dist/host/http.js +27 -9
  22. package/dist/host/http.js.map +1 -1
  23. package/dist/host/socket.d.ts +10 -3
  24. package/dist/host/socket.d.ts.map +1 -1
  25. package/dist/host/socket.js +103 -39
  26. package/dist/host/socket.js.map +1 -1
  27. package/dist/host-internal.d.ts +9 -1
  28. package/dist/host-internal.d.ts.map +1 -1
  29. package/dist/host-internal.js +1 -1
  30. package/dist/index.d.ts +5 -3
  31. package/dist/index.js +3 -3
  32. package/dist/statewire.d.ts +33 -2
  33. package/dist/statewire.d.ts.map +1 -1
  34. package/dist/statewire.js +34 -3
  35. package/dist/statewire.js.map +1 -1
  36. package/dist/transports/fixtures.js +1 -0
  37. package/dist/transports/fixtures.js.map +1 -1
  38. package/dist/transports/remote.d.ts.map +1 -1
  39. package/dist/transports/remote.js +15 -8
  40. package/dist/transports/remote.js.map +1 -1
  41. package/dist/transports/sse.d.ts +2 -2
  42. package/dist/transports/sse.d.ts.map +1 -1
  43. package/dist/transports/sse.js +11 -11
  44. package/dist/transports/sse.js.map +1 -1
  45. package/dist/transports/testing.d.ts +1 -1
  46. package/dist/transports/testing.d.ts.map +1 -1
  47. package/dist/transports/testing.js +14 -6
  48. package/dist/transports/testing.js.map +1 -1
  49. package/dist/transports/ws.d.ts +2 -2
  50. package/dist/transports/ws.d.ts.map +1 -1
  51. package/dist/transports/ws.js +5 -5
  52. package/dist/transports/ws.js.map +1 -1
  53. package/dist/types.d.ts +7 -0
  54. package/dist/types.d.ts.map +1 -1
  55. package/dist/types.js.map +1 -1
  56. package/dist/util/randomId.js +1 -1
  57. package/dist/util/randomId.js.map +1 -1
  58. package/dist/wire/documents.d.ts +25 -2
  59. package/dist/wire/documents.d.ts.map +1 -1
  60. package/dist/wire/documents.js +27 -7
  61. package/dist/wire/documents.js.map +1 -1
  62. package/dist/wire/protocol.d.ts +55 -0
  63. package/dist/wire/protocol.d.ts.map +1 -0
  64. package/dist/wire/protocol.js +221 -0
  65. package/dist/wire/protocol.js.map +1 -0
  66. package/dist/wire/version.d.ts +1 -3
  67. package/dist/wire/version.d.ts.map +1 -1
  68. package/dist/wire/version.js +1 -3
  69. package/dist/wire/version.js.map +1 -1
  70. package/package.json +1 -1
  71. package/src/client/connection.ts +43 -13
  72. package/src/client/core.ts +6 -0
  73. package/src/client/session.ts +339 -42
  74. package/src/client/sessionStore.ts +8 -0
  75. package/src/host/host.ts +1 -0
  76. package/src/host/http.ts +36 -7
  77. package/src/host/socket.ts +141 -50
  78. package/src/host-internal.ts +12 -1
  79. package/src/index.ts +5 -2
  80. package/src/statewire.ts +109 -6
  81. package/src/transports/fixtures.ts +1 -1
  82. package/src/transports/remote.ts +16 -7
  83. package/src/transports/sse.ts +28 -18
  84. package/src/transports/testing.ts +12 -4
  85. package/src/transports/ws.ts +8 -8
  86. package/src/types.ts +11 -0
  87. package/src/util/randomId.ts +3 -3
  88. package/src/wire/documents.ts +126 -8
  89. package/src/wire/protocol.ts +327 -0
  90. 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 attachment = attachSse(h, client.clientId, authorized.auth);
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 attachment = attachSse(h, client.clientId, authorized.auth);
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
- admitAttachFrame(h, attachment.registration, frame);
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
- const admission = admitFrame(h, rec, frame, leased.auth);
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, {
@@ -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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"}
@@ -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
- value: Record<string, unknown>;
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":";;;;;;;;;cAwBa,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,SAAS;IAAc;IAAY,KAAK;;EACxC,UAAU,YAAY;;EAEtB;;KAGU;EACV;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,OAAO;;KACrC;EACH;EACA;EACA;EACA;;KAGU;EACV,KAAK;EACL,KAAK;;cAkDM,kBAAmB,mBAAiB;cAmDpC,aAAc,gBAAc;KAiG7B;EACV;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;KAkQlD;EACN;;EAEA;EACA;IACE;MAAO;MAAa;MAAgB;;;;cAI/B,aACX,GAAG,iBACH,KAAK,WACL,OAAO,aACP,kBACC;KA8XS;EACV,KAAK;EACL,MAAM;EACN,YAAY,sBAAsB;;cAGvB,WACX,GAAG,iBACH,kBACA,QAAQ,sBAAsB,QAC9B,oBACA,mBACC;;;;;;;;;cAyPU,sBACX,WAAW,gBAAgB,YAC3B,UAAU,sBAAsB,mBAChC,cAAc,sBAAsB,0BACnC,sBAAsB"}
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"}
@@ -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
- let value;
73
- try {
74
- value = contextValue(statement["ops"]);
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
- value
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
- for (const statement of ctxNew) {
480
- rec.context = statement.value;
481
- rec.ctxSeq = statement.seq;
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
- for (const statement of cmdNew) admitCmd(h, rec, statement, auth);
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
- const admission = admitFrame(h, rec, frame, conn.auth);
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 = normalizedHeaders(initial.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
- options.onAuthorized?.(auth);
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
- const admission = admitFrame(h, registration.rec, frame, auth);
673
- if (admission.type === "refused") sendTo(h, registration.conn, JSON.stringify(admission.syn));
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: "replace",
762
- path: [],
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;