statewire 0.9.2 → 0.10.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/dist/client/connection.d.ts +1 -0
- package/dist/client/connection.d.ts.map +1 -1
- package/dist/client/connection.js +8 -1
- package/dist/client/connection.js.map +1 -1
- package/dist/client/session.d.ts +7 -4
- package/dist/client/session.d.ts.map +1 -1
- package/dist/client/session.js +59 -11
- package/dist/client/session.js.map +1 -1
- package/dist/client/sessionStore.d.ts +5 -14
- package/dist/client/sessionStore.d.ts.map +1 -1
- package/dist/client/sessionStore.js +4 -1
- package/dist/client/sessionStore.js.map +1 -1
- package/dist/host/http.d.ts +7 -0
- package/dist/host/http.d.ts.map +1 -0
- package/dist/host/http.js +175 -0
- package/dist/host/http.js.map +1 -0
- package/dist/host/socket.d.ts +102 -1
- package/dist/host/socket.d.ts.map +1 -1
- package/dist/host/socket.js +48 -27
- package/dist/host/socket.js.map +1 -1
- package/dist/host-internal.d.ts +5 -0
- package/dist/host-internal.d.ts.map +1 -1
- package/dist/statewire.d.ts +33 -7
- package/dist/statewire.d.ts.map +1 -1
- package/dist/statewire.js +9 -2
- package/dist/statewire.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/src/client/connection.ts +13 -1
- package/src/client/session.ts +85 -15
- package/src/client/sessionStore.ts +8 -15
- package/src/host/http.ts +280 -0
- package/src/host/socket.ts +120 -57
- package/src/host-internal.ts +5 -0
- package/src/statewire.ts +64 -8
- package/src/types.ts +1 -1
|
@@ -1,17 +1,8 @@
|
|
|
1
|
+
import { StatewireClient } from "../statewire.js";
|
|
1
2
|
//#region src/client/sessionStore.d.ts
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
ctxNextSeq: number;
|
|
6
|
-
};
|
|
7
|
-
type StatewireSessionStore = {
|
|
8
|
-
load: () => StatewireSessionRecord | null;
|
|
9
|
-
save: (record: StatewireSessionRecord) => void;
|
|
10
|
-
};
|
|
11
|
-
type StatewireSessionStoreOptions = {
|
|
12
|
-
sessionStore?: StatewireSessionStore | undefined;
|
|
13
|
-
};
|
|
14
|
-
declare const createMemorySessionStore: () => StatewireSessionStore;
|
|
3
|
+
/** The session storage `useStatewire` mounts, read by the transport's session. */
|
|
4
|
+
declare const SessionStorageContext: import("react").Context<StatewireClient.SessionStorage | undefined>;
|
|
5
|
+
declare const createMemorySessionStore: () => StatewireClient.SessionStorage;
|
|
15
6
|
//#endregion
|
|
16
|
-
export {
|
|
7
|
+
export { SessionStorageContext, createMemorySessionStore };
|
|
17
8
|
//# sourceMappingURL=sessionStore.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sessionStore.d.ts","names":[],"sources":["../../src/client/sessionStore.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"sessionStore.d.ts","names":[],"sources":["../../src/client/sessionStore.ts"],"mappings":";;;cAIa,uCAAqB,QAAA,gBAAA;cAIrB,gCAA+B,gBAAgB"}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { createContext } from "@assistant-ui/tap/react-shim";
|
|
1
2
|
//#region src/client/sessionStore.ts
|
|
3
|
+
/** The session storage `useStatewire` mounts, read by the transport's session. */
|
|
4
|
+
const SessionStorageContext = createContext(void 0);
|
|
2
5
|
const createMemorySessionStore = () => {
|
|
3
6
|
let record = null;
|
|
4
7
|
return {
|
|
@@ -9,6 +12,6 @@ const createMemorySessionStore = () => {
|
|
|
9
12
|
};
|
|
10
13
|
};
|
|
11
14
|
//#endregion
|
|
12
|
-
export { createMemorySessionStore };
|
|
15
|
+
export { SessionStorageContext, createMemorySessionStore };
|
|
13
16
|
|
|
14
17
|
//# sourceMappingURL=sessionStore.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sessionStore.js","names":["
|
|
1
|
+
{"version":3,"file":"sessionStore.js","names":["createContext","StatewireClient","SessionStorageContext","SessionStorage","undefined","createMemorySessionStore","record","SessionRecord","load","save","next"],"sources":["../../src/client/sessionStore.ts"],"sourcesContent":["import { createContext } from \"react\";\nimport type { StatewireClient } from \"../statewire\";\n\n/** The session storage `useStatewire` mounts, read by the transport's session. */\nexport const SessionStorageContext = createContext<\n StatewireClient.SessionStorage | undefined\n>(undefined);\n\nexport const createMemorySessionStore = (): StatewireClient.SessionStorage => {\n let record: StatewireClient.SessionRecord | null = null;\n return {\n load: () => record,\n save: (next) => {\n record = next;\n },\n };\n};\n"],"mappings":";;;AAIA,MAAaE,wBAAwBF,cAEnCI,KAAAA,CAAS;AAEX,MAAaC,iCAAiE;CAC5E,IAAIC,SAA+C;CACnD,OAAO;EACLE,YAAYF;EACZG,OAAOC,SAAS;GACdJ,SAASI;EACX;CACF;AACF"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { SocketHostState } from "./socket.js";
|
|
2
|
+
//#region src/host/http.d.ts
|
|
3
|
+
declare const stream: (h: SocketHostState, request: Request) => Promise<Response>;
|
|
4
|
+
declare const frames: (h: SocketHostState, request: Request) => Promise<Response>;
|
|
5
|
+
//#endregion
|
|
6
|
+
export { frames, stream };
|
|
7
|
+
//# sourceMappingURL=http.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.d.ts","names":[],"sources":["../../src/host/http.ts"],"mappings":";;cAiNa,SACX,GAAG,iBACH,SAAS,YACR,QAAQ;cAkCE,SACX,GAAG,iBACH,SAAS,YACR,QAAQ"}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { CLIENT_ID_RE, admitFrame, closeConn, flush, parseFrame, parseFrameValue, register, sendTo } from "./socket.js";
|
|
2
|
+
//#region src/host/http.ts
|
|
3
|
+
const CLIENT_ID_HEADER = "Statewire-Client-Id";
|
|
4
|
+
const LEASE_HEADER = "Statewire-Lease";
|
|
5
|
+
const VERSION_HEADER = "Statewire-Version";
|
|
6
|
+
const VERSION = "1";
|
|
7
|
+
const encoder = new TextEncoder();
|
|
8
|
+
const invalidClientIdReason = (clientId) => {
|
|
9
|
+
if (clientId === "") return "id must be non-empty";
|
|
10
|
+
if (clientId.length > 256) return "id must be at most 256 characters";
|
|
11
|
+
if (!CLIENT_ID_RE.test(clientId)) return "id must contain only [A-Za-z0-9._~-]";
|
|
12
|
+
return null;
|
|
13
|
+
};
|
|
14
|
+
const clientIdResponse = (request) => {
|
|
15
|
+
const clientId = request.headers.get(CLIENT_ID_HEADER);
|
|
16
|
+
if (clientId === null) return { response: Response.json({ detail: `missing ${CLIENT_ID_HEADER} header` }, { status: 400 }) };
|
|
17
|
+
const reason = invalidClientIdReason(clientId);
|
|
18
|
+
if (reason !== null) return { response: Response.json({ detail: `invalid ${CLIENT_ID_HEADER}: ${reason}` }, { status: 400 }) };
|
|
19
|
+
return { clientId };
|
|
20
|
+
};
|
|
21
|
+
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
|
+
const authorize = async (h, headers) => {
|
|
28
|
+
try {
|
|
29
|
+
await h.authorize?.({ headers });
|
|
30
|
+
return null;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (error instanceof Response) return error;
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
const mintLease = () => {
|
|
37
|
+
const bytes = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16));
|
|
38
|
+
return btoa(String.fromCharCode(...bytes)).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
|
|
39
|
+
};
|
|
40
|
+
const attachSse = (h, clientId) => {
|
|
41
|
+
let controller;
|
|
42
|
+
let connection = null;
|
|
43
|
+
let open = true;
|
|
44
|
+
const body = new ReadableStream({
|
|
45
|
+
start: (next) => {
|
|
46
|
+
controller = next;
|
|
47
|
+
},
|
|
48
|
+
cancel: () => {
|
|
49
|
+
open = false;
|
|
50
|
+
connection?.close();
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
const registration = register(h, clientId, {
|
|
54
|
+
send: (data) => {
|
|
55
|
+
if (!open) return;
|
|
56
|
+
const frame = data === "{}" ? ": ping\n\n" : `data: ${data}\n\n`;
|
|
57
|
+
try {
|
|
58
|
+
controller.enqueue(encoder.encode(frame));
|
|
59
|
+
} catch {
|
|
60
|
+
open = false;
|
|
61
|
+
connection?.close();
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
close: () => {
|
|
65
|
+
if (!open) return;
|
|
66
|
+
open = false;
|
|
67
|
+
try {
|
|
68
|
+
controller.close();
|
|
69
|
+
} catch {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
connection = registration?.connection ?? null;
|
|
75
|
+
if (!open) connection?.close();
|
|
76
|
+
return {
|
|
77
|
+
body,
|
|
78
|
+
registration
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
const streamResponse = (attachment, lease) => new Response(attachment.body, {
|
|
82
|
+
status: 200,
|
|
83
|
+
headers: {
|
|
84
|
+
"Content-Type": "text/event-stream",
|
|
85
|
+
[VERSION_HEADER]: VERSION,
|
|
86
|
+
...lease !== null && { [LEASE_HEADER]: lease }
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
const admitAttachFrame = (h, registration, frame) => {
|
|
90
|
+
if (frame.ctx.length === 0 && frame.cmd.length === 0) return;
|
|
91
|
+
const admission = admitFrame(h, registration.rec, frame);
|
|
92
|
+
if (admission.type === "refused") sendTo(h, registration.conn, JSON.stringify(admission.syn));
|
|
93
|
+
};
|
|
94
|
+
const getStream = async (h, request) => {
|
|
95
|
+
const refused = await authorize(h, headersOf(request));
|
|
96
|
+
if (refused !== null) return refused;
|
|
97
|
+
const client = clientIdResponse(request);
|
|
98
|
+
if ("response" in client) return client.response;
|
|
99
|
+
const attachment = attachSse(h, client.clientId);
|
|
100
|
+
const lease = attachment.registration === null ? null : mintLease();
|
|
101
|
+
if (attachment.registration !== null) attachment.registration.rec.lease = lease;
|
|
102
|
+
return streamResponse(attachment, lease);
|
|
103
|
+
};
|
|
104
|
+
const postStream = async (h, request) => {
|
|
105
|
+
const client = clientIdResponse(request);
|
|
106
|
+
if ("response" in client) return client.response;
|
|
107
|
+
let body;
|
|
108
|
+
try {
|
|
109
|
+
body = JSON.parse(await request.text());
|
|
110
|
+
} catch {
|
|
111
|
+
return Response.json({ detail: "body must be JSON" }, { status: 400 });
|
|
112
|
+
}
|
|
113
|
+
let frame;
|
|
114
|
+
try {
|
|
115
|
+
frame = parseFrameValue(body, true);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
if (!(error instanceof Error) || error.name !== "StatewireViolation") throw error;
|
|
118
|
+
return Response.json({ detail: error.message }, { status: 400 });
|
|
119
|
+
}
|
|
120
|
+
if (frame.headers === null) return Response.json({ detail: "first frame must carry headers" }, { status: 400 });
|
|
121
|
+
const refused = await authorize(h, mergedHeaders(request, frame.headers));
|
|
122
|
+
if (refused !== null) return refused;
|
|
123
|
+
const attachment = attachSse(h, client.clientId);
|
|
124
|
+
if (attachment.registration === null) return streamResponse(attachment, null);
|
|
125
|
+
const lease = mintLease();
|
|
126
|
+
attachment.registration.rec.lease = lease;
|
|
127
|
+
admitAttachFrame(h, attachment.registration, frame);
|
|
128
|
+
return streamResponse(attachment, lease);
|
|
129
|
+
};
|
|
130
|
+
const stream = async (h, request) => {
|
|
131
|
+
if (request.method === "GET") return getStream(h, request);
|
|
132
|
+
if (request.method === "POST") return postStream(h, request);
|
|
133
|
+
return new Response(null, {
|
|
134
|
+
status: 405,
|
|
135
|
+
headers: { Allow: "GET, POST" }
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
const finClient = (h, clientId, fin) => {
|
|
139
|
+
flush(h);
|
|
140
|
+
for (const conn of [...h.conns]) {
|
|
141
|
+
if (conn.clientId !== clientId) continue;
|
|
142
|
+
sendTo(h, conn, JSON.stringify({ fin }));
|
|
143
|
+
closeConn(h, conn);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
const violationResponse = (h, clientId, reason) => {
|
|
147
|
+
finClient(h, clientId, {
|
|
148
|
+
reason: "error",
|
|
149
|
+
message: `protocol violation: ${reason}`
|
|
150
|
+
});
|
|
151
|
+
return Response.json({ error: reason }, { status: 400 });
|
|
152
|
+
};
|
|
153
|
+
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
|
+
const lease = request.headers.get(LEASE_HEADER);
|
|
160
|
+
if (h.disposed || h.finished !== null || rec === void 0 || lease === null || lease !== rec.lease) return Response.json({ error: "stale-lease" }, { status: 423 });
|
|
161
|
+
let frame;
|
|
162
|
+
try {
|
|
163
|
+
frame = parseFrame(await request.text(), false);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
if (!(error instanceof Error) || error.name !== "StatewireViolation") throw error;
|
|
166
|
+
return violationResponse(h, client.clientId, error.message);
|
|
167
|
+
}
|
|
168
|
+
const admission = admitFrame(h, rec, frame);
|
|
169
|
+
if (admission.type === "refused") return Response.json(admission.syn, { status: 409 });
|
|
170
|
+
return Response.json({});
|
|
171
|
+
};
|
|
172
|
+
//#endregion
|
|
173
|
+
export { frames, stream };
|
|
174
|
+
|
|
175
|
+
//# sourceMappingURL=http.js.map
|
|
@@ -0,0 +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","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.rec.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.rec.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 if (\n h.disposed ||\n h.finished !== null ||\n rec === undefined ||\n lease === null ||\n lease !== rec.lease\n ) {\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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"}
|
package/dist/host/socket.d.ts
CHANGED
|
@@ -1,7 +1,108 @@
|
|
|
1
1
|
import { Statewire } from "../types.js";
|
|
2
|
+
import { StatewirePath } from "../wire/ops.js";
|
|
3
|
+
import { CommandsHost } from "./host.js";
|
|
4
|
+
import { StatewireHost } from "../host.js";
|
|
5
|
+
import { StatewireCmd } from "../client/connection.js";
|
|
6
|
+
import { StatewireMetaEntry } from "../wire/meta.js";
|
|
2
7
|
import { StatewireHostInternal } from "../host-internal.js";
|
|
3
8
|
import { ResourceElement } from "@assistant-ui/tap";
|
|
4
9
|
//#region src/host/socket.d.ts
|
|
10
|
+
declare const CLIENT_ID_RE: RegExp;
|
|
11
|
+
type WireOp = {
|
|
12
|
+
op: string;
|
|
13
|
+
path?: StatewirePath;
|
|
14
|
+
value?: unknown;
|
|
15
|
+
meta?: StatewireMetaEntry[];
|
|
16
|
+
};
|
|
17
|
+
type Admission = {
|
|
18
|
+
seq: number;
|
|
19
|
+
method: string;
|
|
20
|
+
applied: boolean;
|
|
21
|
+
appliedSent: boolean;
|
|
22
|
+
settled: boolean;
|
|
23
|
+
};
|
|
24
|
+
type ClientRec = {
|
|
25
|
+
id: string;
|
|
26
|
+
lastSeq: number;
|
|
27
|
+
ctxSeq: number;
|
|
28
|
+
lease: string | null;
|
|
29
|
+
touched: number;
|
|
30
|
+
context: Record<string, unknown> | null;
|
|
31
|
+
results: Map<number, {
|
|
32
|
+
ts: number;
|
|
33
|
+
cmd: StatewireCmd;
|
|
34
|
+
}>;
|
|
35
|
+
inflight: Map<number, Admission>;
|
|
36
|
+
};
|
|
37
|
+
type Conn = {
|
|
38
|
+
clientId: string;
|
|
39
|
+
socket: StatewireHostInternal.Socket;
|
|
40
|
+
open: boolean;
|
|
41
|
+
sawFrame: boolean;
|
|
42
|
+
lastSentAt: number;
|
|
43
|
+
};
|
|
44
|
+
type CtxStatement = {
|
|
45
|
+
seq: number;
|
|
46
|
+
value: Record<string, unknown>;
|
|
47
|
+
};
|
|
48
|
+
type CmdStatement = {
|
|
49
|
+
seq: number;
|
|
50
|
+
method: string;
|
|
51
|
+
params: unknown;
|
|
52
|
+
meta?: unknown;
|
|
53
|
+
};
|
|
54
|
+
type ParsedFrame = {
|
|
55
|
+
headers: Record<string, string> | null;
|
|
56
|
+
ctx: CtxStatement[];
|
|
57
|
+
cmd: CmdStatement[];
|
|
58
|
+
};
|
|
59
|
+
declare const parseFrameValue: (frame: unknown, first: boolean) => ParsedFrame;
|
|
60
|
+
declare const parseFrame: (raw: string, first: boolean) => ParsedFrame;
|
|
61
|
+
type SocketHostState = {
|
|
62
|
+
heartbeatMs: number;
|
|
63
|
+
clientRecordLimit: number;
|
|
64
|
+
finOnIdle: boolean;
|
|
65
|
+
authorize: StatewireHostInternal.SocketHostOptions["authorize"];
|
|
66
|
+
clients: Map<string, ClientRec>;
|
|
67
|
+
conns: Set<Conn>;
|
|
68
|
+
pendingOps: WireOp[];
|
|
69
|
+
pendingApplied: Map<string, number[]>;
|
|
70
|
+
pendingCmds: Map<string, StatewireCmd[]>;
|
|
71
|
+
idleWire: boolean;
|
|
72
|
+
finished: StatewireHost.Fin | null;
|
|
73
|
+
disposed: boolean;
|
|
74
|
+
flushScheduled: boolean;
|
|
75
|
+
heartbeatTimer: ReturnType<typeof setInterval> | null;
|
|
76
|
+
stateHost: StatewireHost.StateHost | null;
|
|
77
|
+
commandsHost: CommandsHost | null;
|
|
78
|
+
current: Statewire<unknown> | null;
|
|
79
|
+
post: () => void;
|
|
80
|
+
connect: (clientId: string, socket: StatewireHostInternal.Socket) => StatewireHostInternal.SocketConnection;
|
|
81
|
+
finish: (fin: StatewireHost.Fin) => void;
|
|
82
|
+
stream: (request: Request) => Promise<Response>;
|
|
83
|
+
frames: (request: Request) => Promise<Response>;
|
|
84
|
+
};
|
|
85
|
+
declare const closeConn: (h: SocketHostState, conn: Conn, code?: number, reason?: string) => void;
|
|
86
|
+
declare const sendTo: (h: SocketHostState, conn: Conn, data: string) => void;
|
|
87
|
+
declare const flush: (h: SocketHostState, fin?: StatewireHost.Fin) => void;
|
|
88
|
+
type FrameAdmission = {
|
|
89
|
+
type: "ok";
|
|
90
|
+
} | {
|
|
91
|
+
type: "refused";
|
|
92
|
+
syn: {
|
|
93
|
+
syn: {
|
|
94
|
+
seq: number;
|
|
95
|
+
ctxSeq: number;
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
declare const admitFrame: (h: SocketHostState, rec: ClientRec, frame: ParsedFrame) => FrameAdmission;
|
|
100
|
+
type Registration = {
|
|
101
|
+
rec: ClientRec;
|
|
102
|
+
conn: Conn;
|
|
103
|
+
connection: StatewireHostInternal.SocketConnection;
|
|
104
|
+
};
|
|
105
|
+
declare const register: (h: SocketHostState, clientId: string, socket: StatewireHostInternal.Socket) => Registration | null;
|
|
5
106
|
/**
|
|
6
107
|
* Transport-agnostic WS binding over a statewire/host transport:
|
|
7
108
|
* feed it sockets (anything with `send`/`close`) and raw text frames, and it
|
|
@@ -11,5 +112,5 @@ import { ResourceElement } from "@assistant-ui/tap";
|
|
|
11
112
|
*/
|
|
12
113
|
declare const StatewireSocketHost: (transport: ResourceElement<Statewire>, options?: StatewireHostInternal.SocketHostOptions) => StatewireHostInternal.SocketHost;
|
|
13
114
|
//#endregion
|
|
14
|
-
export { StatewireSocketHost };
|
|
115
|
+
export { CLIENT_ID_RE, ClientRec, Conn, FrameAdmission, ParsedFrame, Registration, SocketHostState, StatewireSocketHost, admitFrame, closeConn, flush, parseFrame, parseFrameValue, register, sendTo };
|
|
15
116
|
//# 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":";;;;;;;;;cAgBa,cAAY;KAKpB;EACH;EACA,OAAO;EACP;EACA,OAAO;;KAGJ;EACH;EACA;EACA;EACA;EACA;;KAGU;EACV;EACA;EACA;EACA;EACA;EACA,SAAS;EACT,SAAS;IAAc;IAAY,KAAK;;EACxC,UAAU,YAAY;;KAGZ;EACV;EACA,QAAQ,sBAAsB;EAC9B;EACA;EACA;;KAcG;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,WAAW,sBAAsB;EACjC,SAAS,YAAY;EACrB,OAAO,IAAI;EACX,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,WAC3B,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;KAuOlD;EACN;;EACA;EAAiB;IAAO;MAAO;MAAa;;;;cAErC,aACX,GAAG,iBACH,KAAK,WACL,OAAO,gBACN;KAgES;EACV,KAAK;EACL,MAAM;EACN,YAAY,sBAAsB;;cAGvB,WACX,GAAG,iBACH,kBACA,QAAQ,sBAAsB,WAC7B;;;;;;;;cAgLU,sBACX,WAAW,gBAAgB,YAC3B,UAAU,sBAAsB,sBAC/B,sBAAsB"}
|
package/dist/host/socket.js
CHANGED
|
@@ -2,6 +2,7 @@ import { StatewireSendError } from "../types.js";
|
|
|
2
2
|
import { getCommandsHost, getStateHost } from "./host.js";
|
|
3
3
|
import { encodeValue, reviveValue } from "../wire/meta.js";
|
|
4
4
|
import { createMacrotaskPoster } from "../util/macrotask.js";
|
|
5
|
+
import { frames, stream } from "./http.js";
|
|
5
6
|
import { useEffect, useState } from "@assistant-ui/tap/react-shim";
|
|
6
7
|
import { createTapRoot, flushTapSync, useResource } from "@assistant-ui/tap";
|
|
7
8
|
//#region src/host/socket.ts
|
|
@@ -38,13 +39,7 @@ const contextValue = (ops) => {
|
|
|
38
39
|
if (!isPlainObject(value)) throw new Error("context value must be an object");
|
|
39
40
|
return value;
|
|
40
41
|
};
|
|
41
|
-
const
|
|
42
|
-
let frame;
|
|
43
|
-
try {
|
|
44
|
-
frame = JSON.parse(raw);
|
|
45
|
-
} catch {
|
|
46
|
-
return violation("malformed frame");
|
|
47
|
-
}
|
|
42
|
+
const parseFrameValue = (frame, first) => {
|
|
48
43
|
if (!isPlainObject(frame)) return violation("malformed frame");
|
|
49
44
|
if (frame["headers"] === void 0 && frame["ctx"] === void 0 && frame["cmd"] === void 0) return violation("malformed frame");
|
|
50
45
|
let headers = null;
|
|
@@ -80,6 +75,15 @@ const parseFrame = (raw, first) => {
|
|
|
80
75
|
cmd
|
|
81
76
|
};
|
|
82
77
|
};
|
|
78
|
+
const parseFrame = (raw, first) => {
|
|
79
|
+
let frame;
|
|
80
|
+
try {
|
|
81
|
+
frame = JSON.parse(raw);
|
|
82
|
+
} catch {
|
|
83
|
+
return violation("malformed frame");
|
|
84
|
+
}
|
|
85
|
+
return parseFrameValue(frame, first);
|
|
86
|
+
};
|
|
83
87
|
const encodePayload = (payload) => {
|
|
84
88
|
if (payload === void 0) return {};
|
|
85
89
|
const { value, meta } = encodeValue(payload);
|
|
@@ -357,20 +361,19 @@ const admitCmd = (h, rec, statement) => {
|
|
|
357
361
|
});
|
|
358
362
|
});
|
|
359
363
|
};
|
|
360
|
-
const admitFrame = (h,
|
|
361
|
-
const rec = h.clients.get(conn.clientId);
|
|
364
|
+
const admitFrame = (h, rec, frame) => {
|
|
362
365
|
rec.touched = Date.now();
|
|
363
366
|
const through = admittedThrough(rec);
|
|
364
367
|
for (const statement of frame.cmd) if (statement.seq <= through) reemit(h, rec, statement.seq);
|
|
365
368
|
const ctxNew = frame.ctx.filter((s) => s.seq > rec.ctxSeq);
|
|
366
369
|
const cmdNew = frame.cmd.filter((s) => s.seq > through);
|
|
367
|
-
if (ctxNew.length > 0 && ctxNew[0].seq !== rec.ctxSeq + 1 || cmdNew.length > 0 && cmdNew[0].seq !== through + 1) {
|
|
368
|
-
|
|
370
|
+
if (ctxNew.length > 0 && ctxNew[0].seq !== rec.ctxSeq + 1 || cmdNew.length > 0 && cmdNew[0].seq !== through + 1) return {
|
|
371
|
+
type: "refused",
|
|
372
|
+
syn: { syn: {
|
|
369
373
|
seq: through,
|
|
370
374
|
ctxSeq: rec.ctxSeq
|
|
371
|
-
} }
|
|
372
|
-
|
|
373
|
-
}
|
|
375
|
+
} }
|
|
376
|
+
};
|
|
374
377
|
for (const statement of ctxNew) {
|
|
375
378
|
rec.context = statement.value;
|
|
376
379
|
rec.ctxSeq = statement.seq;
|
|
@@ -381,6 +384,7 @@ const admitFrame = (h, conn, frame) => {
|
|
|
381
384
|
dur: true
|
|
382
385
|
});
|
|
383
386
|
scheduleFlush(h);
|
|
387
|
+
return { type: "ok" };
|
|
384
388
|
};
|
|
385
389
|
const message = (h, conn, data) => {
|
|
386
390
|
if (!conn.open || h.disposed || h.finished !== null) return;
|
|
@@ -396,7 +400,9 @@ const message = (h, conn, data) => {
|
|
|
396
400
|
return;
|
|
397
401
|
}
|
|
398
402
|
if (frame.ctx.length === 0 && frame.cmd.length === 0) return;
|
|
399
|
-
|
|
403
|
+
const rec = h.clients.get(conn.clientId);
|
|
404
|
+
const admission = admitFrame(h, rec, frame);
|
|
405
|
+
if (admission.type === "refused") sendTo(h, conn, JSON.stringify(admission.syn));
|
|
400
406
|
};
|
|
401
407
|
const gcClients = (h) => {
|
|
402
408
|
let excess = h.clients.size - h.clientRecordLimit;
|
|
@@ -410,17 +416,12 @@ const gcClients = (h) => {
|
|
|
410
416
|
excess -= 1;
|
|
411
417
|
}
|
|
412
418
|
};
|
|
413
|
-
const
|
|
414
|
-
const dead = {
|
|
415
|
-
clientId,
|
|
416
|
-
message: () => {},
|
|
417
|
-
close: () => {}
|
|
418
|
-
};
|
|
419
|
+
const register = (h, clientId, socket) => {
|
|
419
420
|
const refuse = (code, reason) => {
|
|
420
421
|
try {
|
|
421
422
|
socket.close(code, reason);
|
|
422
423
|
} catch {}
|
|
423
|
-
return
|
|
424
|
+
return null;
|
|
424
425
|
};
|
|
425
426
|
if (h.disposed) return refuse(1001, "disposed");
|
|
426
427
|
if (!CLIENT_ID_RE.test(clientId)) return refuse(1008, "invalid client id");
|
|
@@ -429,7 +430,8 @@ const connect = (h, clientId, socket) => {
|
|
|
429
430
|
try {
|
|
430
431
|
socket.send(JSON.stringify({ fin: h.finished }));
|
|
431
432
|
} catch {}
|
|
432
|
-
|
|
433
|
+
refuse(1e3, "finished");
|
|
434
|
+
return null;
|
|
433
435
|
}
|
|
434
436
|
const known = h.clients.has(clientId);
|
|
435
437
|
const rec = known ? h.clients.get(clientId) : (() => {
|
|
@@ -437,6 +439,7 @@ const connect = (h, clientId, socket) => {
|
|
|
437
439
|
id: clientId,
|
|
438
440
|
lastSeq: 0,
|
|
439
441
|
ctxSeq: 0,
|
|
442
|
+
lease: null,
|
|
440
443
|
touched: Date.now(),
|
|
441
444
|
context: null,
|
|
442
445
|
results: /* @__PURE__ */ new Map(),
|
|
@@ -446,6 +449,7 @@ const connect = (h, clientId, socket) => {
|
|
|
446
449
|
return created;
|
|
447
450
|
})();
|
|
448
451
|
rec.touched = Date.now();
|
|
452
|
+
rec.lease = null;
|
|
449
453
|
for (const other of [...h.conns]) {
|
|
450
454
|
if (other.clientId !== clientId) continue;
|
|
451
455
|
sendTo(h, other, JSON.stringify({ fin: { reason: "superseded" } }));
|
|
@@ -478,9 +482,20 @@ const connect = (h, clientId, socket) => {
|
|
|
478
482
|
};
|
|
479
483
|
sendTo(h, conn, JSON.stringify(hello));
|
|
480
484
|
return {
|
|
485
|
+
rec,
|
|
486
|
+
conn,
|
|
487
|
+
connection: {
|
|
488
|
+
clientId,
|
|
489
|
+
message: (data) => message(h, conn, data),
|
|
490
|
+
close: () => closeConn(h, conn)
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
};
|
|
494
|
+
const connect = (h, clientId, socket) => {
|
|
495
|
+
return register(h, clientId, socket)?.connection ?? {
|
|
481
496
|
clientId,
|
|
482
|
-
message: (
|
|
483
|
-
close: () =>
|
|
497
|
+
message: () => {},
|
|
498
|
+
close: () => {}
|
|
484
499
|
};
|
|
485
500
|
};
|
|
486
501
|
const finish = (h, fin) => {
|
|
@@ -506,6 +521,7 @@ const useSocketHost = (transport, options) => {
|
|
|
506
521
|
heartbeatMs: 15e3,
|
|
507
522
|
clientRecordLimit: CLIENT_RECORD_LIMIT,
|
|
508
523
|
finOnIdle: false,
|
|
524
|
+
authorize: void 0,
|
|
509
525
|
clients: /* @__PURE__ */ new Map(),
|
|
510
526
|
conns: /* @__PURE__ */ new Set(),
|
|
511
527
|
pendingOps: [],
|
|
@@ -524,13 +540,16 @@ const useSocketHost = (transport, options) => {
|
|
|
524
540
|
flush(host);
|
|
525
541
|
}),
|
|
526
542
|
connect: (clientId, socket) => connect(host, clientId, socket),
|
|
527
|
-
finish: (fin) => finish(host, fin)
|
|
543
|
+
finish: (fin) => finish(host, fin),
|
|
544
|
+
stream: (request) => stream(host, request),
|
|
545
|
+
frames: (request_0) => frames(host, request_0)
|
|
528
546
|
};
|
|
529
547
|
return host;
|
|
530
548
|
});
|
|
531
549
|
setHeartbeatMs(h, options?.heartbeatIntervalMs ?? 15e3);
|
|
532
550
|
h.clientRecordLimit = options?.clientRecordLimit ?? CLIENT_RECORD_LIMIT;
|
|
533
551
|
h.finOnIdle = options?.finOnIdle ?? false;
|
|
552
|
+
h.authorize = options?.authorize;
|
|
534
553
|
const value = useResource(transport);
|
|
535
554
|
attach(h, value);
|
|
536
555
|
useEffect(() => {
|
|
@@ -557,6 +576,8 @@ const StatewireSocketHost = (transport, options) => {
|
|
|
557
576
|
let disposed = false;
|
|
558
577
|
return {
|
|
559
578
|
connect: (clientId, socket) => flushTapSync(() => root.getValue().connect(clientId, socket)),
|
|
579
|
+
stream: (request) => flushTapSync(() => root.getValue().stream(request)),
|
|
580
|
+
frames: (request) => flushTapSync(() => root.getValue().frames(request)),
|
|
560
581
|
finish: (fin) => flushTapSync(() => root.getValue().finish(fin)),
|
|
561
582
|
dispose: () => {
|
|
562
583
|
if (disposed) return;
|
|
@@ -567,6 +588,6 @@ const StatewireSocketHost = (transport, options) => {
|
|
|
567
588
|
};
|
|
568
589
|
};
|
|
569
590
|
//#endregion
|
|
570
|
-
export { StatewireSocketHost };
|
|
591
|
+
export { CLIENT_ID_RE, StatewireSocketHost, admitFrame, closeConn, flush, parseFrame, parseFrameValue, register, sendTo };
|
|
571
592
|
|
|
572
593
|
//# sourceMappingURL=socket.js.map
|