statewire 0.8.1 → 0.9.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 +2 -2
- package/dist/client/connection.d.ts +23 -19
- package/dist/client/connection.d.ts.map +1 -1
- package/dist/client/connection.js +143 -103
- package/dist/client/connection.js.map +1 -1
- package/dist/client/core.d.ts +30 -3
- package/dist/client/core.d.ts.map +1 -1
- package/dist/client/core.js +222 -260
- package/dist/client/core.js.map +1 -1
- package/dist/client/session.d.ts +34 -39
- package/dist/client/session.d.ts.map +1 -1
- package/dist/client/session.js +380 -373
- package/dist/client/session.js.map +1 -1
- package/dist/client/sessionStore.d.ts +1 -0
- package/dist/client/sessionStore.d.ts.map +1 -1
- package/dist/client/sessionStore.js.map +1 -1
- package/dist/client/timing.d.ts +1 -2
- package/dist/client/timing.d.ts.map +1 -1
- package/dist/client/timing.js +1 -5
- package/dist/client/timing.js.map +1 -1
- package/dist/host/draft.d.ts +2 -5
- package/dist/host/draft.d.ts.map +1 -1
- package/dist/host/draft.js.map +1 -1
- package/dist/host/host.d.ts +5 -8
- package/dist/host/host.d.ts.map +1 -1
- package/dist/host/host.js +23 -32
- package/dist/host/host.js.map +1 -1
- package/dist/host/socket.d.ts +3 -2
- package/dist/host/socket.d.ts.map +1 -1
- package/dist/host/socket.js +484 -424
- package/dist/host/socket.js.map +1 -1
- package/dist/host-internal.d.ts +4 -0
- package/dist/host-internal.d.ts.map +1 -1
- package/dist/host.d.ts +10 -10
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +2 -2
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -2
- package/dist/statewire.d.ts +29 -24
- package/dist/statewire.d.ts.map +1 -1
- package/dist/statewire.js +8 -2
- package/dist/statewire.js.map +1 -1
- package/dist/transports/fake-sse.d.ts +1 -1
- package/dist/transports/fake-sse.d.ts.map +1 -1
- package/dist/transports/fake-sse.js +5 -2
- package/dist/transports/fake-sse.js.map +1 -1
- package/dist/transports/fixtures.d.ts +1 -2
- package/dist/transports/fixtures.d.ts.map +1 -1
- package/dist/transports/fixtures.js +5 -7
- package/dist/transports/fixtures.js.map +1 -1
- package/dist/transports/remote.d.ts +2 -1
- package/dist/transports/remote.d.ts.map +1 -1
- package/dist/transports/remote.js +10 -4
- package/dist/transports/remote.js.map +1 -1
- package/dist/transports/sse.d.ts +32 -46
- package/dist/transports/sse.d.ts.map +1 -1
- package/dist/transports/sse.js +56 -59
- package/dist/transports/sse.js.map +1 -1
- package/dist/transports/testing.d.ts +7 -5
- package/dist/transports/testing.d.ts.map +1 -1
- package/dist/transports/testing.js +41 -14
- package/dist/transports/testing.js.map +1 -1
- package/dist/transports/ws.d.ts +2 -3
- package/dist/transports/ws.d.ts.map +1 -1
- package/dist/transports/ws.js +25 -80
- package/dist/transports/ws.js.map +1 -1
- package/dist/types.d.ts +8 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +3 -4
- package/dist/types.js.map +1 -1
- package/dist/wire/ops.d.ts +1 -5
- package/dist/wire/ops.d.ts.map +1 -1
- package/dist/wire/ops.js +1 -4
- package/dist/wire/ops.js.map +1 -1
- package/package.json +1 -1
- package/src/client/connection.ts +175 -128
- package/src/client/core.ts +287 -278
- package/src/client/session.ts +474 -480
- package/src/client/sessionStore.ts +1 -0
- package/src/client/timing.ts +0 -6
- package/src/host/draft.ts +1 -3
- package/src/host/host.ts +41 -58
- package/src/host/socket.ts +653 -448
- package/src/host-internal.ts +4 -0
- package/src/host.ts +13 -10
- package/src/index.ts +0 -1
- package/src/statewire.ts +38 -32
- package/src/transports/fake-sse.ts +2 -1
- package/src/transports/fixtures.ts +4 -9
- package/src/transports/remote.ts +12 -5
- package/src/transports/sse.ts +99 -115
- package/src/transports/testing.ts +47 -19
- package/src/transports/ws.ts +44 -115
- package/src/types.ts +15 -14
- package/src/wire/ops.ts +9 -16
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.js","names":["ResourceElement","StatewireEnvelope","StatewireSSE","Statewire","openSSEStream","sseFrame","FakeStreamHandle","tick","Promise","r","setTimeout","applyOptimistic","MockTransport","transport","TState","commands","method","params","context","setState","state","
|
|
1
|
+
{"version":3,"file":"testing.js","names":["ResourceElement","StatewireEnvelope","StatewireSSE","StatewireFrame","Statewire","openSSEStream","sseFrame","FakeStreamHandle","tick","Promise","r","setTimeout","applyOptimistic","MockTransport","transport","TState","commands","method","params","context","setState","state","applied","seq","settle","payload","sendEnvelope","envelope","finish","reason","message","clientId","attached","mockCount","createMockTransport","initialState","finished","clients","Map","ctxSeq","streams","Set","stream","send","admit","id","frame","rec","get","statement","ctx","Math","max","root","ops","value","push","cmd","fakeFetch","input","RequestInfo","URL","init","RequestInit","Response","TypeError","url","String","headers","Headers","endsWith","known","has","set","JSON","parse","body","handle","op","path","syn","record","signal","undefined","json","status","fetch","next","type","fin","close","size"],"sources":["../../src/transports/testing.ts"],"sourcesContent":["import type { ResourceElement } from \"@assistant-ui/tap\";\nimport type { StatewireEnvelope } from \"../client/connection\";\nimport { StatewireSSE } from \"./sse\";\nimport type { StatewireFrame } from \"./sse\";\nimport type { Statewire } from \"../types\";\nimport { openSSEStream, sseFrame } from \"./fake-sse\";\nimport type { FakeStreamHandle } from \"./fake-sse\";\n\nexport * from \"./fixtures\";\n\n/** Let queued microtasks and op batches drain through the statewire client. */\nexport const tick = () => new Promise<void>((r) => setTimeout(r, 0));\n\nexport { applyOptimistic } from \"../util/recordMutations\";\n\nexport type MockTransport<TState> = {\n /** Pass as the `transport:` option of useStatewire / Harness. */\n transport: ResourceElement<Statewire<TState | undefined, any>>;\n /** Captured statements in arrival order: cmd as `{method, params}`, ctx as `{context}`. */\n commands: { method?: string; params?: unknown[]; context?: unknown }[];\n /** Replace the replicated state and broadcast it as a root snapshot op. */\n setState: (state: TState) => Promise<void>;\n /** Deliver a bare ack cmd packet on the stream. */\n applied: (seq: number) => Promise<void>;\n /** Deliver a terminal result cmd packet on the stream. */\n settle: (seq: number, payload?: unknown) => Promise<void>;\n /** Deliver an arbitrary envelope on the stream. */\n sendEnvelope: (envelope: StatewireEnvelope) => Promise<void>;\n /** End the stream with a fin; further attaches fail like a dead server. */\n finish: (reason: string, message?: string) => Promise<void>;\n readonly clientId: string;\n readonly attached: boolean;\n};\n\n/**\n * A mock statewire backend: serves `initialState` as the attach snapshot,\n * records submitted frames (receipt only — acks and results are delivered\n * from test code), and exposes the wire as a transport ResourceElement.\n */\nlet mockCount = 0;\n\nexport const createMockTransport = <TState>(\n initialState: TState,\n): MockTransport<TState> => {\n let state: unknown = initialState;\n let clientId = \"\";\n let finished = false;\n const clients = new Map<string, { seq: number; ctxSeq: number }>();\n const streams = new Set<FakeStreamHandle>();\n const commands: MockTransport<unknown>[\"commands\"] = [];\n\n const sendEnvelope = (envelope: StatewireEnvelope) => {\n for (const stream of streams) stream.send(sseFrame(envelope));\n return tick();\n };\n\n const admit = (id: string, frame: StatewireFrame) => {\n const rec = clients.get(id)!;\n for (const statement of frame.ctx ?? []) {\n rec.ctxSeq = Math.max(rec.ctxSeq, statement.seq);\n const root = statement.ops[0] as { value?: unknown } | undefined;\n commands.push({ context: root?.value });\n }\n for (const statement of frame.cmd ?? []) {\n rec.seq = Math.max(rec.seq, statement.seq);\n commands.push({ method: statement.method, params: statement.params });\n }\n };\n\n const fakeFetch = (async (\n input: RequestInfo | URL,\n init?: RequestInit,\n ): Promise<Response> => {\n if (finished) throw new TypeError(\"fetch failed (finished)\");\n const url = String(input);\n const headers = new Headers(init?.headers);\n const id = headers.get(\"Statewire-Client-Id\") ?? \"\";\n if (url.endsWith(\"/stream\")) {\n clientId = id;\n const known = clients.has(id);\n if (!known) clients.set(id, { seq: 0, ctxSeq: 0 });\n const rec = clients.get(id)!;\n const frame = JSON.parse(String(init?.body)) as StatewireFrame;\n admit(id, frame);\n return openSSEStream(\n streams,\n id,\n (handle) => {\n handle.send(\n sseFrame({\n ops: [{ op: \"replace\", path: [], value: state }],\n syn: {\n seq: rec.seq,\n ctxSeq: rec.ctxSeq,\n record: known ? \"resumed\" : \"fresh\",\n },\n }),\n );\n },\n init?.signal ?? undefined,\n { \"Statewire-Lease\": \"mock-lease\" },\n );\n }\n if (url.endsWith(\"/frames\")) {\n admit(id, JSON.parse(String(init?.body)) as StatewireFrame);\n return Response.json({});\n }\n return new Response(null, { status: 404 });\n }) as typeof fetch;\n\n return {\n transport: StatewireSSE({\n url: `http://mock-${++mockCount}/api`,\n fetch: fakeFetch,\n }),\n commands,\n setState: (next) => {\n state = next;\n return sendEnvelope({ ops: [{ op: \"replace\", path: [], value: next }] });\n },\n applied: (seq) => sendEnvelope({ cmd: [{ seq, type: \"applied\" }] }),\n settle: (seq, payload) =>\n sendEnvelope({\n cmd: [\n {\n seq,\n type: \"result\",\n ...(payload !== undefined && { payload }),\n },\n ],\n }),\n sendEnvelope,\n finish: async (reason, message) => {\n await sendEnvelope({\n fin: { reason, ...(message !== undefined && { message }) },\n });\n finished = true;\n for (const stream of [...streams]) stream.close();\n await tick();\n },\n get clientId() {\n return clientId;\n },\n get attached() {\n return streams.size > 0;\n },\n };\n};\n"],"mappings":"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"}
|
package/dist/transports/ws.d.ts
CHANGED
|
@@ -3,8 +3,7 @@ import { Statewire } from "../types.js";
|
|
|
3
3
|
import { ResourceElement } from "@assistant-ui/tap";
|
|
4
4
|
//#region src/transports/ws.d.ts
|
|
5
5
|
declare const WS_SUBPROTOCOL = "statewire.v1";
|
|
6
|
-
declare const
|
|
7
|
-
declare const StatewireWS: <TState = any, TCommands extends Statewire.Commands = Statewire.Commands, TEvent = any>(options: StatewireClient.WSOptions) => ResourceElement<Statewire<TState | undefined, TCommands, TEvent>>;
|
|
6
|
+
declare const StatewireWS: <TState = any, TCommands extends Statewire.Commands = Statewire.Commands>(options: StatewireClient.WSOptions) => ResourceElement<Statewire<TState | undefined, TCommands>>;
|
|
8
7
|
//#endregion
|
|
9
|
-
export { StatewireWS,
|
|
8
|
+
export { StatewireWS, WS_SUBPROTOCOL };
|
|
10
9
|
//# sourceMappingURL=ws.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ws.d.ts","names":[],"sources":["../../src/transports/ws.ts"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"ws.d.ts","names":[],"sources":["../../src/transports/ws.ts"],"mappings":";;;;cA4Ba;cAgLA,cACX,cACA,kBAAkB,UAAU,WAAW,UAAU,UAEjD,SAAS,gBAAgB,cACtB,gBAAgB,UAAU,oBAAoB"}
|
package/dist/transports/ws.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { CLIENT_QUERY_PARAM } from "../host/adapter.js";
|
|
2
2
|
import { timing } from "../client/timing.js";
|
|
3
|
-
import { handleEnvelope,
|
|
3
|
+
import { handleEnvelope, useStatewireConn } from "../client/connection.js";
|
|
4
4
|
import { useStatewireRemote } from "./remote.js";
|
|
5
|
-
import { encodeWireCommand } from "./sse.js";
|
|
6
5
|
import { useState } from "@assistant-ui/tap/react-shim";
|
|
7
6
|
import { flushTapSync, resource } from "@assistant-ui/tap";
|
|
8
7
|
//#region src/transports/ws.ts
|
|
@@ -15,33 +14,14 @@ const socketUrl = async (options, clientId) => {
|
|
|
15
14
|
params.set(CLIENT_QUERY_PARAM, clientId);
|
|
16
15
|
return `${url.replace(/\/$/, "").replace(/^http/, "ws")}/ws?${params.toString()}`;
|
|
17
16
|
};
|
|
18
|
-
const
|
|
17
|
+
const firstFrame = async (options, stale, lanes) => {
|
|
19
18
|
const { headers } = options;
|
|
20
|
-
if (headers === void 0) return null;
|
|
21
19
|
const resolved = new Headers(typeof headers === "function" ? await headers({ staleAuth: stale }) : headers);
|
|
22
20
|
return JSON.stringify({
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
headers: Object.fromEntries(resolved),
|
|
22
|
+
...lanes ?? {}
|
|
25
23
|
});
|
|
26
24
|
};
|
|
27
|
-
const settleOverloaded = (w, { seq, retryAfterMs }) => {
|
|
28
|
-
if (seq === void 0) throw overloadedError(retryAfterMs);
|
|
29
|
-
const entry = w.pending.find((p) => p.seq === seq);
|
|
30
|
-
if (!entry) return;
|
|
31
|
-
w.pending = w.pending.filter((p) => p !== entry);
|
|
32
|
-
entry.reject(overloadedError(retryAfterMs));
|
|
33
|
-
};
|
|
34
|
-
const resolvePending = (w, matches) => {
|
|
35
|
-
const remaining = [];
|
|
36
|
-
for (const entry of w.pending) if (matches(entry)) entry.resolve({ type: "receipt" });
|
|
37
|
-
else remaining.push(entry);
|
|
38
|
-
w.pending = remaining;
|
|
39
|
-
};
|
|
40
|
-
const failPending = (w, error) => {
|
|
41
|
-
const pending = w.pending;
|
|
42
|
-
w.pending = [];
|
|
43
|
-
for (const entry of pending) entry.reject(error);
|
|
44
|
-
};
|
|
45
25
|
const runSocket = (c, w, ws, frame, staleAuth, abort) => new Promise((resolve, reject) => {
|
|
46
26
|
let timer;
|
|
47
27
|
let timedOut = false;
|
|
@@ -57,41 +37,30 @@ const runSocket = (c, w, ws, frame, staleAuth, abort) => new Promise((resolve, r
|
|
|
57
37
|
arm();
|
|
58
38
|
abort.signal.addEventListener("abort", () => ws.close());
|
|
59
39
|
ws.onerror = () => {};
|
|
60
|
-
|
|
40
|
+
ws.onopen = () => ws.send(frame);
|
|
61
41
|
const stale = () => Boolean(outcome) || closed || abort.signal.aborted;
|
|
62
42
|
const fail = (error) => {
|
|
63
43
|
clearTimeout(timer);
|
|
64
44
|
reject(error instanceof Error ? error : /* @__PURE__ */ new Error("protocol"));
|
|
65
45
|
ws.close();
|
|
66
46
|
};
|
|
67
|
-
const processEnv = (env) => {
|
|
68
|
-
const done = flushTapSync(() => handleEnvelope(c, env));
|
|
69
|
-
if (env.ack !== void 0) {
|
|
70
|
-
const ack = env.ack;
|
|
71
|
-
resolvePending(w, (entry) => entry.seq <= ack);
|
|
72
|
-
}
|
|
73
|
-
if (env.res !== void 0) {
|
|
74
|
-
const seqs = new Set(env.res.map((r) => r.seq));
|
|
75
|
-
resolvePending(w, (entry) => seqs.has(entry.seq));
|
|
76
|
-
}
|
|
77
|
-
if (done) {
|
|
78
|
-
outcome = done;
|
|
79
|
-
ws.close();
|
|
80
|
-
}
|
|
81
|
-
};
|
|
82
47
|
ws.onmessage = (event) => {
|
|
83
48
|
if (stale()) return;
|
|
84
49
|
arm();
|
|
85
|
-
let
|
|
50
|
+
let env;
|
|
86
51
|
try {
|
|
87
|
-
|
|
52
|
+
env = JSON.parse(String(event.data));
|
|
88
53
|
} catch {
|
|
89
54
|
fail(/* @__PURE__ */ new Error("protocol"));
|
|
90
55
|
return;
|
|
91
56
|
}
|
|
57
|
+
if (typeof env === "object" && env !== null && Object.keys(env).length === 0) return;
|
|
92
58
|
try {
|
|
93
|
-
|
|
94
|
-
|
|
59
|
+
const done = flushTapSync(() => handleEnvelope(c, env));
|
|
60
|
+
if (done) {
|
|
61
|
+
outcome = done;
|
|
62
|
+
ws.close();
|
|
63
|
+
}
|
|
95
64
|
} catch (error) {
|
|
96
65
|
fail(error);
|
|
97
66
|
}
|
|
@@ -99,9 +68,8 @@ const runSocket = (c, w, ws, frame, staleAuth, abort) => new Promise((resolve, r
|
|
|
99
68
|
ws.onclose = (event) => {
|
|
100
69
|
closed = true;
|
|
101
70
|
clearTimeout(timer);
|
|
102
|
-
if (w.socket === ws) failPending(w, /* @__PURE__ */ new Error("network"));
|
|
103
71
|
if (outcome) resolve(outcome);
|
|
104
|
-
else if (event?.code ===
|
|
72
|
+
else if (event?.code === UNAUTHORIZED_CLOSE) {
|
|
105
73
|
if (staleAuth) c.episode = { cause: "unauthorized" };
|
|
106
74
|
w.authStale = true;
|
|
107
75
|
reject(/* @__PURE__ */ new Error("unauthorized"));
|
|
@@ -114,7 +82,8 @@ const attemptSocket = async (c, w, options, abort) => {
|
|
|
114
82
|
const url = await socketUrl(options, c.ctx.clientId);
|
|
115
83
|
const stale = w.authStale;
|
|
116
84
|
w.authStale = false;
|
|
117
|
-
const
|
|
85
|
+
const lanes = flushTapSync(() => c.ctx.takeAttachLanes());
|
|
86
|
+
const frame = await firstFrame(options, stale, lanes);
|
|
118
87
|
abort.signal.throwIfAborted();
|
|
119
88
|
const { protocols, webSocket } = options;
|
|
120
89
|
const WS = webSocket ?? globalThis.WebSocket;
|
|
@@ -126,43 +95,19 @@ const attemptSocket = async (c, w, options, abort) => {
|
|
|
126
95
|
if (w.socket === ws) w.socket = null;
|
|
127
96
|
}
|
|
128
97
|
};
|
|
129
|
-
const
|
|
98
|
+
const sendFrame = (w, frame) => {
|
|
130
99
|
const ws = w.socket;
|
|
131
100
|
if (!ws || ws.readyState !== OPEN) return Promise.reject(/* @__PURE__ */ new Error("network"));
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
},
|
|
139
|
-
reject: (error) => {
|
|
140
|
-
clearTimeout(timer);
|
|
141
|
-
reject(error);
|
|
142
|
-
}
|
|
143
|
-
};
|
|
144
|
-
const timer = setTimeout(() => {
|
|
145
|
-
w.pending = w.pending.filter((p) => p !== entry);
|
|
146
|
-
reject(/* @__PURE__ */ new Error("timeout"));
|
|
147
|
-
ws.close();
|
|
148
|
-
}, timing.heartbeatTimeoutMs);
|
|
149
|
-
w.pending.push(entry);
|
|
150
|
-
try {
|
|
151
|
-
ws.send(JSON.stringify({
|
|
152
|
-
...encodeWireCommand(command),
|
|
153
|
-
seq
|
|
154
|
-
}));
|
|
155
|
-
} catch {
|
|
156
|
-
clearTimeout(timer);
|
|
157
|
-
w.pending = w.pending.filter((p) => p !== entry);
|
|
158
|
-
reject(/* @__PURE__ */ new Error("network"));
|
|
159
|
-
}
|
|
160
|
-
});
|
|
101
|
+
try {
|
|
102
|
+
ws.send(JSON.stringify(frame));
|
|
103
|
+
} catch {
|
|
104
|
+
return Promise.reject(/* @__PURE__ */ new Error("network"));
|
|
105
|
+
}
|
|
106
|
+
return Promise.resolve({ type: "ok" });
|
|
161
107
|
};
|
|
162
108
|
const useWsLink = (options, ctx) => {
|
|
163
109
|
const [w] = useState(() => ({
|
|
164
110
|
socket: null,
|
|
165
|
-
pending: [],
|
|
166
111
|
authStale: false
|
|
167
112
|
}));
|
|
168
113
|
const c_0 = useStatewireConn({
|
|
@@ -175,13 +120,13 @@ const useWsLink = (options, ctx) => {
|
|
|
175
120
|
get connection() {
|
|
176
121
|
return c_0.connection;
|
|
177
122
|
},
|
|
178
|
-
send: (
|
|
123
|
+
send: (frame) => sendFrame(w, frame)
|
|
179
124
|
}));
|
|
180
125
|
return link;
|
|
181
126
|
};
|
|
182
127
|
const useStatewireWS = (options) => useStatewireRemote(options, useWsLink);
|
|
183
128
|
const StatewireWS = resource(useStatewireWS);
|
|
184
129
|
//#endregion
|
|
185
|
-
export { StatewireWS,
|
|
130
|
+
export { StatewireWS, WS_SUBPROTOCOL };
|
|
186
131
|
|
|
187
132
|
//# sourceMappingURL=ws.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ws.js","names":["useState","flushTapSync","resource","ResourceElement","timing","handleEnvelope","overloadedError","useStatewireConn","Conn","StatewireEnvelope","Statewire","StatewireClient","useStatewireRemote","CLIENT_QUERY_PARAM","encodeWireCommand","RemoteLink","StatewireSubmitResult","StatewireRemoteProps","StatewireWireCommand","PendingCommand","seq","resolve","result","reject","error","Error","WsChannel","socket","WebSocket","pending","authStale","OPEN","WS_SUBPROTOCOL","UNAUTHORIZED_CLOSE","socketUrl","options","WSOptions","clientId","url","query","params","URLSearchParams","set","base","replace","toString","headersFrame","stale","Promise","headers","undefined","resolved","Headers","staleAuth","JSON","stringify","type","Object","fromEntries","settleOverloaded","w","retryAfterMs","entry","find","p","filter","resolvePending","matches","remaining","push","failPending","runSocket","c","ws","frame","abort","AbortController","timer","ReturnType","setTimeout","timedOut","outcome","closed","arm","clearTimeout","close","heartbeatTimeoutMs","signal","addEventListener","onerror","onopen","send","Boolean","aborted","fail","processEnv","env","done","ack","res","seqs","Set","map","r","has","onmessage","event","overloaded","parse","String","data","onclose","code","episode","cause","attemptSocket","ctx","throwIfAborted","protocols","webSocket","WS","globalThis","extra","sendCommand","command","readyState","useWsLink","onChange","attemptOnce","link","connection","args","useStatewireWS","StatewireWS","Commands","TState","TCommands","TEvent"],"sources":["../../src/transports/ws.ts"],"sourcesContent":["import { useState } from \"react\";\nimport { flushTapSync, resource } from \"@assistant-ui/tap\";\nimport type { ResourceElement } from \"@assistant-ui/tap\";\nimport { timing } from \"../client/timing\";\nimport {\n handleEnvelope,\n overloadedError,\n useStatewireConn,\n} from \"../client/connection\";\nimport type { Conn, StatewireEnvelope } from \"../client/connection\";\nimport type { Statewire } from \"../types\";\nimport type { StatewireClient } from \"../statewire\";\nimport { useStatewireRemote } from \"./remote\";\nimport { CLIENT_QUERY_PARAM } from \"../host/adapter\";\nimport { encodeWireCommand } from \"./sse\";\nimport type {\n RemoteLink,\n StatewireSubmitResult,\n StatewireRemoteProps,\n StatewireWireCommand,\n} from \"./sse\";\n\ntype PendingCommand = {\n seq: number;\n resolve: (result: StatewireSubmitResult) => void;\n reject: (error: Error) => void;\n};\n\ntype WsChannel = {\n socket: WebSocket | null;\n pending: PendingCommand[];\n authStale: boolean;\n};\n\nconst OPEN = 1;\n\nexport const WS_SUBPROTOCOL = \"statewire.v1\";\nexport const UNAUTHORIZED_CLOSE = 4401;\n\nconst socketUrl = async (\n options: StatewireClient.WSOptions,\n clientId: string,\n) => {\n const { url, query } = options;\n const params = new URLSearchParams(\n typeof query === \"function\" ? await query() : query,\n );\n params.set(CLIENT_QUERY_PARAM, clientId);\n const base = url.replace(/\\/$/, \"\").replace(/^http/, \"ws\");\n return `${base}/ws?${params.toString()}`;\n};\n\nconst headersFrame = async (\n options: StatewireClient.WSOptions,\n stale: boolean,\n): Promise<string | null> => {\n const { headers } = options;\n if (headers === undefined) return null;\n const resolved = new Headers(\n typeof headers === \"function\"\n ? await headers({ staleAuth: stale })\n : headers,\n );\n return JSON.stringify({\n type: \"headers\",\n headers: Object.fromEntries(resolved),\n });\n};\n\nconst settleOverloaded = (\n w: WsChannel,\n { seq, retryAfterMs }: { seq?: number; retryAfterMs?: number },\n) => {\n if (seq === undefined) throw overloadedError(retryAfterMs);\n const entry = w.pending.find((p) => p.seq === seq);\n if (!entry) return;\n w.pending = w.pending.filter((p) => p !== entry);\n entry.reject(overloadedError(retryAfterMs));\n};\n\nconst resolvePending = (\n w: WsChannel,\n matches: (entry: PendingCommand) => boolean,\n) => {\n const remaining: PendingCommand[] = [];\n for (const entry of w.pending) {\n if (matches(entry)) entry.resolve({ type: \"receipt\" });\n else remaining.push(entry);\n }\n w.pending = remaining;\n};\n\nconst failPending = (w: WsChannel, error: Error) => {\n const pending = w.pending;\n w.pending = [];\n for (const entry of pending) entry.reject(error);\n};\n\nconst runSocket = (\n c: Conn,\n w: WsChannel,\n ws: WebSocket,\n frame: string | null,\n staleAuth: boolean,\n abort: AbortController,\n): Promise<\"stop\" | \"reattach\"> =>\n new Promise((resolve, reject) => {\n let timer: ReturnType<typeof setTimeout> | undefined;\n let timedOut = false;\n let outcome: \"stop\" | \"reattach\" | null = null;\n let closed = false;\n const arm = () => {\n clearTimeout(timer);\n timer = setTimeout(() => {\n timedOut = true;\n ws.close();\n }, timing.heartbeatTimeoutMs);\n };\n arm();\n abort.signal.addEventListener(\"abort\", () => ws.close());\n ws.onerror = () => {};\n if (frame !== null) ws.onopen = () => ws.send(frame);\n const stale = () => Boolean(outcome) || closed || abort.signal.aborted;\n const fail = (error: unknown) => {\n clearTimeout(timer);\n reject(error instanceof Error ? error : new Error(\"protocol\"));\n ws.close();\n };\n const processEnv = (env: StatewireEnvelope) => {\n const done = flushTapSync(() => handleEnvelope(c, env));\n if (env.ack !== undefined) {\n const ack = env.ack;\n resolvePending(w, (entry) => entry.seq <= ack);\n }\n if (env.res !== undefined) {\n const seqs = new Set(env.res.map((r) => r.seq));\n resolvePending(w, (entry) => seqs.has(entry.seq));\n }\n if (done) {\n outcome = done;\n ws.close();\n }\n };\n ws.onmessage = (event) => {\n if (stale()) return;\n arm();\n let frame: StatewireEnvelope & {\n overloaded?: { seq?: number; retryAfterMs?: number };\n };\n try {\n frame = JSON.parse(String(event.data)) as typeof frame;\n } catch {\n fail(new Error(\"protocol\"));\n return;\n }\n try {\n if (frame.overloaded !== undefined) {\n settleOverloaded(w, frame.overloaded);\n } else {\n processEnv(frame);\n }\n } catch (error) {\n fail(error);\n }\n };\n ws.onclose = (event?: { code?: number }) => {\n closed = true;\n clearTimeout(timer);\n if (w.socket === ws) failPending(w, new Error(\"network\"));\n if (outcome) resolve(outcome);\n else if (event?.code === UNAUTHORIZED_CLOSE) {\n if (staleAuth) c.episode = { cause: \"unauthorized\" };\n w.authStale = true;\n reject(new Error(\"unauthorized\"));\n } else reject(new Error(timedOut ? \"timeout\" : \"network\"));\n };\n });\n\nconst attemptSocket = async (\n c: Conn,\n w: WsChannel,\n options: StatewireClient.WSOptions,\n abort: AbortController,\n): Promise<\"stop\" | \"reattach\"> => {\n let ws: WebSocket | null = null;\n try {\n const url = await socketUrl(options, c.ctx.clientId);\n const stale = w.authStale;\n w.authStale = false;\n const frame = await headersFrame(options, stale);\n abort.signal.throwIfAborted();\n const { protocols, webSocket } = options;\n const WS = webSocket ?? globalThis.WebSocket;\n if (!WS) throw new Error(\"no WebSocket implementation\");\n const extra =\n protocols === undefined\n ? []\n : typeof protocols === \"string\"\n ? [protocols]\n : protocols;\n ws = new WS(url, [WS_SUBPROTOCOL, ...extra]);\n w.socket = ws;\n return await runSocket(c, w, ws, frame, stale, abort);\n } finally {\n if (w.socket === ws) w.socket = null;\n }\n};\n\nconst sendCommand = (\n w: WsChannel,\n { seq, command }: { seq: number; command: StatewireWireCommand },\n): Promise<StatewireSubmitResult> => {\n const ws = w.socket;\n if (!ws || ws.readyState !== OPEN) {\n return Promise.reject(new Error(\"network\"));\n }\n return new Promise((resolve, reject) => {\n const entry: PendingCommand = {\n seq,\n resolve: (result) => {\n clearTimeout(timer);\n resolve(result);\n },\n reject: (error) => {\n clearTimeout(timer);\n reject(error);\n },\n };\n // A timed-out command fails the socket: the retry then rides reconnect +\n // attach-seq arbitration.\n const timer = setTimeout(() => {\n w.pending = w.pending.filter((p) => p !== entry);\n reject(new Error(\"timeout\"));\n ws.close();\n }, timing.heartbeatTimeoutMs);\n w.pending.push(entry);\n try {\n ws.send(JSON.stringify({ ...encodeWireCommand(command), seq }));\n } catch {\n clearTimeout(timer);\n w.pending = w.pending.filter((p) => p !== entry);\n reject(new Error(\"network\"));\n }\n });\n};\n\nconst useWsLink = (\n options: StatewireClient.WSOptions,\n ctx: StatewireRemoteProps,\n): RemoteLink => {\n const [w] = useState(\n (): WsChannel => ({ socket: null, pending: [], authStale: false }),\n );\n const c = useStatewireConn({\n url: options.url,\n onChange: options.onChange,\n ctx,\n attemptOnce: (c, abort) => attemptSocket(c, w, options, abort),\n });\n const [link] = useState(\n (): RemoteLink => ({\n get connection() {\n return c.connection;\n },\n send: (args) => sendCommand(w, args),\n }),\n );\n return link;\n};\n\nconst useStatewireWS = (\n options: StatewireClient.WSOptions,\n): Statewire<any, any, any> => useStatewireRemote(options, useWsLink);\n\nexport const StatewireWS = resource(useStatewireWS) as <\n TState = any,\n TCommands extends Statewire.Commands = Statewire.Commands,\n TEvent = any,\n>(\n options: StatewireClient.WSOptions,\n) => ResourceElement<Statewire<TState | undefined, TCommands, TEvent>>;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ws.js","names":["useState","flushTapSync","resource","ResourceElement","timing","handleEnvelope","useStatewireConn","AttemptOutcome","Conn","StatewireEnvelope","Statewire","StatewireClient","useStatewireRemote","CLIENT_QUERY_PARAM","RemoteLink","StatewireFrame","StatewireSubmitResult","StatewireRemoteProps","WsChannel","socket","WebSocket","authStale","OPEN","WS_SUBPROTOCOL","UNAUTHORIZED_CLOSE","socketUrl","options","WSOptions","clientId","url","query","params","URLSearchParams","set","base","replace","toString","firstFrame","stale","lanes","Promise","headers","resolved","Headers","staleAuth","JSON","stringify","Object","fromEntries","runSocket","c","w","ws","frame","abort","AbortController","resolve","reject","timer","ReturnType","setTimeout","timedOut","outcome","closed","arm","clearTimeout","close","heartbeatTimeoutMs","signal","addEventListener","onerror","onopen","send","Boolean","aborted","fail","error","Error","onmessage","event","env","parse","String","data","keys","length","done","onclose","code","episode","cause","attemptSocket","ctx","takeAttachLanes","throwIfAborted","protocols","webSocket","WS","globalThis","extra","undefined","sendFrame","readyState","type","useWsLink","onChange","attemptOnce","link","connection","useStatewireWS","StatewireWS","Commands","TState","TCommands"],"sources":["../../src/transports/ws.ts"],"sourcesContent":["import { useState } from \"react\";\nimport { flushTapSync, resource } from \"@assistant-ui/tap\";\nimport type { ResourceElement } from \"@assistant-ui/tap\";\nimport { timing } from \"../client/timing\";\nimport { handleEnvelope, useStatewireConn } from \"../client/connection\";\nimport type {\n AttemptOutcome,\n Conn,\n StatewireEnvelope,\n} from \"../client/connection\";\nimport type { Statewire } from \"../types\";\nimport type { StatewireClient } from \"../statewire\";\nimport { useStatewireRemote } from \"./remote\";\nimport { CLIENT_QUERY_PARAM } from \"../host/adapter\";\nimport type {\n RemoteLink,\n StatewireFrame,\n StatewireSubmitResult,\n StatewireRemoteProps,\n} from \"./sse\";\n\ntype WsChannel = {\n socket: WebSocket | null;\n authStale: boolean;\n};\n\nconst OPEN = 1;\n\nexport const WS_SUBPROTOCOL = \"statewire.v1\";\nconst UNAUTHORIZED_CLOSE = 4401;\n\nconst socketUrl = async (\n options: StatewireClient.WSOptions,\n clientId: string,\n) => {\n const { url, query } = options;\n const params = new URLSearchParams(\n typeof query === \"function\" ? await query() : query,\n );\n params.set(CLIENT_QUERY_PARAM, clientId);\n const base = url.replace(/\\/$/, \"\").replace(/^http/, \"ws\");\n return `${base}/ws?${params.toString()}`;\n};\n\nconst firstFrame = async (\n options: StatewireClient.WSOptions,\n stale: boolean,\n lanes: StatewireFrame | null,\n): Promise<string> => {\n const { headers } = options;\n const resolved = new Headers(\n typeof headers === \"function\"\n ? await headers({ staleAuth: stale })\n : headers,\n );\n return JSON.stringify({\n headers: Object.fromEntries(resolved),\n ...(lanes ?? {}),\n });\n};\n\nconst runSocket = (\n c: Conn,\n w: WsChannel,\n ws: WebSocket,\n frame: string,\n staleAuth: boolean,\n abort: AbortController,\n): Promise<AttemptOutcome> =>\n new Promise((resolve, reject) => {\n let timer: ReturnType<typeof setTimeout> | undefined;\n let timedOut = false;\n let outcome: AttemptOutcome | null = null;\n let closed = false;\n const arm = () => {\n clearTimeout(timer);\n timer = setTimeout(() => {\n timedOut = true;\n ws.close();\n }, timing.heartbeatTimeoutMs);\n };\n arm();\n abort.signal.addEventListener(\"abort\", () => ws.close());\n ws.onerror = () => {};\n ws.onopen = () => ws.send(frame);\n const stale = () => Boolean(outcome) || closed || abort.signal.aborted;\n const fail = (error: unknown) => {\n clearTimeout(timer);\n reject(error instanceof Error ? error : new Error(\"protocol\"));\n ws.close();\n };\n ws.onmessage = (event) => {\n if (stale()) return;\n arm();\n let env: StatewireEnvelope;\n try {\n env = JSON.parse(String(event.data)) as StatewireEnvelope;\n } catch {\n fail(new Error(\"protocol\"));\n return;\n }\n // {} heartbeats live below the packet layer: invisible to handlers.\n if (\n typeof env === \"object\" &&\n env !== null &&\n Object.keys(env).length === 0\n ) {\n return;\n }\n try {\n const done = flushTapSync(() => handleEnvelope(c, env));\n if (done) {\n outcome = done;\n ws.close();\n }\n } catch (error) {\n fail(error);\n }\n };\n ws.onclose = (event?: { code?: number }) => {\n closed = true;\n clearTimeout(timer);\n if (outcome) resolve(outcome);\n else if (event?.code === UNAUTHORIZED_CLOSE) {\n if (staleAuth) c.episode = { cause: \"unauthorized\" };\n w.authStale = true;\n reject(new Error(\"unauthorized\"));\n } else reject(new Error(timedOut ? \"timeout\" : \"network\"));\n };\n });\n\nconst attemptSocket = async (\n c: Conn,\n w: WsChannel,\n options: StatewireClient.WSOptions,\n abort: AbortController,\n): Promise<AttemptOutcome> => {\n let ws: WebSocket | null = null;\n try {\n const url = await socketUrl(options, c.ctx.clientId);\n const stale = w.authStale;\n w.authStale = false;\n const lanes = flushTapSync(() => c.ctx.takeAttachLanes());\n const frame = await firstFrame(options, stale, lanes);\n abort.signal.throwIfAborted();\n const { protocols, webSocket } = options;\n const WS = webSocket ?? globalThis.WebSocket;\n if (!WS) throw new Error(\"no WebSocket implementation\");\n const extra =\n protocols === undefined\n ? []\n : typeof protocols === \"string\"\n ? [protocols]\n : protocols;\n ws = new WS(url, [WS_SUBPROTOCOL, ...extra]);\n w.socket = ws;\n return await runSocket(c, w, ws, frame, stale, abort);\n } finally {\n if (w.socket === ws) w.socket = null;\n }\n};\n\nconst sendFrame = (\n w: WsChannel,\n frame: StatewireFrame,\n): Promise<StatewireSubmitResult> => {\n const ws = w.socket;\n if (!ws || ws.readyState !== OPEN) {\n return Promise.reject(new Error(\"network\"));\n }\n try {\n ws.send(JSON.stringify(frame));\n } catch {\n return Promise.reject(new Error(\"network\"));\n }\n return Promise.resolve({ type: \"ok\" });\n};\n\nconst useWsLink = (\n options: StatewireClient.WSOptions,\n ctx: StatewireRemoteProps,\n): RemoteLink => {\n const [w] = useState((): WsChannel => ({ socket: null, authStale: false }));\n const c = useStatewireConn({\n url: options.url,\n onChange: options.onChange,\n ctx,\n attemptOnce: (c, abort) => attemptSocket(c, w, options, abort),\n });\n const [link] = useState(\n (): RemoteLink => ({\n get connection() {\n return c.connection;\n },\n send: (frame) => sendFrame(w, frame),\n }),\n );\n return link;\n};\n\nconst useStatewireWS = (\n options: StatewireClient.WSOptions,\n): Statewire<any, any> => useStatewireRemote(options, useWsLink);\n\nexport const StatewireWS = resource(useStatewireWS) as <\n TState = any,\n TCommands extends Statewire.Commands = Statewire.Commands,\n>(\n options: StatewireClient.WSOptions,\n) => ResourceElement<Statewire<TState | undefined, TCommands>>;\n"],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ declare namespace Statewire {
|
|
|
12
12
|
method: K;
|
|
13
13
|
params: Parameters<TCommands[K]>;
|
|
14
14
|
seq: number;
|
|
15
|
-
status: "queued" | "posted" | "
|
|
15
|
+
status: "queued" | "posted" | "applied" | "settled";
|
|
16
16
|
result?: unknown;
|
|
17
17
|
failure?: StatewireClient.SendFailure;
|
|
18
18
|
}; }[keyof TCommands];
|
|
@@ -35,23 +35,23 @@ declare namespace Statewire {
|
|
|
35
35
|
} | {
|
|
36
36
|
op: "remove";
|
|
37
37
|
path: StatewirePath;
|
|
38
|
-
} | {
|
|
39
|
-
op: "event";
|
|
40
|
-
value: object;
|
|
41
38
|
};
|
|
42
39
|
type Changes = {
|
|
43
40
|
isChangedAt(path: readonly (string | number)[]): boolean;
|
|
44
41
|
getChangedKeys(path: readonly (string | number)[]): string[];
|
|
45
42
|
};
|
|
46
43
|
}
|
|
47
|
-
interface Statewire<TState = any, TCommands extends Statewire.Commands = Statewire.Commands
|
|
44
|
+
interface Statewire<TState = any, TCommands extends Statewire.Commands = Statewire.Commands> {
|
|
48
45
|
state: TState;
|
|
49
46
|
commands: Statewire.CommandsProxy<TCommands>;
|
|
50
|
-
emit?: (event: TEvent) => void;
|
|
51
47
|
connection?: StatewireClient.Connection;
|
|
52
48
|
source?: Statewire.StateSource;
|
|
53
|
-
/**
|
|
54
|
-
|
|
49
|
+
/** Highest command seq released from the resend queue by the server's dur flag. */
|
|
50
|
+
durableSeq?: number;
|
|
51
|
+
/** Negation of the server's broadcast idle level; absent means always busy. */
|
|
52
|
+
isBusy?: boolean;
|
|
53
|
+
/** Replace this client's ambient context on the server; it rides the ctx lane (no verdict). */
|
|
54
|
+
setContext?: (value: Record<string, unknown>) => void;
|
|
55
55
|
}
|
|
56
56
|
declare class StatewireSendError extends Error {
|
|
57
57
|
readonly failure: StatewireClient.SendFailure;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","names":[],"sources":["../src/types.ts"],"mappings":";;;kBAGiB;OACH,WAAW,mBAAmB;OAE9B,QAAQ,kBAAkB,WAAW,eAC9C,WAAW;IAAc,QAAQ;IAAG,QAAQ,WAAW,UAAU;aAC5D;OAEI,cAAc,kBAAkB,eACzC,WAAW,gBACP,MAAM,WAAW,UAAU,QAC3B,QAAQ,QAAQ,WAAW,UAAU;OAGhC,cAAc,kBAAkB,WAAW,eACpD,WAAW;IACV,QAAQ;IACR,QAAQ,WAAW,UAAU;IAC7B;IACA;IACA;IACA,UAAU,gBAAgB;aAEtB;;OAGI;IACV;;IAEA,mBAAmB;;IAEnB,cAAc;;OAGJ;IACN;IAAe,MAAM;IAAe;;IACpC;IAAW,MAAM;IAAe;;IAChC;IAAc,MAAM;;
|
|
1
|
+
{"version":3,"file":"types.d.ts","names":[],"sources":["../src/types.ts"],"mappings":";;;kBAGiB;OACH,WAAW,mBAAmB;OAE9B,QAAQ,kBAAkB,WAAW,eAC9C,WAAW;IAAc,QAAQ;IAAG,QAAQ,WAAW,UAAU;aAC5D;OAEI,cAAc,kBAAkB,eACzC,WAAW,gBACP,MAAM,WAAW,UAAU,QAC3B,QAAQ,QAAQ,WAAW,UAAU;OAGhC,cAAc,kBAAkB,WAAW,eACpD,WAAW;IACV,QAAQ;IACR,QAAQ,WAAW,UAAU;IAC7B;IACA;IACA;IACA,UAAU,gBAAgB;aAEtB;;OAGI;IACV;;IAEA,mBAAmB;;IAEnB,cAAc;;OAGJ;IACN;IAAe,MAAM;IAAe;;IACpC;IAAW,MAAM;IAAe;;IAChC;IAAc,MAAM;;OAEd;IACV,YAAY;IACZ,eAAe;;;UAIF,UACf,cACA,kBAAkB,UAAU,WAAW,UAAU;EAEjD,OAAO;EACP,UAAU,UAAU,cAAc;EAClC,aAAa,gBAAgB;EAC7B,SAAS,UAAU;;EAEnB;;EAEA;;EAEA,cAAc,OAAO;;cAsBV,2BAA2B;WAC7B,SAAS,gBAAgB;EAEtB,YAAA,SAAS,gBAAgB"}
|
package/dist/types.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
//#region src/types.ts
|
|
2
2
|
const describe = (failure) => {
|
|
3
3
|
switch (failure.fate) {
|
|
4
|
-
case "rejected": return `statewire: command rejected: ${failure.message}`;
|
|
5
|
-
case "unknown-command": return "statewire: unknown command";
|
|
6
|
-
case "invalid-params": return `statewire: invalid params: ${failure.message}`;
|
|
4
|
+
case "rejected": return `statewire: command rejected${failure.code ? ` (${failure.code})` : ""}${failure.message ? `: ${failure.message}` : ""}`;
|
|
7
5
|
case "aborted": return `statewire: command aborted (${failure.cause})`;
|
|
8
6
|
case "lost": return `statewire: command lost (${failure.cause})`;
|
|
7
|
+
case "failed": return failure.message ? `statewire: command failed: ${failure.message}` : "statewire: command failed";
|
|
9
8
|
case "crashed": return "statewire: command crashed";
|
|
10
|
-
case "
|
|
9
|
+
case "expired": return `statewire: command result expired (${failure.cause})`;
|
|
11
10
|
}
|
|
12
11
|
};
|
|
13
12
|
var StatewireSendError = class extends Error {
|
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","names":["StatewireClient","StatewirePath","Statewire","Commands","Record","args","Command","TCommands","method","K","params","Parameters","CommandsProxy","Promise","Awaited","ReturnType","CommandUpdate","seq","status","result","failure","SendFailure","StateSource","snapshot","takeChanged","setOnChange","listener","Op","op","path","value","Changes","isChangedAt","getChangedKeys","state","TState","commands","
|
|
1
|
+
{"version":3,"file":"types.js","names":["StatewireClient","StatewirePath","Statewire","Commands","Record","args","Command","TCommands","method","K","params","Parameters","CommandsProxy","Promise","Awaited","ReturnType","CommandUpdate","seq","status","result","failure","SendFailure","StateSource","snapshot","takeChanged","setOnChange","listener","Op","op","path","value","Changes","isChangedAt","getChangedKeys","state","TState","commands","connection","Connection","source","durableSeq","isBusy","setContext","describe","fate","code","message","cause","StatewireSendError","Error","constructor","name"],"sources":["../src/types.ts"],"sourcesContent":["import type { StatewireClient } from \"./statewire\";\nimport type { StatewirePath } from \"./wire/ops\";\n\nexport namespace Statewire {\n export type Commands = Record<string, (...args: never[]) => unknown>;\n\n export type Command<TCommands extends Commands = Commands> = {\n [K in keyof TCommands]: { method: K; params: Parameters<TCommands[K]> };\n }[keyof TCommands];\n\n export type CommandsProxy<TCommands extends Commands> = {\n [K in keyof TCommands]: (\n ...args: Parameters<TCommands[K]>\n ) => Promise<Awaited<ReturnType<TCommands[K]>>>;\n };\n\n export type CommandUpdate<TCommands extends Commands = Commands> = {\n [K in keyof TCommands]: {\n method: K;\n params: Parameters<TCommands[K]>;\n seq: number;\n status: \"queued\" | \"posted\" | \"applied\" | \"settled\";\n result?: unknown;\n failure?: StatewireClient.SendFailure;\n };\n }[keyof TCommands];\n\n /** Push-based canonical state feed a statewire hands to the client core. */\n export type StateSource = {\n snapshot: () => unknown;\n /** Drain the paths changed since the last take (`[]` entry = whole state). */\n takeChanged: () => StatewirePath[];\n /** Synchronous, per change; the consumer owns coalescing. */\n setOnChange: (listener: (() => void) | null) => void;\n };\n\n export type Op =\n | { op: \"replace\"; path: StatewirePath; value: unknown }\n | { op: \"add\"; path: StatewirePath; value: unknown }\n | { op: \"remove\"; path: StatewirePath };\n\n export type Changes = {\n isChangedAt(path: readonly (string | number)[]): boolean;\n getChangedKeys(path: readonly (string | number)[]): string[];\n };\n}\n\nexport interface Statewire<\n TState = any,\n TCommands extends Statewire.Commands = Statewire.Commands,\n> {\n state: TState;\n commands: Statewire.CommandsProxy<TCommands>;\n connection?: StatewireClient.Connection;\n source?: Statewire.StateSource;\n /** Highest command seq released from the resend queue by the server's dur flag. */\n durableSeq?: number;\n /** Negation of the server's broadcast idle level; absent means always busy. */\n isBusy?: boolean;\n /** Replace this client's ambient context on the server; it rides the ctx lane (no verdict). */\n setContext?: (value: Record<string, unknown>) => void;\n}\n\nconst describe = (failure: StatewireClient.SendFailure): string => {\n switch (failure.fate) {\n case \"rejected\":\n return `statewire: command rejected${failure.code ? ` (${failure.code})` : \"\"}${failure.message ? `: ${failure.message}` : \"\"}`;\n case \"aborted\":\n return `statewire: command aborted (${failure.cause})`;\n case \"lost\":\n return `statewire: command lost (${failure.cause})`;\n case \"failed\":\n return failure.message\n ? `statewire: command failed: ${failure.message}`\n : \"statewire: command failed\";\n case \"crashed\":\n return \"statewire: command crashed\";\n case \"expired\":\n return `statewire: command result expired (${failure.cause})`;\n }\n};\n\nexport class StatewireSendError extends Error {\n readonly failure: StatewireClient.SendFailure;\n\n constructor(failure: StatewireClient.SendFailure) {\n super(describe(failure));\n this.name = \"StatewireSendError\";\n this.failure = failure;\n }\n}\n"],"mappings":";AA+DA,MAAM2C,YAAYvB,YAAiD;CACjE,QAAQA,QAAQwB,MAAhB;EACE,KAAK,YACH,OAAO,8BAA8BxB,QAAQyB,OAAO,KAAKzB,QAAQyB,KAAI,KAAM,KAAKzB,QAAQ0B,UAAU,KAAK1B,QAAQ0B,YAAY;EAC7H,KAAK,WACH,OAAO,+BAA+B1B,QAAQ2B,MAAK;EACrD,KAAK,QACH,OAAO,4BAA4B3B,QAAQ2B,MAAK;EAClD,KAAK,UACH,OAAO3B,QAAQ0B,UACX,8BAA8B1B,QAAQ0B,YACtC;EACN,KAAK,WACH,OAAO;EACT,KAAK,WACH,OAAO,sCAAsC1B,QAAQ2B,MAAK;CAC9D;AACF;AAEA,IAAaC,qBAAb,cAAwCC,MAAM;CAC5C;CAEAC,YAAY9B,SAAsC;EAChD,MAAMuB,SAASvB,OAAO,CAAC;EACvB,KAAK+B,OAAO;EACZ,KAAK/B,UAAUA;CACjB;AACF"}
|
package/dist/wire/ops.d.ts
CHANGED
|
@@ -2,16 +2,12 @@ import { Statewire } from "../types.js";
|
|
|
2
2
|
//#region src/wire/ops.d.ts
|
|
3
3
|
type StatewirePath = readonly (string | number)[];
|
|
4
4
|
declare const sanitizeValue: (value: unknown, parent?: string) => unknown;
|
|
5
|
-
|
|
6
|
-
op: "replace" | "add" | "remove";
|
|
7
|
-
}>;
|
|
8
|
-
declare const applyOp: (root: unknown, op: MutableOp) => {
|
|
5
|
+
declare const applyOp: (root: unknown, op: Statewire.Op) => {
|
|
9
6
|
state: unknown;
|
|
10
7
|
changedPaths: StatewirePath[];
|
|
11
8
|
};
|
|
12
9
|
declare const applyOps: (state: unknown, ops: readonly unknown[]) => {
|
|
13
10
|
state: unknown;
|
|
14
|
-
events: unknown[];
|
|
15
11
|
changedPaths: StatewirePath[];
|
|
16
12
|
};
|
|
17
13
|
//#endregion
|
package/dist/wire/ops.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ops.d.ts","names":[],"sources":["../../src/wire/ops.ts"],"mappings":";;KAIY;cAwDC,gBAAiB,gBAAgB;
|
|
1
|
+
{"version":3,"file":"ops.d.ts","names":[],"sources":["../../src/wire/ops.ts"],"mappings":";;KAIY;cAwDC,gBAAiB,gBAAgB;cAmGjC,UACX,eACA,IAAI,UAAU;EACX;EAAgB,cAAc;;cAUtB,WACX,gBACA;EAEA;EACA,cAAc"}
|
package/dist/wire/ops.js
CHANGED
|
@@ -92,17 +92,14 @@ const applyOp = (root, op) => {
|
|
|
92
92
|
};
|
|
93
93
|
};
|
|
94
94
|
const applyOps = (state, ops) => {
|
|
95
|
-
const events = [];
|
|
96
95
|
const mutable = [];
|
|
97
|
-
for (const op of ops) if (op.op === "
|
|
98
|
-
else if (op.op === "replace" || op.op === "add" || op.op === "remove") mutable.push(op);
|
|
96
|
+
for (const op of ops) if (op.op === "replace" || op.op === "add" || op.op === "remove") mutable.push(op);
|
|
99
97
|
else warnUnknownOp(op.op);
|
|
100
98
|
const recorded = recordMutations({ state }, (draft) => {
|
|
101
99
|
for (const op of mutable) applyOpToDraft(draft, op);
|
|
102
100
|
});
|
|
103
101
|
return {
|
|
104
102
|
state: recorded.state.state,
|
|
105
|
-
events,
|
|
106
103
|
changedPaths: unboxPaths(recorded.changedPaths)
|
|
107
104
|
};
|
|
108
105
|
};
|
package/dist/wire/ops.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ops.js","names":["recordMutations","isUnsafeKey","walkStatePath","Statewire","StatewirePath","warnStripped","key","parent","process","env","console","warn","undefined","unserializable","kind","path","Error","length","join","sanitizeAt","value","Date","getTime","Array","isArray","map","item","index","out","Record","Object","keys","sanitizeValue","MutableOp","Extract","Op","op","invalid","insertedText","current","offset","slice","applyAdd","box","state","walked","Number","isInteger","target","leaf","splice","next","applyInList","list","applyInObject","obj","hasOwn","applyOpToDraft","warnedOps","Set","warnUnknownOp","type","has","add","JSON","stringify","unboxPaths","paths","applyOp","root","changedPaths","recorded","draft","applyOps","ops","events","mutable","push"],"sources":["../../src/wire/ops.ts"],"sourcesContent":["import { recordMutations } from \"../util/recordMutations\";\nimport { isUnsafeKey, walkStatePath } from \"../util/statePath\";\nimport type { Statewire } from \"../types\";\n\nexport type StatewirePath = readonly (string | number)[];\n\nconst warnStripped = (key: string, parent: string | undefined) => {\n if (\n typeof process === \"undefined\" ||\n process.env[\"NODE_ENV\"] !== \"production\"\n ) {\n console.warn(\n `statewire: stripped unsafe key \"${key}\"` +\n (parent === undefined ? \"\" : ` under \"${parent}\"`),\n );\n }\n};\n\nconst unserializable = (\n kind: \"function\" | \"undefined\",\n path: readonly (string | number)[],\n): never => {\n throw new Error(\n `statewire: cannot serialize ${kind}` +\n (path.length === 0 ? \"\" : ` at ${path.join(\".\")}`),\n );\n};\n\nconst sanitizeAt = (\n value: unknown,\n parent: string | undefined,\n path: readonly (string | number)[],\n): unknown => {\n if (value instanceof Date) return new Date(value.getTime());\n if (Array.isArray(value)) {\n return value.map((item, index) => {\n if (item === undefined) unserializable(\"undefined\", [...path, index]);\n return sanitizeAt(item, undefined, [...path, index]);\n });\n }\n if (typeof value === \"object\" && value !== null) {\n const out: Record<string, unknown> = {};\n for (const key of Object.keys(value)) {\n if (isUnsafeKey(key, parent)) {\n warnStripped(key, parent);\n continue;\n }\n const item = (value as Record<string, unknown>)[key];\n if (item === undefined) continue;\n out[key] = sanitizeAt(item, key, [...path, key]);\n }\n return out;\n }\n if (typeof value === \"function\") unserializable(\"function\", path);\n return value;\n};\n\n// One clone pass: strips unsafe keys (with a warning); explicit-undefined\n// object entries are omitted (delete semantics), functions and undefined\n// list items throw.\nexport const sanitizeValue = (value: unknown, parent?: string): unknown =>\n sanitizeAt(value, parent, parent === undefined ? [] : [parent]);\n\ntype MutableOp = Extract<Statewire.Op, { op: \"replace\" | \"add\" | \"remove\" }>;\n\nconst invalid = (op: MutableOp) =>\n new Error(`Statewire cannot apply ${op.op} at path: ${op.path.join(\".\")}`);\n\nconst insertedText = (\n current: string,\n op: Extract<MutableOp, { op: \"add\" }>,\n offset: number,\n): string => {\n if (typeof op.value !== \"string\" || offset > current.length) {\n throw invalid(op);\n }\n return current.slice(0, offset) + op.value + current.slice(offset);\n};\n\nconst applyAdd = (\n box: { state: unknown },\n op: Extract<MutableOp, { op: \"add\" }>,\n) => {\n const offset = op.path[op.path.length - 1];\n const walked = walkStatePath(box.state, op.path.slice(0, -1), \"resolve\");\n if (\n walked === undefined ||\n typeof offset !== \"number\" ||\n !Number.isInteger(offset) ||\n offset < 0\n ) {\n throw invalid(op);\n }\n const target = walked.leaf;\n if (Array.isArray(target)) {\n if (offset > target.length) throw invalid(op);\n target.splice(offset, 0, sanitizeValue(op.value));\n } else if (typeof target === \"string\") {\n const next = insertedText(target, op, offset);\n if (op.path.length === 1) {\n box.state = next;\n } else {\n (walked.parent as Record<string | number, unknown>)[walked.key] = next;\n }\n } else {\n throw invalid(op);\n }\n};\n\nconst applyInList = (list: unknown[], index: number, op: MutableOp) => {\n if (index >= list.length) throw invalid(op);\n if (op.op === \"replace\") {\n list[index] = sanitizeValue(op.value);\n } else {\n list.splice(index, 1);\n }\n};\n\nconst applyInObject = (\n obj: Record<string, unknown>,\n key: string,\n op: MutableOp,\n) => {\n if (op.op === \"replace\") {\n obj[key] = sanitizeValue(op.value, key);\n } else {\n if (!Object.hasOwn(obj, key)) throw invalid(op);\n delete obj[key];\n }\n};\n\nconst applyOpToDraft = (box: { state: unknown }, op: MutableOp): void => {\n if (op.op === \"add\") {\n if (op.path.length === 0) throw invalid(op);\n applyAdd(box, op);\n return;\n }\n if (op.path.length === 0) {\n if (op.op !== \"replace\") throw invalid(op);\n box.state = sanitizeValue(op.value);\n return;\n }\n const walked = walkStatePath(box.state, op.path, \"parent\");\n if (walked === undefined) throw invalid(op);\n if (typeof walked.key === \"number\") {\n applyInList(walked.parent as unknown[], walked.key, op);\n } else {\n applyInObject(walked.parent as Record<string, unknown>, walked.key, op);\n }\n};\n\nconst warnedOps = new Set<unknown>();\nconst warnUnknownOp = (type: unknown) => {\n if (warnedOps.has(type)) return;\n warnedOps.add(type);\n console.warn(`statewire: ignoring unknown op type ${JSON.stringify(type)}`);\n};\n\nconst unboxPaths = (paths: readonly StatewirePath[]): StatewirePath[] =>\n paths.map((path) => path.slice(1));\n\nexport const applyOp = (\n root: unknown,\n op: MutableOp,\n): { state: unknown; changedPaths: StatewirePath[] } => {\n const recorded = recordMutations({ state: root }, (draft) =>\n applyOpToDraft(draft, op),\n );\n return {\n state: recorded.state.state,\n changedPaths: unboxPaths(recorded.changedPaths),\n };\n};\n\nexport const applyOps = (\n state: unknown,\n ops: readonly unknown[],\n): {\n state: unknown;\n events: unknown[];\n changedPaths: StatewirePath[];\n} => {\n const events: unknown[] = [];\n const mutable: MutableOp[] = [];\n for (const op of ops as Statewire.Op[]) {\n if (op.op === \"event\") {\n events.push(sanitizeValue(op.value));\n } else if (op.op === \"replace\" || op.op === \"add\" || op.op === \"remove\") {\n mutable.push(op);\n } else {\n warnUnknownOp((op as { op: unknown }).op);\n }\n }\n const recorded = recordMutations({ state }, (draft) => {\n for (const op of mutable) applyOpToDraft(draft, op);\n });\n return {\n state: recorded.state.state,\n events,\n changedPaths: unboxPaths(recorded.changedPaths),\n };\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ops.js","names":["recordMutations","isUnsafeKey","walkStatePath","Statewire","StatewirePath","warnStripped","key","parent","process","env","console","warn","undefined","unserializable","kind","path","Error","length","join","sanitizeAt","value","Date","getTime","Array","isArray","map","item","index","out","Record","Object","keys","sanitizeValue","invalid","op","Op","insertedText","current","Extract","offset","slice","applyAdd","box","state","walked","Number","isInteger","target","leaf","splice","next","applyInList","list","applyInObject","obj","hasOwn","applyOpToDraft","warnedOps","Set","warnUnknownOp","type","has","add","JSON","stringify","unboxPaths","paths","applyOp","root","changedPaths","recorded","draft","applyOps","ops","mutable","push"],"sources":["../../src/wire/ops.ts"],"sourcesContent":["import { recordMutations } from \"../util/recordMutations\";\nimport { isUnsafeKey, walkStatePath } from \"../util/statePath\";\nimport type { Statewire } from \"../types\";\n\nexport type StatewirePath = readonly (string | number)[];\n\nconst warnStripped = (key: string, parent: string | undefined) => {\n if (\n typeof process === \"undefined\" ||\n process.env[\"NODE_ENV\"] !== \"production\"\n ) {\n console.warn(\n `statewire: stripped unsafe key \"${key}\"` +\n (parent === undefined ? \"\" : ` under \"${parent}\"`),\n );\n }\n};\n\nconst unserializable = (\n kind: \"function\" | \"undefined\",\n path: readonly (string | number)[],\n): never => {\n throw new Error(\n `statewire: cannot serialize ${kind}` +\n (path.length === 0 ? \"\" : ` at ${path.join(\".\")}`),\n );\n};\n\nconst sanitizeAt = (\n value: unknown,\n parent: string | undefined,\n path: readonly (string | number)[],\n): unknown => {\n if (value instanceof Date) return new Date(value.getTime());\n if (Array.isArray(value)) {\n return value.map((item, index) => {\n if (item === undefined) unserializable(\"undefined\", [...path, index]);\n return sanitizeAt(item, undefined, [...path, index]);\n });\n }\n if (typeof value === \"object\" && value !== null) {\n const out: Record<string, unknown> = {};\n for (const key of Object.keys(value)) {\n if (isUnsafeKey(key, parent)) {\n warnStripped(key, parent);\n continue;\n }\n const item = (value as Record<string, unknown>)[key];\n if (item === undefined) continue;\n out[key] = sanitizeAt(item, key, [...path, key]);\n }\n return out;\n }\n if (typeof value === \"function\") unserializable(\"function\", path);\n return value;\n};\n\n// One clone pass: strips unsafe keys (with a warning); explicit-undefined\n// object entries are omitted (delete semantics), functions and undefined\n// list items throw.\nexport const sanitizeValue = (value: unknown, parent?: string): unknown =>\n sanitizeAt(value, parent, parent === undefined ? [] : [parent]);\n\nconst invalid = (op: Statewire.Op) =>\n new Error(`Statewire cannot apply ${op.op} at path: ${op.path.join(\".\")}`);\n\nconst insertedText = (\n current: string,\n op: Extract<Statewire.Op, { op: \"add\" }>,\n offset: number,\n): string => {\n if (typeof op.value !== \"string\" || offset > current.length) {\n throw invalid(op);\n }\n return current.slice(0, offset) + op.value + current.slice(offset);\n};\n\nconst applyAdd = (\n box: { state: unknown },\n op: Extract<Statewire.Op, { op: \"add\" }>,\n) => {\n const offset = op.path[op.path.length - 1];\n const walked = walkStatePath(box.state, op.path.slice(0, -1), \"resolve\");\n if (\n walked === undefined ||\n typeof offset !== \"number\" ||\n !Number.isInteger(offset) ||\n offset < 0\n ) {\n throw invalid(op);\n }\n const target = walked.leaf;\n if (Array.isArray(target)) {\n if (offset > target.length) throw invalid(op);\n target.splice(offset, 0, sanitizeValue(op.value));\n } else if (typeof target === \"string\") {\n const next = insertedText(target, op, offset);\n if (op.path.length === 1) {\n box.state = next;\n } else {\n (walked.parent as Record<string | number, unknown>)[walked.key] = next;\n }\n } else {\n throw invalid(op);\n }\n};\n\nconst applyInList = (list: unknown[], index: number, op: Statewire.Op) => {\n if (index >= list.length) throw invalid(op);\n if (op.op === \"replace\") {\n list[index] = sanitizeValue(op.value);\n } else {\n list.splice(index, 1);\n }\n};\n\nconst applyInObject = (\n obj: Record<string, unknown>,\n key: string,\n op: Statewire.Op,\n) => {\n if (op.op === \"replace\") {\n obj[key] = sanitizeValue(op.value, key);\n } else {\n if (!Object.hasOwn(obj, key)) throw invalid(op);\n delete obj[key];\n }\n};\n\nconst applyOpToDraft = (box: { state: unknown }, op: Statewire.Op): void => {\n if (op.op === \"add\") {\n if (op.path.length === 0) throw invalid(op);\n applyAdd(box, op);\n return;\n }\n if (op.path.length === 0) {\n if (op.op !== \"replace\") throw invalid(op);\n box.state = sanitizeValue(op.value);\n return;\n }\n const walked = walkStatePath(box.state, op.path, \"parent\");\n if (walked === undefined) throw invalid(op);\n if (typeof walked.key === \"number\") {\n applyInList(walked.parent as unknown[], walked.key, op);\n } else {\n applyInObject(walked.parent as Record<string, unknown>, walked.key, op);\n }\n};\n\nconst warnedOps = new Set<unknown>();\nconst warnUnknownOp = (type: unknown) => {\n if (warnedOps.has(type)) return;\n warnedOps.add(type);\n console.warn(`statewire: ignoring unknown op type ${JSON.stringify(type)}`);\n};\n\nconst unboxPaths = (paths: readonly StatewirePath[]): StatewirePath[] =>\n paths.map((path) => path.slice(1));\n\nexport const applyOp = (\n root: unknown,\n op: Statewire.Op,\n): { state: unknown; changedPaths: StatewirePath[] } => {\n const recorded = recordMutations({ state: root }, (draft) =>\n applyOpToDraft(draft, op),\n );\n return {\n state: recorded.state.state,\n changedPaths: unboxPaths(recorded.changedPaths),\n };\n};\n\nexport const applyOps = (\n state: unknown,\n ops: readonly unknown[],\n): {\n state: unknown;\n changedPaths: StatewirePath[];\n} => {\n const mutable: Statewire.Op[] = [];\n for (const op of ops as Statewire.Op[]) {\n if (op.op === \"replace\" || op.op === \"add\" || op.op === \"remove\") {\n mutable.push(op);\n } else {\n warnUnknownOp((op as { op: unknown }).op);\n }\n }\n const recorded = recordMutations({ state }, (draft) => {\n for (const op of mutable) applyOpToDraft(draft, op);\n });\n return {\n state: recorded.state.state,\n changedPaths: unboxPaths(recorded.changedPaths),\n };\n};\n"],"mappings":"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"}
|