statewire 0.13.1 → 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.
- package/README.md +29 -78
- package/dist/client/connection.d.ts +20 -6
- package/dist/client/connection.d.ts.map +1 -1
- package/dist/client/connection.js +120 -52
- package/dist/client/connection.js.map +1 -1
- package/dist/client/core.d.ts +3 -2
- package/dist/client/core.d.ts.map +1 -1
- package/dist/client/core.js +10 -4
- 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 +97 -17
- 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 +2 -1
- package/dist/host/http.d.ts.map +1 -1
- package/dist/host/http.js +108 -47
- package/dist/host/http.js.map +1 -1
- package/dist/host/socket.d.ts +33 -8
- package/dist/host/socket.d.ts.map +1 -1
- package/dist/host/socket.js +388 -58
- 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 +42 -4
- 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/statewire.d.ts +21 -11
- package/dist/statewire.d.ts.map +1 -1
- package/dist/statewire.js +2 -2
- package/dist/statewire.js.map +1 -1
- package/dist/transports/sse.d.ts +1 -4
- package/dist/transports/sse.d.ts.map +1 -1
- package/dist/transports/sse.js +47 -56
- package/dist/transports/sse.js.map +1 -1
- package/dist/transports/testing.js +2 -2
- package/dist/transports/testing.js.map +1 -1
- package/dist/transports/ws.d.ts +1 -1
- package/dist/transports/ws.d.ts.map +1 -1
- package/dist/transports/ws.js +54 -41
- 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/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 +213 -67
- package/src/client/core.ts +10 -3
- package/src/client/session.ts +106 -30
- package/src/host/adapter.ts +10 -2
- package/src/host/host.ts +54 -4
- package/src/host/http.ts +168 -64
- package/src/host/socket.ts +603 -60
- package/src/host/work.ts +50 -0
- package/src/host-internal.ts +44 -3
- package/src/host.ts +18 -2
- package/src/statewire.ts +33 -10
- package/src/transports/sse.ts +60 -68
- package/src/transports/testing.ts +2 -2
- package/src/transports/ws.ts +29 -48
- package/src/types.ts +1 -0
- 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,7 +1,8 @@
|
|
|
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
4
|
declare const stream: (h: SocketHostState, request: Request) => Promise<Response>;
|
|
4
5
|
declare const frames: (h: SocketHostState, request: Request) => Promise<Response>;
|
|
5
6
|
//#endregion
|
|
6
|
-
export { frames, stream };
|
|
7
|
+
export { MAX_FRAME_BODY_BYTES, frames, stream };
|
|
7
8
|
//# sourceMappingURL=http.d.ts.map
|
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,10 +1,39 @@
|
|
|
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 laggardFin = encoder.encode("data: {\"fin\":{\"reason\":\"reconnect\",\"code\":\"client-fell-behind\"}}\n\n");
|
|
9
|
+
const readFrameBody = async (request) => {
|
|
10
|
+
const declared = request.headers.get("content-length");
|
|
11
|
+
if (declared !== null && Number(declared) > 8388608) {
|
|
12
|
+
await request.body?.cancel("frame body too large").catch(() => {});
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
if (request.body === null) return "";
|
|
16
|
+
const reader = request.body.getReader();
|
|
17
|
+
const chunks = [];
|
|
18
|
+
let size = 0;
|
|
19
|
+
for (;;) {
|
|
20
|
+
const { done, value } = await reader.read();
|
|
21
|
+
if (done) break;
|
|
22
|
+
size += value.length;
|
|
23
|
+
if (size > 8388608) {
|
|
24
|
+
await reader.cancel("frame body too large").catch(() => {});
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
chunks.push(value);
|
|
28
|
+
}
|
|
29
|
+
const body = new Uint8Array(size);
|
|
30
|
+
let offset = 0;
|
|
31
|
+
for (const chunk of chunks) {
|
|
32
|
+
body.set(chunk, offset);
|
|
33
|
+
offset += chunk.length;
|
|
34
|
+
}
|
|
35
|
+
return new TextDecoder().decode(body);
|
|
36
|
+
};
|
|
8
37
|
const invalidClientIdReason = (clientId) => {
|
|
9
38
|
if (clientId === "") return "id must be non-empty";
|
|
10
39
|
if (clientId.length > 256) return "id must be at most 256 characters";
|
|
@@ -19,28 +48,29 @@ const clientIdResponse = (request) => {
|
|
|
19
48
|
return { clientId };
|
|
20
49
|
};
|
|
21
50
|
const headersOf = (request) => Object.fromEntries(request.headers);
|
|
22
|
-
const mergedHeaders = (request, extra) => {
|
|
23
|
-
const headers = headersOf(request);
|
|
24
|
-
for (const [name, value] of Object.entries(extra)) headers[name.toLowerCase()] = value;
|
|
25
|
-
return headers;
|
|
26
|
-
};
|
|
27
51
|
const authorize = async (h, headers) => {
|
|
28
52
|
try {
|
|
29
|
-
await h.authorize?.({ headers });
|
|
30
|
-
return null;
|
|
53
|
+
return { auth: await h.authorize?.({ headers }) };
|
|
31
54
|
} catch (error) {
|
|
32
|
-
if (error instanceof Response) return error;
|
|
55
|
+
if (error instanceof Response) return { refused: error };
|
|
33
56
|
throw error;
|
|
34
57
|
}
|
|
35
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
|
+
};
|
|
36
65
|
const mintLease = () => {
|
|
37
66
|
const bytes = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16));
|
|
38
67
|
return btoa(String.fromCharCode(...bytes)).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
|
|
39
68
|
};
|
|
40
|
-
const attachSse = (h, clientId) => {
|
|
69
|
+
const attachSse = (h, clientId, auth) => {
|
|
41
70
|
let controller;
|
|
42
71
|
let connection = null;
|
|
43
72
|
let open = true;
|
|
73
|
+
let backpressureActive = false;
|
|
44
74
|
const body = new ReadableStream({
|
|
45
75
|
start: (next) => {
|
|
46
76
|
controller = next;
|
|
@@ -49,10 +79,19 @@ const attachSse = (h, clientId) => {
|
|
|
49
79
|
open = false;
|
|
50
80
|
connection?.close();
|
|
51
81
|
}
|
|
52
|
-
});
|
|
82
|
+
}, { highWaterMark: h.streamQueueSize });
|
|
53
83
|
const registration = register(h, clientId, {
|
|
54
84
|
send: (data) => {
|
|
55
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
|
+
}
|
|
56
95
|
const frame = data === "{}" ? ": ping\n\n" : `data: ${data}\n\n`;
|
|
57
96
|
try {
|
|
58
97
|
controller.enqueue(encoder.encode(frame));
|
|
@@ -70,33 +109,42 @@ const attachSse = (h, clientId) => {
|
|
|
70
109
|
return;
|
|
71
110
|
}
|
|
72
111
|
}
|
|
73
|
-
});
|
|
112
|
+
}, false, auth);
|
|
74
113
|
connection = registration?.connection ?? null;
|
|
75
114
|
if (!open) connection?.close();
|
|
76
115
|
return {
|
|
77
116
|
body,
|
|
78
|
-
registration
|
|
117
|
+
registration,
|
|
118
|
+
activateBackpressure: () => {
|
|
119
|
+
backpressureActive = true;
|
|
120
|
+
}
|
|
79
121
|
};
|
|
80
122
|
};
|
|
81
|
-
const streamResponse = (attachment, lease) =>
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
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
|
+
};
|
|
89
134
|
const admitAttachFrame = (h, registration, frame) => {
|
|
90
135
|
if (frame.ctx.length === 0 && frame.cmd.length === 0) return;
|
|
91
|
-
const admission = admitFrame(h, registration.rec, frame);
|
|
136
|
+
const admission = admitFrame(h, registration.rec, frame, registration.conn.auth);
|
|
92
137
|
if (admission.type === "refused") sendTo(h, registration.conn, JSON.stringify(admission.syn));
|
|
93
138
|
};
|
|
94
139
|
const getStream = async (h, request) => {
|
|
95
|
-
const
|
|
96
|
-
if (refused
|
|
140
|
+
const authorized = await authorize(h, headersOf(request));
|
|
141
|
+
if ("refused" in authorized) return authorized.refused;
|
|
97
142
|
const client = clientIdResponse(request);
|
|
98
143
|
if ("response" in client) return client.response;
|
|
99
|
-
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);
|
|
100
148
|
const lease = attachment.registration === null ? null : mintLease();
|
|
101
149
|
if (attachment.registration !== null) attachment.registration.conn.lease = lease;
|
|
102
150
|
return streamResponse(attachment, lease);
|
|
@@ -104,23 +152,30 @@ const getStream = async (h, request) => {
|
|
|
104
152
|
const postStream = async (h, request) => {
|
|
105
153
|
const client = clientIdResponse(request);
|
|
106
154
|
if ("response" in client) return client.response;
|
|
155
|
+
const raw = await readFrameBody(request);
|
|
156
|
+
if (raw === null) return Response.json({ detail: "frame body too large" }, { status: 413 });
|
|
107
157
|
let body;
|
|
108
158
|
try {
|
|
109
|
-
body =
|
|
159
|
+
body = raw.trim() ? JSON.parse(raw) : null;
|
|
110
160
|
} catch {
|
|
111
161
|
return Response.json({ detail: "body must be JSON" }, { status: 400 });
|
|
112
162
|
}
|
|
113
163
|
let frame;
|
|
114
164
|
try {
|
|
115
|
-
frame =
|
|
165
|
+
frame = body === null && !raw.trim() ? {
|
|
166
|
+
ctx: [],
|
|
167
|
+
cmd: []
|
|
168
|
+
} : parseFrameValue(body);
|
|
116
169
|
} catch (error) {
|
|
117
170
|
if (!(error instanceof Error) || error.name !== "StatewireViolation") throw error;
|
|
118
171
|
return Response.json({ detail: error.message }, { status: 400 });
|
|
119
172
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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);
|
|
124
179
|
if (attachment.registration === null) return streamResponse(attachment, null);
|
|
125
180
|
const lease = mintLease();
|
|
126
181
|
attachment.registration.conn.lease = lease;
|
|
@@ -145,32 +200,38 @@ const finClient = (h, clientId, fin) => {
|
|
|
145
200
|
};
|
|
146
201
|
const violationResponse = (h, clientId, reason) => {
|
|
147
202
|
finClient(h, clientId, {
|
|
148
|
-
reason: "error",
|
|
149
|
-
message:
|
|
203
|
+
reason: "protocol-error",
|
|
204
|
+
message: reason
|
|
150
205
|
});
|
|
151
206
|
return Response.json({ error: reason }, { status: 400 });
|
|
152
207
|
};
|
|
153
208
|
const frames = async (h, request) => {
|
|
154
|
-
const refused = await authorize(h, headersOf(request));
|
|
155
|
-
if (refused !== null) return refused;
|
|
156
|
-
const client = clientIdResponse(request);
|
|
157
|
-
if ("response" in client) return client.response;
|
|
158
|
-
const rec = h.clients.get(client.clientId);
|
|
159
209
|
const lease = request.headers.get(LEASE_HEADER);
|
|
160
|
-
const
|
|
161
|
-
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);
|
|
214
|
+
const raw = await readFrameBody(request);
|
|
215
|
+
if (raw === null) return Response.json({ error: "frame body too large" }, { status: 413 });
|
|
162
216
|
let frame;
|
|
163
217
|
try {
|
|
164
|
-
frame = parseFrame(
|
|
218
|
+
frame = parseFrame(raw);
|
|
165
219
|
} catch (error) {
|
|
166
220
|
if (!(error instanceof Error) || error.name !== "StatewireViolation") throw error;
|
|
167
|
-
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
|
+
});
|
|
168
231
|
}
|
|
169
|
-
const admission = admitFrame(h, rec, frame);
|
|
170
|
-
if (admission.type === "refused") return Response.json(admission.syn, { status: 409 });
|
|
171
232
|
return Response.json({});
|
|
172
233
|
};
|
|
173
234
|
//#endregion
|
|
174
|
-
export { frames, stream };
|
|
235
|
+
export { MAX_FRAME_BODY_BYTES, frames, stream };
|
|
175
236
|
|
|
176
237
|
//# sourceMappingURL=http.js.map
|
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","invalidClientIdReason","clientId","length","test","clientIdResponse","request","Request","response","Response","headers","get","json","detail","status","reason","headersOf","Record","Object","fromEntries","mergedHeaders","extra","name","value","entries","toLowerCase","authorize","h","Promise","error","mintLease","bytes","crypto","getRandomValues","Uint8Array","btoa","String","fromCharCode","replaceAll","replace","SseAttachment","body","ReadableStream","registration","attachSse","controller","ReadableStreamDefaultController","connection","open","start","next","cancel","close","socket","send","data","frame","enqueue","encode","streamResponse","attachment","lease","admitAttachFrame","ctx","cmd","admission","rec","type","conn","JSON","stringify","syn","getStream","refused","client","postStream","parse","text","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();\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 let body: unknown;\n try {\n body = JSON.parse(await request.text());\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 let frame: ParsedFrame;\n try {\n frame = parseFrame(await request.text(), 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,6 +47,16 @@ 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;
|
|
52
|
+
};
|
|
53
|
+
type PendingConn = {
|
|
54
|
+
socket: StatewireHostInternal.Socket;
|
|
55
|
+
open: boolean;
|
|
56
|
+
authorizing: boolean;
|
|
57
|
+
queued: string[];
|
|
58
|
+
timer: ReturnType<typeof setTimeout> | null;
|
|
59
|
+
connection: StatewireHostInternal.SocketConnection | null;
|
|
43
60
|
};
|
|
44
61
|
type CtxStatement = {
|
|
45
62
|
seq: number;
|
|
@@ -52,19 +69,24 @@ type CmdStatement = {
|
|
|
52
69
|
meta?: unknown;
|
|
53
70
|
};
|
|
54
71
|
type ParsedFrame = {
|
|
55
|
-
headers: Record<string, string> | null;
|
|
56
72
|
ctx: CtxStatement[];
|
|
57
73
|
cmd: CmdStatement[];
|
|
58
74
|
};
|
|
59
|
-
declare const parseFrameValue: (frame: unknown
|
|
60
|
-
declare const parseFrame: (raw: string
|
|
75
|
+
declare const parseFrameValue: (frame: unknown) => ParsedFrame;
|
|
76
|
+
declare const parseFrame: (raw: string) => ParsedFrame;
|
|
61
77
|
type SocketHostState = {
|
|
62
78
|
heartbeatMs: number;
|
|
79
|
+
wsInitTimeoutMs: number;
|
|
80
|
+
maxSubscribers: number;
|
|
81
|
+
maxInflightCommands: number;
|
|
82
|
+
streamQueueSize: number;
|
|
63
83
|
clientRecordLimit: number;
|
|
64
84
|
finOnIdle: boolean;
|
|
85
|
+
pushback: (() => number | null) | undefined;
|
|
65
86
|
authorize: StatewireHostInternal.SocketHostOptions["authorize"];
|
|
66
87
|
clients: Map<string, ClientRec>;
|
|
67
88
|
conns: Set<Conn>;
|
|
89
|
+
pendingConns: Set<PendingConn>;
|
|
68
90
|
pendingOps: WireOp[];
|
|
69
91
|
pendingApplied: Map<string, number[]>;
|
|
70
92
|
pendingCmds: Map<string, StatewireCmd[]>;
|
|
@@ -76,14 +98,16 @@ type SocketHostState = {
|
|
|
76
98
|
stateHost: StatewireHost.StateHost | null;
|
|
77
99
|
commandsHost: CommandsHost | null;
|
|
78
100
|
current: Statewire<unknown> | null;
|
|
101
|
+
persistence: StatewireHostInternal.SocketHostPersistence | undefined;
|
|
79
102
|
post: () => void;
|
|
80
|
-
connect: (clientId: string, socket: StatewireHostInternal.Socket) => StatewireHostInternal.SocketConnection | null;
|
|
103
|
+
connect: (clientId: string, socket: StatewireHostInternal.Socket, options?: StatewireHostInternal.SocketConnectOptions) => StatewireHostInternal.SocketConnection | null;
|
|
81
104
|
finish: (fin: StatewireHost.Fin) => void;
|
|
82
105
|
stream: (request: Request) => Promise<Response>;
|
|
83
106
|
frames: (request: Request) => Promise<Response>;
|
|
84
107
|
};
|
|
85
108
|
declare const closeConn: (h: SocketHostState, conn: Conn, code?: number, reason?: string) => void;
|
|
86
109
|
declare const sendTo: (h: SocketHostState, conn: Conn, data: string) => void;
|
|
110
|
+
declare const atSubscriberLimit: (h: SocketHostState) => boolean;
|
|
87
111
|
declare const flush: (h: SocketHostState, fin?: StatewireHost.Fin) => void;
|
|
88
112
|
type FrameAdmission = {
|
|
89
113
|
type: "ok";
|
|
@@ -93,16 +117,17 @@ type FrameAdmission = {
|
|
|
93
117
|
syn: {
|
|
94
118
|
seq: number;
|
|
95
119
|
ctxSeq: number;
|
|
120
|
+
retry?: number;
|
|
96
121
|
};
|
|
97
122
|
};
|
|
98
123
|
};
|
|
99
|
-
declare const admitFrame: (h: SocketHostState, rec: ClientRec, frame: ParsedFrame) => FrameAdmission;
|
|
124
|
+
declare const admitFrame: (h: SocketHostState, rec: ClientRec, frame: ParsedFrame, auth: unknown) => FrameAdmission;
|
|
100
125
|
type Registration = {
|
|
101
126
|
rec: ClientRec;
|
|
102
127
|
conn: Conn;
|
|
103
128
|
connection: StatewireHostInternal.SocketConnection;
|
|
104
129
|
};
|
|
105
|
-
declare const register: (h: SocketHostState, clientId: string, socket: StatewireHostInternal.Socket) => Registration | null;
|
|
130
|
+
declare const register: (h: SocketHostState, clientId: string, socket: StatewireHostInternal.Socket, sawFrame?: boolean, auth?: unknown) => Registration | null;
|
|
106
131
|
/**
|
|
107
132
|
* Transport-agnostic WS binding over a statewire/host transport:
|
|
108
133
|
* feed it sockets (anything with `send`/`close`) and raw text frames, and it
|
|
@@ -111,7 +136,7 @@ declare const register: (h: SocketHostState, clientId: string, socket: Statewire
|
|
|
111
136
|
* arrays fanned out to every attach of a client id, idle edges, and `fin` on
|
|
112
137
|
* finish.
|
|
113
138
|
*/
|
|
114
|
-
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;
|
|
115
140
|
//#endregion
|
|
116
|
-
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 };
|
|
117
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"}
|