statewire 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -80
- package/dist/client/connection.d.ts +19 -6
- package/dist/client/connection.d.ts.map +1 -1
- package/dist/client/connection.js +79 -40
- package/dist/client/connection.js.map +1 -1
- package/dist/client/core.d.ts +1 -0
- package/dist/client/core.d.ts.map +1 -1
- package/dist/client/core.js +5 -0
- package/dist/client/core.js.map +1 -1
- package/dist/client/session.d.ts +2 -0
- package/dist/client/session.d.ts.map +1 -1
- package/dist/client/session.js +98 -19
- package/dist/client/session.js.map +1 -1
- package/dist/host/adapter.d.ts +1 -1
- package/dist/host/adapter.d.ts.map +1 -1
- package/dist/host/adapter.js +7 -1
- package/dist/host/adapter.js.map +1 -1
- package/dist/host/host.d.ts +1 -0
- package/dist/host/host.d.ts.map +1 -1
- package/dist/host/host.js +45 -5
- package/dist/host/host.js.map +1 -1
- package/dist/host/http.d.ts +1 -1
- package/dist/host/http.d.ts.map +1 -1
- package/dist/host/http.js +75 -47
- package/dist/host/http.js.map +1 -1
- package/dist/host/socket.d.ts +23 -7
- package/dist/host/socket.d.ts.map +1 -1
- package/dist/host/socket.js +275 -74
- package/dist/host/socket.js.map +1 -1
- package/dist/host/work.d.ts +7 -0
- package/dist/host/work.d.ts.map +1 -0
- package/dist/host/work.js +53 -0
- package/dist/host/work.js.map +1 -0
- package/dist/host-internal.d.ts +35 -5
- package/dist/host-internal.d.ts.map +1 -1
- package/dist/host-internal.js +2 -1
- package/dist/host.d.ts +16 -3
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +2 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/statewire.d.ts +13 -4
- package/dist/statewire.d.ts.map +1 -1
- package/dist/statewire.js.map +1 -1
- package/dist/transports/sse.d.ts +2 -5
- package/dist/transports/sse.d.ts.map +1 -1
- package/dist/transports/sse.js +50 -59
- package/dist/transports/sse.js.map +1 -1
- package/dist/transports/testing.js +4 -4
- package/dist/transports/testing.js.map +1 -1
- package/dist/transports/ws.d.ts +3 -3
- package/dist/transports/ws.d.ts.map +1 -1
- package/dist/transports/ws.js +57 -44
- package/dist/transports/ws.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/util/randomId.js +1 -1
- package/dist/util/randomId.js.map +1 -1
- package/dist/wire/documents.d.ts +20 -0
- package/dist/wire/documents.d.ts.map +1 -0
- package/dist/wire/documents.js +74 -0
- package/dist/wire/documents.js.map +1 -0
- package/dist/wire/limits.d.ts +10 -0
- package/dist/wire/limits.d.ts.map +1 -0
- package/dist/wire/limits.js +11 -0
- package/dist/wire/limits.js.map +1 -0
- package/dist/wire/meta.d.ts.map +1 -1
- package/dist/wire/meta.js +1 -1
- package/dist/wire/meta.js.map +1 -1
- package/dist/wire/ops.d.ts.map +1 -1
- package/dist/wire/ops.js +1 -2
- package/dist/wire/ops.js.map +1 -1
- package/dist/wire/version.d.ts +7 -0
- package/dist/wire/version.d.ts.map +1 -0
- package/dist/wire/version.js +8 -0
- package/dist/wire/version.js.map +1 -0
- package/package.json +1 -1
- package/src/client/connection.ts +159 -52
- package/src/client/core.ts +6 -0
- package/src/client/session.ts +105 -32
- package/src/host/adapter.ts +10 -2
- package/src/host/host.ts +54 -4
- package/src/host/http.ts +132 -66
- package/src/host/socket.ts +410 -81
- package/src/host/work.ts +50 -0
- package/src/host-internal.ts +32 -4
- package/src/host.ts +18 -2
- package/src/index.ts +2 -2
- package/src/statewire.ts +18 -4
- package/src/transports/sse.ts +70 -78
- package/src/transports/testing.ts +4 -4
- package/src/transports/ws.ts +36 -55
- package/src/types.ts +1 -0
- package/src/util/randomId.ts +3 -3
- package/src/wire/documents.ts +109 -0
- package/src/wire/limits.ts +6 -0
- package/src/wire/meta.ts +6 -1
- package/src/wire/ops.ts +1 -2
- package/src/wire/version.ts +3 -0
package/dist/host/http.d.ts
CHANGED
|
@@ -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
|
package/dist/host/http.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.d.ts","names":[],"sources":["../../src/host/http.ts"],"mappings":"
|
|
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 {
|
|
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
|
|
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) =>
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
125
|
-
if (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
|
|
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 =
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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:
|
|
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
|
|
192
|
-
if (h.disposed || h.finished !== null ||
|
|
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
|
|
218
|
+
frame = parseFrame(raw);
|
|
198
219
|
} catch (error) {
|
|
199
220
|
if (!(error instanceof Error) || error.name !== "StatewireViolation") throw error;
|
|
200
|
-
return violationResponse(h,
|
|
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
|
package/dist/host/http.js.map
CHANGED
|
@@ -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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"}
|
package/dist/host/socket.d.ts
CHANGED
|
@@ -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
|
|
67
|
-
declare const parseFrame: (raw: string
|
|
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":";;;;;;;;;
|
|
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"}
|