statewire 0.14.0 → 0.15.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 (94) hide show
  1. package/README.md +29 -80
  2. package/dist/client/connection.d.ts +19 -6
  3. package/dist/client/connection.d.ts.map +1 -1
  4. package/dist/client/connection.js +79 -40
  5. package/dist/client/connection.js.map +1 -1
  6. package/dist/client/core.d.ts +1 -0
  7. package/dist/client/core.d.ts.map +1 -1
  8. package/dist/client/core.js +5 -0
  9. package/dist/client/core.js.map +1 -1
  10. package/dist/client/session.d.ts +2 -0
  11. package/dist/client/session.d.ts.map +1 -1
  12. package/dist/client/session.js +96 -16
  13. package/dist/client/session.js.map +1 -1
  14. package/dist/host/adapter.d.ts +1 -1
  15. package/dist/host/adapter.d.ts.map +1 -1
  16. package/dist/host/adapter.js +7 -1
  17. package/dist/host/adapter.js.map +1 -1
  18. package/dist/host/host.d.ts +1 -0
  19. package/dist/host/host.d.ts.map +1 -1
  20. package/dist/host/host.js +45 -5
  21. package/dist/host/host.js.map +1 -1
  22. package/dist/host/http.d.ts +1 -1
  23. package/dist/host/http.d.ts.map +1 -1
  24. package/dist/host/http.js +75 -47
  25. package/dist/host/http.js.map +1 -1
  26. package/dist/host/socket.d.ts +23 -7
  27. package/dist/host/socket.d.ts.map +1 -1
  28. package/dist/host/socket.js +275 -74
  29. package/dist/host/socket.js.map +1 -1
  30. package/dist/host/work.d.ts +7 -0
  31. package/dist/host/work.d.ts.map +1 -0
  32. package/dist/host/work.js +53 -0
  33. package/dist/host/work.js.map +1 -0
  34. package/dist/host-internal.d.ts +35 -5
  35. package/dist/host-internal.d.ts.map +1 -1
  36. package/dist/host-internal.js +2 -1
  37. package/dist/host.d.ts +16 -3
  38. package/dist/host.d.ts.map +1 -1
  39. package/dist/host.js +2 -1
  40. package/dist/statewire.d.ts +11 -2
  41. package/dist/statewire.d.ts.map +1 -1
  42. package/dist/statewire.js.map +1 -1
  43. package/dist/transports/sse.d.ts +1 -4
  44. package/dist/transports/sse.d.ts.map +1 -1
  45. package/dist/transports/sse.js +47 -56
  46. package/dist/transports/sse.js.map +1 -1
  47. package/dist/transports/testing.js +2 -2
  48. package/dist/transports/testing.js.map +1 -1
  49. package/dist/transports/ws.d.ts +1 -1
  50. package/dist/transports/ws.d.ts.map +1 -1
  51. package/dist/transports/ws.js +54 -41
  52. package/dist/transports/ws.js.map +1 -1
  53. package/dist/types.d.ts +3 -0
  54. package/dist/types.d.ts.map +1 -1
  55. package/dist/types.js.map +1 -1
  56. package/dist/wire/documents.d.ts +20 -0
  57. package/dist/wire/documents.d.ts.map +1 -0
  58. package/dist/wire/documents.js +74 -0
  59. package/dist/wire/documents.js.map +1 -0
  60. package/dist/wire/limits.d.ts +10 -0
  61. package/dist/wire/limits.d.ts.map +1 -0
  62. package/dist/wire/limits.js +11 -0
  63. package/dist/wire/limits.js.map +1 -0
  64. package/dist/wire/meta.d.ts.map +1 -1
  65. package/dist/wire/meta.js +1 -1
  66. package/dist/wire/meta.js.map +1 -1
  67. package/dist/wire/ops.d.ts.map +1 -1
  68. package/dist/wire/ops.js +1 -2
  69. package/dist/wire/ops.js.map +1 -1
  70. package/dist/wire/version.d.ts +7 -0
  71. package/dist/wire/version.d.ts.map +1 -0
  72. package/dist/wire/version.js +8 -0
  73. package/dist/wire/version.js.map +1 -0
  74. package/package.json +1 -1
  75. package/src/client/connection.ts +159 -52
  76. package/src/client/core.ts +6 -0
  77. package/src/client/session.ts +104 -29
  78. package/src/host/adapter.ts +10 -2
  79. package/src/host/host.ts +54 -4
  80. package/src/host/http.ts +132 -66
  81. package/src/host/socket.ts +410 -81
  82. package/src/host/work.ts +50 -0
  83. package/src/host-internal.ts +32 -4
  84. package/src/host.ts +18 -2
  85. package/src/statewire.ts +16 -2
  86. package/src/transports/sse.ts +60 -68
  87. package/src/transports/testing.ts +2 -2
  88. package/src/transports/ws.ts +29 -48
  89. package/src/types.ts +1 -0
  90. package/src/wire/documents.ts +109 -0
  91. package/src/wire/limits.ts +6 -0
  92. package/src/wire/meta.ts +6 -1
  93. package/src/wire/ops.ts +1 -2
  94. package/src/wire/version.ts +3 -0
@@ -1,6 +1,6 @@
1
1
  import { SocketHostState } from "./socket.js";
2
+ import { MAX_FRAME_BODY_BYTES } from "../wire/limits.js";
2
3
  //#region src/host/http.d.ts
3
- declare const MAX_FRAME_BODY_BYTES: number;
4
4
  declare const stream: (h: SocketHostState, request: Request) => Promise<Response>;
5
5
  declare const frames: (h: SocketHostState, request: Request) => Promise<Response>;
6
6
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"http.d.ts","names":[],"sources":["../../src/host/http.ts"],"mappings":";;cAkBa;cAiOA,SACX,GAAG,iBACH,SAAS,YACR,QAAQ;cAkCE,SACX,GAAG,iBACH,SAAS,YACR,QAAQ"}
1
+ {"version":3,"file":"http.d.ts","names":[],"sources":["../../src/host/http.ts"],"mappings":";;;cAiTa,SACX,GAAG,iBACH,SAAS,YACR,QAAQ;cAkCE,SACX,GAAG,iBACH,SAAS,YACR,QAAQ"}
package/dist/host/http.js CHANGED
@@ -1,11 +1,11 @@
1
- import { CLIENT_ID_RE, admitFrame, closeConn, flush, parseFrame, parseFrameValue, register, sendTo } from "./socket.js";
1
+ import { MAX_FRAME_BODY_BYTES } from "../wire/limits.js";
2
+ import { VERSION_HEADER } from "../wire/version.js";
3
+ import { CLIENT_ID_RE, PRINCIPAL_MISMATCH_MESSAGE, SUBSCRIBER_LIMIT_MESSAGE, admitFrame, atSubscriberLimit, authEquals, closeConn, flush, parseFrame, parseFrameValue, register, sendTo } from "./socket.js";
2
4
  //#region src/host/http.ts
3
5
  const CLIENT_ID_HEADER = "Statewire-Client-Id";
4
6
  const LEASE_HEADER = "Statewire-Lease";
5
- const VERSION_HEADER = "Statewire-Version";
6
- const VERSION = "1";
7
7
  const encoder = new TextEncoder();
8
- const MAX_FRAME_BODY_BYTES = 8388608;
8
+ const laggardFin = encoder.encode("data: {\"fin\":{\"reason\":\"reconnect\",\"code\":\"client-fell-behind\"}}\n\n");
9
9
  const readFrameBody = async (request) => {
10
10
  const declared = request.headers.get("content-length");
11
11
  if (declared !== null && Number(declared) > 8388608) {
@@ -48,28 +48,29 @@ const clientIdResponse = (request) => {
48
48
  return { clientId };
49
49
  };
50
50
  const headersOf = (request) => Object.fromEntries(request.headers);
51
- const mergedHeaders = (request, extra) => {
52
- const headers = headersOf(request);
53
- for (const [name, value] of Object.entries(extra)) headers[name.toLowerCase()] = value;
54
- return headers;
55
- };
56
51
  const authorize = async (h, headers) => {
57
52
  try {
58
- await h.authorize?.({ headers });
59
- return null;
53
+ return { auth: await h.authorize?.({ headers }) };
60
54
  } catch (error) {
61
- if (error instanceof Response) return error;
55
+ if (error instanceof Response) return { refused: error };
62
56
  throw error;
63
57
  }
64
58
  };
59
+ const subscriberLimitResponse = () => Response.json({ detail: SUBSCRIBER_LIMIT_MESSAGE }, { status: 503 });
60
+ const principalMismatchResponse = (h, clientId, auth) => {
61
+ const rec = h.clients.get(clientId);
62
+ if (rec !== void 0 && !authEquals(rec.principal, auth)) return Response.json({ detail: PRINCIPAL_MISMATCH_MESSAGE }, { status: 403 });
63
+ return null;
64
+ };
65
65
  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) => {
69
+ const attachSse = (h, clientId, auth) => {
70
70
  let controller;
71
71
  let connection = null;
72
72
  let open = true;
73
+ let backpressureActive = false;
73
74
  const body = new ReadableStream({
74
75
  start: (next) => {
75
76
  controller = next;
@@ -78,10 +79,19 @@ const attachSse = (h, clientId) => {
78
79
  open = false;
79
80
  connection?.close();
80
81
  }
81
- });
82
+ }, { highWaterMark: h.streamQueueSize });
82
83
  const registration = register(h, clientId, {
83
84
  send: (data) => {
84
85
  if (!open) return;
86
+ if (backpressureActive && controller.desiredSize !== null && controller.desiredSize <= 0) {
87
+ open = false;
88
+ try {
89
+ controller.enqueue(laggardFin);
90
+ controller.close();
91
+ } catch {}
92
+ connection?.close();
93
+ return;
94
+ }
85
95
  const frame = data === "{}" ? ": ping\n\n" : `data: ${data}\n\n`;
86
96
  try {
87
97
  controller.enqueue(encoder.encode(frame));
@@ -99,33 +109,42 @@ const attachSse = (h, clientId) => {
99
109
  return;
100
110
  }
101
111
  }
102
- });
112
+ }, false, auth);
103
113
  connection = registration?.connection ?? null;
104
114
  if (!open) connection?.close();
105
115
  return {
106
116
  body,
107
- registration
117
+ registration,
118
+ activateBackpressure: () => {
119
+ backpressureActive = true;
120
+ }
108
121
  };
109
122
  };
110
- const streamResponse = (attachment, lease) => new Response(attachment.body, {
111
- status: 200,
112
- headers: {
113
- "Content-Type": "text/event-stream",
114
- [VERSION_HEADER]: VERSION,
115
- ...lease !== null && { [LEASE_HEADER]: lease }
116
- }
117
- });
123
+ const streamResponse = (attachment, lease) => {
124
+ attachment.activateBackpressure();
125
+ return new Response(attachment.body, {
126
+ status: 200,
127
+ headers: {
128
+ "Content-Type": "text/event-stream",
129
+ [VERSION_HEADER]: "1",
130
+ ...lease !== null && { [LEASE_HEADER]: lease }
131
+ }
132
+ });
133
+ };
118
134
  const admitAttachFrame = (h, registration, frame) => {
119
135
  if (frame.ctx.length === 0 && frame.cmd.length === 0) return;
120
- const admission = admitFrame(h, registration.rec, frame);
136
+ const admission = admitFrame(h, registration.rec, frame, registration.conn.auth);
121
137
  if (admission.type === "refused") sendTo(h, registration.conn, JSON.stringify(admission.syn));
122
138
  };
123
139
  const getStream = async (h, request) => {
124
- const refused = await authorize(h, headersOf(request));
125
- if (refused !== null) return refused;
140
+ const authorized = await authorize(h, headersOf(request));
141
+ if ("refused" in authorized) return authorized.refused;
126
142
  const client = clientIdResponse(request);
127
143
  if ("response" in client) return client.response;
128
- const attachment = attachSse(h, client.clientId);
144
+ const mismatch = principalMismatchResponse(h, client.clientId, authorized.auth);
145
+ if (mismatch !== null) return mismatch;
146
+ if (atSubscriberLimit(h)) return subscriberLimitResponse();
147
+ const attachment = attachSse(h, client.clientId, authorized.auth);
129
148
  const lease = attachment.registration === null ? null : mintLease();
130
149
  if (attachment.registration !== null) attachment.registration.conn.lease = lease;
131
150
  return streamResponse(attachment, lease);
@@ -137,21 +156,26 @@ const postStream = async (h, request) => {
137
156
  if (raw === null) return Response.json({ detail: "frame body too large" }, { status: 413 });
138
157
  let body;
139
158
  try {
140
- body = JSON.parse(raw);
159
+ body = raw.trim() ? JSON.parse(raw) : null;
141
160
  } catch {
142
161
  return Response.json({ detail: "body must be JSON" }, { status: 400 });
143
162
  }
144
163
  let frame;
145
164
  try {
146
- frame = parseFrameValue(body, true);
165
+ frame = body === null && !raw.trim() ? {
166
+ ctx: [],
167
+ cmd: []
168
+ } : parseFrameValue(body);
147
169
  } catch (error) {
148
170
  if (!(error instanceof Error) || error.name !== "StatewireViolation") throw error;
149
171
  return Response.json({ detail: error.message }, { status: 400 });
150
172
  }
151
- if (frame.headers === null) return Response.json({ detail: "first frame must carry headers" }, { status: 400 });
152
- const refused = await authorize(h, mergedHeaders(request, frame.headers));
153
- if (refused !== null) return refused;
154
- const attachment = attachSse(h, client.clientId);
173
+ const authorized = await authorize(h, headersOf(request));
174
+ if ("refused" in authorized) return authorized.refused;
175
+ const mismatch = principalMismatchResponse(h, client.clientId, authorized.auth);
176
+ if (mismatch !== null) return mismatch;
177
+ if (atSubscriberLimit(h)) return subscriberLimitResponse();
178
+ const attachment = attachSse(h, client.clientId, authorized.auth);
155
179
  if (attachment.registration === null) return streamResponse(attachment, null);
156
180
  const lease = mintLease();
157
181
  attachment.registration.conn.lease = lease;
@@ -176,31 +200,35 @@ const finClient = (h, clientId, fin) => {
176
200
  };
177
201
  const violationResponse = (h, clientId, reason) => {
178
202
  finClient(h, clientId, {
179
- reason: "error",
180
- message: `protocol violation: ${reason}`
203
+ reason: "protocol-error",
204
+ message: reason
181
205
  });
182
206
  return Response.json({ error: reason }, { status: 400 });
183
207
  };
184
208
  const frames = async (h, request) => {
185
- const refused = await authorize(h, headersOf(request));
186
- if (refused !== null) return refused;
187
- const client = clientIdResponse(request);
188
- if ("response" in client) return client.response;
189
- const rec = h.clients.get(client.clientId);
190
209
  const lease = request.headers.get(LEASE_HEADER);
191
- const live = lease !== null && [...h.conns].some((conn) => conn.open && conn.clientId === client.clientId && conn.lease === lease);
192
- if (h.disposed || h.finished !== null || rec === void 0 || !live) return Response.json({ error: "stale-lease" }, { status: 423 });
210
+ const leased = lease === null ? void 0 : [...h.conns].find((conn) => conn.open && conn.lease === lease);
211
+ if (h.disposed || h.finished !== null || leased === void 0) return Response.json({ error: "stale-lease" }, { status: 423 });
212
+ const clientId = leased.clientId;
213
+ const rec = h.clients.get(clientId);
193
214
  const raw = await readFrameBody(request);
194
215
  if (raw === null) return Response.json({ error: "frame body too large" }, { status: 413 });
195
216
  let frame;
196
217
  try {
197
- frame = parseFrame(raw, false);
218
+ frame = parseFrame(raw);
198
219
  } catch (error) {
199
220
  if (!(error instanceof Error) || error.name !== "StatewireViolation") throw error;
200
- return violationResponse(h, client.clientId, error.message);
221
+ return violationResponse(h, clientId, error.message);
222
+ }
223
+ 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);
225
+ if (admission.type === "refused") {
226
+ const retry = admission.syn.syn.retry;
227
+ return Response.json(admission.syn, {
228
+ status: retry === void 0 ? 409 : 429,
229
+ ...retry !== void 0 && { headers: { "Retry-After": String(Math.ceil(retry / 1e3)) } }
230
+ });
201
231
  }
202
- const admission = admitFrame(h, rec, frame);
203
- if (admission.type === "refused") return Response.json(admission.syn, { status: 409 });
204
232
  return Response.json({});
205
233
  };
206
234
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"http.js","names":["StatewireHost","CLIENT_ID_RE","admitFrame","closeConn","flush","parseFrame","parseFrameValue","register","sendTo","ParsedFrame","Registration","SocketHostState","CLIENT_ID_HEADER","LEASE_HEADER","VERSION_HEADER","VERSION","encoder","TextEncoder","MAX_FRAME_BODY_BYTES","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","mergedHeaders","extra","name","entries","toLowerCase","authorize","h","error","mintLease","bytes","crypto","getRandomValues","btoa","String","fromCharCode","replaceAll","replace","SseAttachment","ReadableStream","registration","attachSse","controller","ReadableStreamDefaultController","connection","open","start","next","close","socket","send","data","frame","enqueue","encode","streamResponse","attachment","lease","admitAttachFrame","ctx","cmd","admission","rec","type","conn","JSON","stringify","syn","getStream","refused","client","postStream","raw","parse","Error","message","stream","method","Allow","finClient","fin","Fin","conns","violationResponse","frames","clients","live","some","disposed","finished","undefined"],"sources":["../../src/host/http.ts"],"sourcesContent":["import type { StatewireHost } from \"../host\";\nimport {\n CLIENT_ID_RE,\n admitFrame,\n closeConn,\n flush,\n parseFrame,\n parseFrameValue,\n register,\n sendTo,\n} from \"./socket\";\nimport type { ParsedFrame, Registration, SocketHostState } from \"./socket\";\n\nconst CLIENT_ID_HEADER = \"Statewire-Client-Id\";\nconst LEASE_HEADER = \"Statewire-Lease\";\nconst VERSION_HEADER = \"Statewire-Version\";\nconst VERSION = \"1\";\nconst encoder = new TextEncoder();\nexport const MAX_FRAME_BODY_BYTES = 8 * 1024 * 1024;\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 mergedHeaders = (\n request: Request,\n extra: Record<string, string>,\n): Record<string, string> => {\n const headers = headersOf(request);\n for (const [name, value] of Object.entries(extra)) {\n headers[name.toLowerCase()] = value;\n }\n return headers;\n};\n\nconst authorize = async (\n h: SocketHostState,\n headers: Record<string, string>,\n): Promise<Response | null> => {\n try {\n await h.authorize?.({ headers });\n return null;\n } catch (error) {\n if (error instanceof Response) return error;\n throw error;\n }\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};\n\nconst attachSse = (h: SocketHostState, clientId: string): SseAttachment => {\n let controller: ReadableStreamDefaultController<Uint8Array>;\n let connection: Registration[\"connection\"] | null = null;\n let open = true;\n const body = new ReadableStream({\n start: (next) => {\n controller = next;\n },\n cancel: () => {\n open = false;\n connection?.close();\n },\n });\n const socket = {\n send: (data: string) => {\n if (!open) return;\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);\n connection = registration?.connection ?? null;\n if (!open) connection?.close();\n return { body, registration };\n};\n\nconst streamResponse = (\n attachment: SseAttachment,\n lease: string | null,\n): Response =>\n new Response(attachment.body, {\n status: 200,\n headers: {\n \"Content-Type\": \"text/event-stream\",\n [VERSION_HEADER]: VERSION,\n ...(lease !== null && { [LEASE_HEADER]: lease }),\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(h, registration.rec, frame);\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 refused = await authorize(h, headersOf(request));\n if (refused !== null) return refused;\n const client = clientIdResponse(request);\n if (\"response\" in client) return client.response;\n const attachment = attachSse(h, client.clientId);\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 = JSON.parse(raw);\n } catch {\n return Response.json({ detail: \"body must be JSON\" }, { status: 400 });\n }\n let frame: ParsedFrame;\n try {\n frame = parseFrameValue(body, true);\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 if (frame.headers === null) {\n return Response.json(\n { detail: \"first frame must carry headers\" },\n { status: 400 },\n );\n }\n const refused = await authorize(h, mergedHeaders(request, frame.headers));\n if (refused !== null) return refused;\n const attachment = attachSse(h, client.clientId);\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: \"error\",\n message: `protocol violation: ${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 refused = await authorize(h, headersOf(request));\n if (refused !== null) return refused;\n const client = clientIdResponse(request);\n if (\"response\" in client) return client.response;\n const rec = h.clients.get(client.clientId);\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 live =\n lease !== null &&\n [...h.conns].some(\n (conn) =>\n conn.open && conn.clientId === client.clientId && conn.lease === lease,\n );\n if (h.disposed || h.finished !== null || rec === undefined || !live) {\n return Response.json({ error: \"stale-lease\" }, { status: 423 });\n }\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, false);\n } catch (error) {\n if (!(error instanceof Error) || error.name !== \"StatewireViolation\") {\n throw error;\n }\n return violationResponse(h, client.clientId, error.message);\n }\n const admission = admitFrame(h, rec, frame);\n if (admission.type === \"refused\") {\n return Response.json(admission.syn, { status: 409 });\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","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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"}
@@ -8,6 +8,11 @@ import { StatewireHostInternal } from "../host-internal.js";
8
8
  import { ResourceElement } from "@assistant-ui/tap";
9
9
  //#region src/host/socket.d.ts
10
10
  declare const CLIENT_ID_RE: RegExp;
11
+ declare const PRINCIPAL_MISMATCH_MESSAGE = "client id is bound to another principal";
12
+ /** Structural equality over auth values: primitives, Dates, arrays, and plain
13
+ * objects compare by value; anything else by identity. */
14
+ declare const authEquals: (a: unknown, b: unknown) => boolean;
15
+ declare const SUBSCRIBER_LIMIT_MESSAGE = "stream subscriber limit reached";
11
16
  type WireOp = {
12
17
  op: string;
13
18
  path?: StatewirePath;
@@ -32,6 +37,8 @@ type ClientRec = {
32
37
  cmd: StatewireCmd;
33
38
  }>;
34
39
  inflight: Map<number, Admission>;
40
+ /** The auth captured at the record's first attach; later attaches must match. */
41
+ principal: unknown;
35
42
  };
36
43
  type Conn = {
37
44
  clientId: string;
@@ -40,11 +47,14 @@ type Conn = {
40
47
  sawFrame: boolean;
41
48
  lastSentAt: number;
42
49
  lease: string | null;
50
+ /** The awaited `authorize` return captured at this attach's authorization. */
51
+ auth: unknown;
43
52
  };
44
53
  type PendingConn = {
45
54
  socket: StatewireHostInternal.Socket;
46
55
  open: boolean;
47
56
  authorizing: boolean;
57
+ queued: string[];
48
58
  timer: ReturnType<typeof setTimeout> | null;
49
59
  connection: StatewireHostInternal.SocketConnection | null;
50
60
  };
@@ -59,17 +69,20 @@ type CmdStatement = {
59
69
  meta?: unknown;
60
70
  };
61
71
  type ParsedFrame = {
62
- headers: Record<string, string> | null;
63
72
  ctx: CtxStatement[];
64
73
  cmd: CmdStatement[];
65
74
  };
66
- declare const parseFrameValue: (frame: unknown, first: boolean) => ParsedFrame;
67
- declare const parseFrame: (raw: string, first: boolean) => ParsedFrame;
75
+ declare const parseFrameValue: (frame: unknown) => ParsedFrame;
76
+ declare const parseFrame: (raw: string) => ParsedFrame;
68
77
  type SocketHostState = {
69
78
  heartbeatMs: number;
70
79
  wsInitTimeoutMs: number;
80
+ maxSubscribers: number;
81
+ maxInflightCommands: number;
82
+ streamQueueSize: number;
71
83
  clientRecordLimit: number;
72
84
  finOnIdle: boolean;
85
+ pushback: (() => number | null) | undefined;
73
86
  authorize: StatewireHostInternal.SocketHostOptions["authorize"];
74
87
  clients: Map<string, ClientRec>;
75
88
  conns: Set<Conn>;
@@ -85,6 +98,7 @@ type SocketHostState = {
85
98
  stateHost: StatewireHost.StateHost | null;
86
99
  commandsHost: CommandsHost | null;
87
100
  current: Statewire<unknown> | null;
101
+ persistence: StatewireHostInternal.SocketHostPersistence | undefined;
88
102
  post: () => void;
89
103
  connect: (clientId: string, socket: StatewireHostInternal.Socket, options?: StatewireHostInternal.SocketConnectOptions) => StatewireHostInternal.SocketConnection | null;
90
104
  finish: (fin: StatewireHost.Fin) => void;
@@ -93,6 +107,7 @@ type SocketHostState = {
93
107
  };
94
108
  declare const closeConn: (h: SocketHostState, conn: Conn, code?: number, reason?: string) => void;
95
109
  declare const sendTo: (h: SocketHostState, conn: Conn, data: string) => void;
110
+ declare const atSubscriberLimit: (h: SocketHostState) => boolean;
96
111
  declare const flush: (h: SocketHostState, fin?: StatewireHost.Fin) => void;
97
112
  type FrameAdmission = {
98
113
  type: "ok";
@@ -102,16 +117,17 @@ type FrameAdmission = {
102
117
  syn: {
103
118
  seq: number;
104
119
  ctxSeq: number;
120
+ retry?: number;
105
121
  };
106
122
  };
107
123
  };
108
- declare const admitFrame: (h: SocketHostState, rec: ClientRec, frame: ParsedFrame) => FrameAdmission;
124
+ declare const admitFrame: (h: SocketHostState, rec: ClientRec, frame: ParsedFrame, auth: unknown) => FrameAdmission;
109
125
  type Registration = {
110
126
  rec: ClientRec;
111
127
  conn: Conn;
112
128
  connection: StatewireHostInternal.SocketConnection;
113
129
  };
114
- declare const register: (h: SocketHostState, clientId: string, socket: StatewireHostInternal.Socket, sawFrame?: boolean) => Registration | null;
130
+ declare const register: (h: SocketHostState, clientId: string, socket: StatewireHostInternal.Socket, sawFrame?: boolean, auth?: unknown) => Registration | null;
115
131
  /**
116
132
  * Transport-agnostic WS binding over a statewire/host transport:
117
133
  * feed it sockets (anything with `send`/`close`) and raw text frames, and it
@@ -120,7 +136,7 @@ declare const register: (h: SocketHostState, clientId: string, socket: Statewire
120
136
  * arrays fanned out to every attach of a client id, idle edges, and `fin` on
121
137
  * finish.
122
138
  */
123
- declare const StatewireSocketHost: (transport: ResourceElement<Statewire>, options?: StatewireHostInternal.SocketHostOptions) => StatewireHostInternal.SocketHost;
139
+ declare const StatewireSocketHost: (transport: ResourceElement<Statewire>, options?: StatewireHostInternal.SocketHostOptions, persistence?: StatewireHostInternal.SocketHostPersistence) => StatewireHostInternal.SocketHost;
124
140
  //#endregion
125
- export { CLIENT_ID_RE, ClientRec, Conn, FrameAdmission, ParsedFrame, Registration, SocketHostState, StatewireSocketHost, admitFrame, closeConn, flush, parseFrame, parseFrameValue, register, sendTo };
141
+ export { CLIENT_ID_RE, ClientRec, Conn, FrameAdmission, PRINCIPAL_MISMATCH_MESSAGE, ParsedFrame, Registration, SUBSCRIBER_LIMIT_MESSAGE, SocketHostState, StatewireSocketHost, admitFrame, atSubscriberLimit, authEquals, closeConn, flush, parseFrame, parseFrameValue, register, sendTo };
126
142
  //# sourceMappingURL=socket.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"socket.d.ts","names":[],"sources":["../../src/host/socket.ts"],"mappings":";;;;;;;;;cAgBa,cAAY;KAKpB;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;;KAGZ;EACV;EACA,QAAQ,sBAAsB;EAC9B;EACA;EACA;EACA;;KAGG;EACH,QAAQ,sBAAsB;EAC9B;EACA;EACA,OAAO,kBAAkB;EACzB,YAAY,sBAAsB;;KAc/B;EAAiB;EAAa,OAAO;;KACrC;EACH;EACA;EACA;EACA;;KAGU;EACV,SAAS;EACT,KAAK;EACL,KAAK;;cAgDM,kBACX,gBACA,mBACC;cAoDU,aAAc,aAAa,mBAAiB;KAmE7C;EACV;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;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;cAsC1C,QAAS,GAAG,iBAAiB,MAAM,cAAc;KAyOlD;EACN;;EACA;EAAiB;IAAO;MAAO;MAAa;;;;cAErC,aACX,GAAG,iBACH,KAAK,WACL,OAAO,gBACN;KAgQS;EACV,KAAK;EACL,MAAM;EACN,YAAY,sBAAsB;;cAGvB,WACX,GAAG,iBACH,kBACA,QAAQ,sBAAsB,QAC9B,uBACC;;;;;;;;;cA4KU,sBACX,WAAW,gBAAgB,YAC3B,UAAU,sBAAsB,sBAC/B,sBAAsB"}
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"}