statewire 0.0.5 → 0.5.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 +1 -1
- package/dist/client/connection.d.ts +1 -1
- package/dist/client/connection.js.map +1 -1
- package/dist/client/core.d.ts.map +1 -1
- package/dist/client/core.js +1 -0
- package/dist/client/core.js.map +1 -1
- package/dist/client/session.d.ts +1 -5
- package/dist/client/session.d.ts.map +1 -1
- package/dist/client/session.js +18 -29
- package/dist/client/session.js.map +1 -1
- package/dist/host/host.d.ts +2 -14
- package/dist/host/host.d.ts.map +1 -1
- package/dist/host/host.js +41 -92
- package/dist/host/host.js.map +1 -1
- package/dist/host.d.ts +11 -17
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +2 -2
- package/dist/statewire.d.ts +8 -14
- package/dist/statewire.d.ts.map +1 -1
- package/dist/statewire.js.map +1 -1
- package/dist/transports/remote.js +1 -4
- package/dist/transports/remote.js.map +1 -1
- package/dist/transports/sse.d.ts +4 -1
- package/dist/transports/sse.d.ts.map +1 -1
- package/dist/transports/sse.js +41 -25
- package/dist/transports/sse.js.map +1 -1
- package/dist/transports/ws.d.ts +2 -1
- package/dist/transports/ws.d.ts.map +1 -1
- package/dist/transports/ws.js +25 -6
- package/dist/transports/ws.js.map +1 -1
- package/dist/util/statePath.js +8 -6
- package/dist/util/statePath.js.map +1 -1
- package/package.json +1 -1
- package/src/client/connection.ts +1 -1
- package/src/client/core.ts +1 -0
- package/src/client/session.ts +9 -26
- package/src/host/host.ts +55 -113
- package/src/host.ts +9 -19
- package/src/statewire.ts +8 -14
- package/src/transports/remote.ts +1 -7
- package/src/transports/sse.ts +71 -27
- package/src/transports/ws.ts +35 -4
package/dist/transports/sse.js
CHANGED
|
@@ -20,9 +20,9 @@ const encodeWireCommand = (command) => ({
|
|
|
20
20
|
...command.context !== void 0 && { context: contextOps(command.context) }
|
|
21
21
|
});
|
|
22
22
|
const baseUrl = (options) => options.url.replace(/\/$/, "");
|
|
23
|
-
const requestHeaders = async (options, extra) => {
|
|
23
|
+
const requestHeaders = async (options, extra, ctx) => {
|
|
24
24
|
const { headers } = options;
|
|
25
|
-
const merged = new Headers(typeof headers === "function" ? await headers() : headers);
|
|
25
|
+
const merged = new Headers(typeof headers === "function" ? await headers(ctx) : headers);
|
|
26
26
|
for (const [name, value] of Object.entries(extra)) merged.set(name, value);
|
|
27
27
|
return merged;
|
|
28
28
|
};
|
|
@@ -55,11 +55,13 @@ const consume = async (c, reader, heartbeat, dead) => {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
};
|
|
58
|
-
const attemptStream = async (c, options, abort) => {
|
|
58
|
+
const attemptStream = async (c, options, auth, abort) => {
|
|
59
|
+
const stale = auth.stale;
|
|
60
|
+
auth.stale = false;
|
|
59
61
|
const headers = await requestHeaders(options, {
|
|
60
62
|
"Statewire-Client-Id": c.ctx.clientId,
|
|
61
63
|
accept: "text/event-stream"
|
|
62
|
-
});
|
|
64
|
+
}, { staleAuth: stale });
|
|
63
65
|
const heartbeat = new AbortController();
|
|
64
66
|
const headerTimer = setTimeout(() => heartbeat.abort(), timing.heartbeatTimeoutMs);
|
|
65
67
|
let res;
|
|
@@ -73,6 +75,11 @@ const attemptStream = async (c, options, abort) => {
|
|
|
73
75
|
clearTimeout(headerTimer);
|
|
74
76
|
}
|
|
75
77
|
if (res.status === 429 || res.status === 503) throw overloaded(res);
|
|
78
|
+
if (res.status === 401) {
|
|
79
|
+
if (stale) c.episode = { cause: "unauthorized" };
|
|
80
|
+
auth.stale = true;
|
|
81
|
+
throw new Error("unauthorized");
|
|
82
|
+
}
|
|
76
83
|
if (!res.ok || !res.body) throw new Error(`HTTP ${res.status}`);
|
|
77
84
|
const reader = res.body.getReader();
|
|
78
85
|
try {
|
|
@@ -82,19 +89,21 @@ const attemptStream = async (c, options, abort) => {
|
|
|
82
89
|
}
|
|
83
90
|
};
|
|
84
91
|
const overloaded = (res) => overloadedError(parseRetryAfter(res.headers.get("Retry-After")));
|
|
85
|
-
const
|
|
92
|
+
const postCommand = async (c, options, { seq, command }, timeout, stale) => timedFetch(options, `${baseUrl(options)}/commands`, {
|
|
93
|
+
method: "POST",
|
|
94
|
+
headers: await requestHeaders(options, {
|
|
95
|
+
"content-type": "application/json",
|
|
96
|
+
"Statewire-Client-Id": c.ctx.clientId,
|
|
97
|
+
"Statewire-Command-Seq": String(seq),
|
|
98
|
+
...c.lease !== null && { "Statewire-Lease": c.lease }
|
|
99
|
+
}, { staleAuth: stale }),
|
|
100
|
+
body: JSON.stringify(encodeWireCommand(command)),
|
|
101
|
+
signal: AbortSignal.any([c.lifetime?.signal ?? AbortSignal.abort(), timeout])
|
|
102
|
+
}, timeout);
|
|
103
|
+
const sendCommand = async (c, options, auth, args) => {
|
|
86
104
|
const timeout = AbortSignal.timeout(timing.heartbeatTimeoutMs);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
headers: await requestHeaders(options, {
|
|
90
|
-
"content-type": "application/json",
|
|
91
|
-
"Statewire-Client-Id": c.ctx.clientId,
|
|
92
|
-
"Statewire-Command-Seq": String(seq),
|
|
93
|
-
...c.lease !== null && { "Statewire-Lease": c.lease }
|
|
94
|
-
}),
|
|
95
|
-
body: JSON.stringify(encodeWireCommand(command)),
|
|
96
|
-
signal: AbortSignal.any([c.lifetime?.signal ?? AbortSignal.abort(), timeout])
|
|
97
|
-
}, timeout);
|
|
105
|
+
let res = await postCommand(c, options, args, timeout, false);
|
|
106
|
+
if (res.status === 401 && typeof options.headers === "function") res = await postCommand(c, options, args, timeout, true);
|
|
98
107
|
if (res.ok) {
|
|
99
108
|
let body;
|
|
100
109
|
try {
|
|
@@ -110,32 +119,39 @@ const sendCommand = async (c, options, { seq, command }) => {
|
|
|
110
119
|
response: reviveResponse(body.res)
|
|
111
120
|
} : { type: "receipt" };
|
|
112
121
|
}
|
|
113
|
-
if (res.status === 409 || res.status === 412 || res.status === 423) {
|
|
114
|
-
const verdict = res.status === 409 ? "seq-gap" : res.status === 412 ? "unknown-client" : "stale-lease";
|
|
115
|
-
if ((await res.json().catch(() => null))?.error === verdict) return { type: verdict };
|
|
116
|
-
throw new Error(`HTTP ${res.status}`);
|
|
117
|
-
}
|
|
118
122
|
if (res.status === 429 || res.status === 503) throw overloaded(res);
|
|
119
|
-
if (res.status
|
|
123
|
+
if (res.status === 401) {
|
|
124
|
+
auth.stale = true;
|
|
125
|
+
c.episode = { cause: "unauthorized" };
|
|
126
|
+
c.connection.reconnect();
|
|
127
|
+
throw new Error("unauthorized");
|
|
128
|
+
}
|
|
129
|
+
const body = await res.json().catch(() => null);
|
|
130
|
+
if (body?.error === "rejected") return {
|
|
120
131
|
type: "rejected",
|
|
121
|
-
message: `HTTP ${res.status}`
|
|
132
|
+
message: body.rejections?.[0]?.message ?? `HTTP ${res.status}`
|
|
122
133
|
};
|
|
134
|
+
if (res.status === 409 || res.status === 412 || res.status === 423) {
|
|
135
|
+
const verdict = res.status === 409 ? "seq-gap" : res.status === 412 ? "unknown-client" : "stale-lease";
|
|
136
|
+
if (body?.error === verdict) return { type: verdict };
|
|
137
|
+
}
|
|
123
138
|
throw new Error(`HTTP ${res.status}`);
|
|
124
139
|
};
|
|
125
140
|
const useSseLink = (options, ctx) => {
|
|
126
141
|
const latest = useRef(options);
|
|
127
142
|
latest.current = options;
|
|
143
|
+
const [auth] = useState(() => ({ stale: false }));
|
|
128
144
|
const c_0 = useStatewireConn({
|
|
129
145
|
url: options.url,
|
|
130
146
|
onChange: options.onChange,
|
|
131
147
|
ctx,
|
|
132
|
-
attemptOnce: (c, abort) => attemptStream(c, options, abort)
|
|
148
|
+
attemptOnce: (c, abort) => attemptStream(c, options, auth, abort)
|
|
133
149
|
});
|
|
134
150
|
const [link] = useState(() => ({
|
|
135
151
|
get connection() {
|
|
136
152
|
return c_0.connection;
|
|
137
153
|
},
|
|
138
|
-
send: (args) => sendCommand(c_0, latest.current, args)
|
|
154
|
+
send: (args) => sendCommand(c_0, latest.current, auth, args)
|
|
139
155
|
}));
|
|
140
156
|
return link;
|
|
141
157
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sse.js","names":["useRef","useState","flushTapSync","resource","ResourceElement","encodeParams","reviveResponse","createSseParser","timing","classify","handleEnvelope","overloadedError","parseRetryAfter","useStatewireConn","Conn","StatewireEnvelope","StatewireResponse","Statewire","StatewireClient","StatewireHost","useStatewireRemote","StatewireRemoteProps","clientId","onSnapshot","args","ops","lastSeq","onOps","res","ack","onFinish","Fin","StatewireWireCommand","method","params","context","Record","contextOps","value","op","path","encodeWireCommand","command","undefined","StatewireSubmitResult","type","response","message","ConnectionState","RemoteLink","connection","Connection","send","seq","Promise","baseUrl","options","SSEOptions","url","replace","requestHeaders","extra","headers","merged","Headers","name","Object","entries","set","timedFetch","input","init","RequestInit","signal","AbortSignal","timeout","Response","fetch","globalThis","consume","c","reader","ReadableStreamDefaultReader","Uint8Array","heartbeat","AbortController","dead","decoder","TextDecoder","parse","timer","setTimeout","abort","heartbeatTimeoutMs","result","ReadableStreamReadResult","read","clearTimeout","done","data","decode","stream","env","JSON","attemptStream","ctx","accept","headerTimer","cache","any","status","overloaded","ok","body","Error","getReader","aborted","cancel","catch","get","sendCommand","String","lease","stringify","lifetime","json","verdict","const","error","useSseLink","latest","current","onChange","attemptOnce","link","useStatewireSSE","StatewireSSE","Commands","TState","TCommands","TEvent"],"sources":["../../src/transports/sse.ts"],"sourcesContent":["import { useRef, useState } from \"react\";\nimport { flushTapSync, resource } from \"@assistant-ui/tap\";\nimport type { ResourceElement } from \"@assistant-ui/tap\";\nimport { encodeParams, reviveResponse } from \"../wire/meta\";\nimport { createSseParser } from \"./sse-parser\";\nimport { timing } from \"../client/timing\";\nimport {\n classify,\n handleEnvelope,\n overloadedError,\n parseRetryAfter,\n useStatewireConn,\n} from \"../client/connection\";\nimport type {\n Conn,\n StatewireEnvelope,\n StatewireResponse,\n} from \"../client/connection\";\nimport type { Statewire } from \"../types\";\nimport type { StatewireClient } from \"../statewire\";\nimport type { StatewireHost } from \"../host\";\nimport { useStatewireRemote } from \"./remote\";\n\nexport type StatewireRemoteProps = {\n clientId: string;\n onSnapshot: (args: { ops: unknown[]; lastSeq: number }) => void;\n onOps: (args: {\n ops: unknown[];\n res?: StatewireResponse[];\n ack?: number;\n }) => void;\n onFinish: (args: StatewireHost.Fin) => void;\n};\n\nexport type StatewireWireCommand =\n | { method: string; params: readonly unknown[]; context?: never }\n | { method?: never; params?: never; context: Record<string, unknown> };\n\n/** The wire shape of a client context submission: one root replace op. */\nexport const contextOps = (value: Record<string, unknown>) => [\n { op: \"replace\", path: [], value },\n];\n\nexport const encodeWireCommand = (command: StatewireWireCommand) => ({\n ...(command.method !== undefined && {\n method: command.method,\n ...encodeParams(command.params),\n }),\n ...(command.context !== undefined && {\n context: contextOps(command.context),\n }),\n});\n\nexport type StatewireSubmitResult =\n | { type: \"receipt\"; response?: StatewireResponse }\n | { type: \"rejected\"; message: string }\n | { type: \"seq-gap\" }\n | { type: \"unknown-client\" }\n | { type: \"stale-lease\" };\n\nexport type ConnectionState = StatewireClient.ConnectionState;\n\n/** The live wire a remote session drives: connection status plus command POST. */\nexport type RemoteLink = {\n connection: StatewireClient.Connection;\n send: (args: {\n seq: number;\n command: StatewireWireCommand;\n }) => Promise<StatewireSubmitResult>;\n};\n\nconst baseUrl = (options: StatewireClient.SSEOptions) =>\n options.url.replace(/\\/$/, \"\");\n\nconst requestHeaders = async (\n options: StatewireClient.SSEOptions,\n extra: Record<string, string>,\n) => {\n const { headers } = options;\n const merged = new Headers(\n typeof headers === \"function\" ? await headers() : headers,\n );\n for (const [name, value] of Object.entries(extra)) merged.set(name, value);\n return merged;\n};\n\nconst timedFetch = async (\n options: StatewireClient.SSEOptions,\n input: string,\n init: RequestInit & { signal: AbortSignal },\n timeout: AbortSignal,\n): Promise<Response> => {\n try {\n return await (options.fetch ?? globalThis.fetch)(input, init);\n } catch {\n throw classify(timeout);\n }\n};\n\nconst consume = async (\n c: Conn,\n reader: ReadableStreamDefaultReader<Uint8Array>,\n heartbeat: AbortController,\n dead: () => boolean,\n): Promise<\"stop\" | \"reattach\" | \"ended\"> => {\n const decoder = new TextDecoder();\n const parse = createSseParser();\n for (;;) {\n const timer = setTimeout(\n () => heartbeat.abort(),\n timing.heartbeatTimeoutMs,\n );\n let result: ReadableStreamReadResult<Uint8Array>;\n try {\n result = await reader.read();\n } catch {\n throw classify(heartbeat.signal);\n } finally {\n clearTimeout(timer);\n }\n if (result.done) return \"ended\";\n for (const data of parse(decoder.decode(result.value, { stream: true }))) {\n if (dead()) return \"ended\";\n const env = JSON.parse(data) as StatewireEnvelope;\n const done = flushTapSync(() => handleEnvelope(c, env));\n if (done) return done;\n }\n }\n};\n\nconst attemptStream = async (\n c: Conn,\n options: StatewireClient.SSEOptions,\n abort: AbortController,\n): Promise<\"stop\" | \"reattach\" | \"ended\"> => {\n const headers = await requestHeaders(options, {\n \"Statewire-Client-Id\": c.ctx.clientId,\n accept: \"text/event-stream\",\n });\n const heartbeat = new AbortController();\n const headerTimer = setTimeout(\n () => heartbeat.abort(),\n timing.heartbeatTimeoutMs,\n );\n let res: Response;\n try {\n res = await timedFetch(\n options,\n `${baseUrl(options)}/stream`,\n {\n headers,\n cache: \"no-store\",\n signal: AbortSignal.any([abort.signal, heartbeat.signal]),\n },\n heartbeat.signal,\n );\n } finally {\n clearTimeout(headerTimer);\n }\n if (res.status === 429 || res.status === 503) throw overloaded(res);\n if (!res.ok || !res.body) throw new Error(`HTTP ${res.status}`);\n const reader = res.body.getReader();\n try {\n return await consume(c, reader, heartbeat, () => abort.signal.aborted);\n } finally {\n reader.cancel().catch(() => {});\n }\n};\n\nconst overloaded = (res: Response) =>\n overloadedError(parseRetryAfter(res.headers.get(\"Retry-After\")));\n\nconst sendCommand = async (\n c: Conn,\n options: StatewireClient.SSEOptions,\n { seq, command }: { seq: number; command: StatewireWireCommand },\n): Promise<StatewireSubmitResult> => {\n const timeout = AbortSignal.timeout(timing.heartbeatTimeoutMs);\n const res = await timedFetch(\n options,\n `${baseUrl(options)}/commands`,\n {\n method: \"POST\",\n headers: await requestHeaders(options, {\n \"content-type\": \"application/json\",\n \"Statewire-Client-Id\": c.ctx.clientId,\n \"Statewire-Command-Seq\": String(seq),\n ...(c.lease !== null && { \"Statewire-Lease\": c.lease }),\n }),\n body: JSON.stringify(encodeWireCommand(command)),\n signal: AbortSignal.any([\n c.lifetime?.signal ?? AbortSignal.abort(),\n timeout,\n ]),\n },\n timeout,\n );\n if (res.ok) {\n let body: { res?: StatewireResponse } | null;\n try {\n body = (await res.json()) as { res?: StatewireResponse } | null;\n } catch {\n return {\n type: \"rejected\",\n message: \"statewire: protocol error: malformed /commands receipt body\",\n };\n }\n return body?.res !== undefined\n ? { type: \"receipt\", response: reviveResponse(body.res) }\n : { type: \"receipt\" };\n }\n if (res.status === 409 || res.status === 412 || res.status === 423) {\n // Only a body naming the same verdict as the status is statewire's own;\n // a bare status is middleware noise — a transport fault, retried.\n const verdict =\n res.status === 409\n ? (\"seq-gap\" as const)\n : res.status === 412\n ? (\"unknown-client\" as const)\n : (\"stale-lease\" as const);\n const body = (await res.json().catch(() => null)) as {\n error?: string;\n } | null;\n if (body?.error === verdict) return { type: verdict };\n throw new Error(`HTTP ${res.status}`);\n }\n if (res.status === 429 || res.status === 503) throw overloaded(res);\n if (res.status >= 400 && res.status < 500) {\n return { type: \"rejected\", message: `HTTP ${res.status}` };\n }\n throw new Error(`HTTP ${res.status}`);\n};\n\nconst useSseLink = (\n options: StatewireClient.SSEOptions,\n ctx: StatewireRemoteProps,\n): RemoteLink => {\n const latest = useRef(options);\n latest.current = options;\n const c = useStatewireConn({\n url: options.url,\n onChange: options.onChange,\n ctx,\n attemptOnce: (c, abort) => attemptStream(c, options, abort),\n });\n const [link] = useState(\n (): RemoteLink => ({\n get connection() {\n return c.connection;\n },\n send: (args) => sendCommand(c, latest.current, args),\n }),\n );\n return link;\n};\n\nconst useStatewireSSE = (\n options: StatewireClient.SSEOptions,\n): Statewire<any, any, any> => useStatewireRemote(options, useSseLink);\n\nexport const StatewireSSE = resource(useStatewireSSE) as <\n TState = any,\n TCommands extends Statewire.Commands = Statewire.Commands,\n TEvent = any,\n>(\n options: StatewireClient.SSEOptions,\n) => ResourceElement<Statewire<TState | undefined, TCommands, TEvent>>;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"sse.js","names":["useRef","useState","flushTapSync","resource","ResourceElement","encodeParams","reviveResponse","createSseParser","timing","classify","handleEnvelope","overloadedError","parseRetryAfter","useStatewireConn","Conn","StatewireEnvelope","StatewireResponse","Statewire","StatewireClient","StatewireHost","useStatewireRemote","StatewireRemoteProps","clientId","onSnapshot","args","ops","lastSeq","onOps","res","ack","onFinish","Fin","StatewireWireCommand","method","params","context","Record","contextOps","value","op","path","encodeWireCommand","command","undefined","StatewireSubmitResult","type","response","message","ConnectionState","RemoteLink","connection","Connection","send","seq","Promise","baseUrl","options","SSEOptions","url","replace","AuthState","stale","requestHeaders","extra","ctx","staleAuth","headers","merged","Headers","name","Object","entries","set","timedFetch","input","init","RequestInit","signal","AbortSignal","timeout","Response","fetch","globalThis","consume","c","reader","ReadableStreamDefaultReader","Uint8Array","heartbeat","AbortController","dead","decoder","TextDecoder","parse","timer","setTimeout","abort","heartbeatTimeoutMs","result","ReadableStreamReadResult","read","clearTimeout","done","data","decode","stream","env","JSON","attemptStream","auth","accept","headerTimer","cache","any","status","overloaded","episode","cause","Error","ok","body","getReader","aborted","cancel","catch","get","postCommand","String","lease","stringify","lifetime","sendCommand","json","reconnect","error","rejections","verdict","const","useSseLink","latest","current","onChange","attemptOnce","link","useStatewireSSE","StatewireSSE","Commands","TState","TCommands","TEvent"],"sources":["../../src/transports/sse.ts"],"sourcesContent":["import { useRef, useState } from \"react\";\nimport { flushTapSync, resource } from \"@assistant-ui/tap\";\nimport type { ResourceElement } from \"@assistant-ui/tap\";\nimport { encodeParams, reviveResponse } from \"../wire/meta\";\nimport { createSseParser } from \"./sse-parser\";\nimport { timing } from \"../client/timing\";\nimport {\n classify,\n handleEnvelope,\n overloadedError,\n parseRetryAfter,\n useStatewireConn,\n} from \"../client/connection\";\nimport type {\n Conn,\n StatewireEnvelope,\n StatewireResponse,\n} from \"../client/connection\";\nimport type { Statewire } from \"../types\";\nimport type { StatewireClient } from \"../statewire\";\nimport type { StatewireHost } from \"../host\";\nimport { useStatewireRemote } from \"./remote\";\n\nexport type StatewireRemoteProps = {\n clientId: string;\n onSnapshot: (args: { ops: unknown[]; lastSeq: number }) => void;\n onOps: (args: {\n ops: unknown[];\n res?: StatewireResponse[];\n ack?: number;\n }) => void;\n onFinish: (args: StatewireHost.Fin) => void;\n};\n\nexport type StatewireWireCommand =\n | { method: string; params: readonly unknown[]; context?: never }\n | { method?: never; params?: never; context: Record<string, unknown> };\n\n/** The wire shape of a client context submission: one root replace op. */\nexport const contextOps = (value: Record<string, unknown>) => [\n { op: \"replace\", path: [], value },\n];\n\nexport const encodeWireCommand = (command: StatewireWireCommand) => ({\n ...(command.method !== undefined && {\n method: command.method,\n ...encodeParams(command.params),\n }),\n ...(command.context !== undefined && {\n context: contextOps(command.context),\n }),\n});\n\nexport type StatewireSubmitResult =\n | { type: \"receipt\"; response?: StatewireResponse }\n | { type: \"rejected\"; message: string }\n | { type: \"seq-gap\" }\n | { type: \"unknown-client\" }\n | { type: \"stale-lease\" };\n\nexport type ConnectionState = StatewireClient.ConnectionState;\n\n/** The live wire a remote session drives: connection status plus command POST. */\nexport type RemoteLink = {\n connection: StatewireClient.Connection;\n send: (args: {\n seq: number;\n command: StatewireWireCommand;\n }) => Promise<StatewireSubmitResult>;\n};\n\nconst baseUrl = (options: StatewireClient.SSEOptions) =>\n options.url.replace(/\\/$/, \"\");\n\nexport type AuthState = { stale: boolean };\n\nconst requestHeaders = async (\n options: StatewireClient.SSEOptions,\n extra: Record<string, string>,\n ctx: { staleAuth: boolean },\n) => {\n const { headers } = options;\n const merged = new Headers(\n typeof headers === \"function\" ? await headers(ctx) : headers,\n );\n for (const [name, value] of Object.entries(extra)) merged.set(name, value);\n return merged;\n};\n\nconst timedFetch = async (\n options: StatewireClient.SSEOptions,\n input: string,\n init: RequestInit & { signal: AbortSignal },\n timeout: AbortSignal,\n): Promise<Response> => {\n try {\n return await (options.fetch ?? globalThis.fetch)(input, init);\n } catch {\n throw classify(timeout);\n }\n};\n\nconst consume = async (\n c: Conn,\n reader: ReadableStreamDefaultReader<Uint8Array>,\n heartbeat: AbortController,\n dead: () => boolean,\n): Promise<\"stop\" | \"reattach\" | \"ended\"> => {\n const decoder = new TextDecoder();\n const parse = createSseParser();\n for (;;) {\n const timer = setTimeout(\n () => heartbeat.abort(),\n timing.heartbeatTimeoutMs,\n );\n let result: ReadableStreamReadResult<Uint8Array>;\n try {\n result = await reader.read();\n } catch {\n throw classify(heartbeat.signal);\n } finally {\n clearTimeout(timer);\n }\n if (result.done) return \"ended\";\n for (const data of parse(decoder.decode(result.value, { stream: true }))) {\n if (dead()) return \"ended\";\n const env = JSON.parse(data) as StatewireEnvelope;\n const done = flushTapSync(() => handleEnvelope(c, env));\n if (done) return done;\n }\n }\n};\n\nconst attemptStream = async (\n c: Conn,\n options: StatewireClient.SSEOptions,\n auth: AuthState,\n abort: AbortController,\n): Promise<\"stop\" | \"reattach\" | \"ended\"> => {\n const stale = auth.stale;\n auth.stale = false;\n const headers = await requestHeaders(\n options,\n {\n \"Statewire-Client-Id\": c.ctx.clientId,\n accept: \"text/event-stream\",\n },\n { staleAuth: stale },\n );\n const heartbeat = new AbortController();\n const headerTimer = setTimeout(\n () => heartbeat.abort(),\n timing.heartbeatTimeoutMs,\n );\n let res: Response;\n try {\n res = await timedFetch(\n options,\n `${baseUrl(options)}/stream`,\n {\n headers,\n cache: \"no-store\",\n signal: AbortSignal.any([abort.signal, heartbeat.signal]),\n },\n heartbeat.signal,\n );\n } finally {\n clearTimeout(headerTimer);\n }\n if (res.status === 429 || res.status === 503) throw overloaded(res);\n if (res.status === 401) {\n if (stale) c.episode = { cause: \"unauthorized\" };\n auth.stale = true;\n throw new Error(\"unauthorized\");\n }\n if (!res.ok || !res.body) throw new Error(`HTTP ${res.status}`);\n const reader = res.body.getReader();\n try {\n return await consume(c, reader, heartbeat, () => abort.signal.aborted);\n } finally {\n reader.cancel().catch(() => {});\n }\n};\n\nconst overloaded = (res: Response) =>\n overloadedError(parseRetryAfter(res.headers.get(\"Retry-After\")));\n\nconst postCommand = async (\n c: Conn,\n options: StatewireClient.SSEOptions,\n { seq, command }: { seq: number; command: StatewireWireCommand },\n timeout: AbortSignal,\n stale: boolean,\n): Promise<Response> =>\n timedFetch(\n options,\n `${baseUrl(options)}/commands`,\n {\n method: \"POST\",\n headers: await requestHeaders(\n options,\n {\n \"content-type\": \"application/json\",\n \"Statewire-Client-Id\": c.ctx.clientId,\n \"Statewire-Command-Seq\": String(seq),\n ...(c.lease !== null && { \"Statewire-Lease\": c.lease }),\n },\n { staleAuth: stale },\n ),\n body: JSON.stringify(encodeWireCommand(command)),\n signal: AbortSignal.any([\n c.lifetime?.signal ?? AbortSignal.abort(),\n timeout,\n ]),\n },\n timeout,\n );\n\nconst sendCommand = async (\n c: Conn,\n options: StatewireClient.SSEOptions,\n auth: AuthState,\n args: { seq: number; command: StatewireWireCommand },\n): Promise<StatewireSubmitResult> => {\n const timeout = AbortSignal.timeout(timing.heartbeatTimeoutMs);\n let res = await postCommand(c, options, args, timeout, false);\n if (res.status === 401 && typeof options.headers === \"function\") {\n res = await postCommand(c, options, args, timeout, true);\n }\n if (res.ok) {\n let body: { res?: StatewireResponse } | null;\n try {\n body = (await res.json()) as { res?: StatewireResponse } | null;\n } catch {\n return {\n type: \"rejected\",\n message: \"statewire: protocol error: malformed /commands receipt body\",\n };\n }\n return body?.res !== undefined\n ? { type: \"receipt\", response: reviveResponse(body.res) }\n : { type: \"receipt\" };\n }\n if (res.status === 429 || res.status === 503) throw overloaded(res);\n if (res.status === 401) {\n // Fresh credentials did not clear the 401: signal auth-failed on the\n // connection and let the send retry as a transport fault.\n auth.stale = true;\n c.episode = { cause: \"unauthorized\" };\n c.connection.reconnect();\n throw new Error(\"unauthorized\");\n }\n // Bodies settle, statuses pace: only a statewire-shaped body carries a\n // verdict; any bare status is middleware noise — a transport fault, retried.\n const body = (await res.json().catch(() => null)) as {\n error?: string;\n rejections?: { message?: string }[];\n } | null;\n if (body?.error === \"rejected\") {\n return {\n type: \"rejected\",\n message: body.rejections?.[0]?.message ?? `HTTP ${res.status}`,\n };\n }\n if (res.status === 409 || res.status === 412 || res.status === 423) {\n const verdict =\n res.status === 409\n ? (\"seq-gap\" as const)\n : res.status === 412\n ? (\"unknown-client\" as const)\n : (\"stale-lease\" as const);\n if (body?.error === verdict) return { type: verdict };\n }\n throw new Error(`HTTP ${res.status}`);\n};\n\nconst useSseLink = (\n options: StatewireClient.SSEOptions,\n ctx: StatewireRemoteProps,\n): RemoteLink => {\n const latest = useRef(options);\n latest.current = options;\n const [auth] = useState((): AuthState => ({ stale: false }));\n const c = useStatewireConn({\n url: options.url,\n onChange: options.onChange,\n ctx,\n attemptOnce: (c, abort) => attemptStream(c, options, auth, abort),\n });\n const [link] = useState(\n (): RemoteLink => ({\n get connection() {\n return c.connection;\n },\n send: (args) => sendCommand(c, latest.current, auth, args),\n }),\n );\n return link;\n};\n\nconst useStatewireSSE = (\n options: StatewireClient.SSEOptions,\n): Statewire<any, any, any> => useStatewireRemote(options, useSseLink);\n\nexport const StatewireSSE = resource(useStatewireSSE) as <\n TState = any,\n TCommands extends Statewire.Commands = Statewire.Commands,\n TEvent = any,\n>(\n options: StatewireClient.SSEOptions,\n) => ResourceElement<Statewire<TState | undefined, TCommands, TEvent>>;\n"],"mappings":"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"}
|
package/dist/transports/ws.d.ts
CHANGED
|
@@ -3,7 +3,8 @@ 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 UNAUTHORIZED_CLOSE = 4401;
|
|
6
7
|
declare const StatewireWS: <TState = any, TCommands extends Statewire.Commands = Statewire.Commands, TEvent = any>(options: StatewireClient.WSOptions) => ResourceElement<Statewire<TState | undefined, TCommands, TEvent>>;
|
|
7
8
|
//#endregion
|
|
8
|
-
export { StatewireWS, WS_SUBPROTOCOL };
|
|
9
|
+
export { StatewireWS, UNAUTHORIZED_CLOSE, WS_SUBPROTOCOL };
|
|
9
10
|
//# 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":";;;;cAoCa;cACA;cA6OA,cACX,cACA,kBAAkB,UAAU,WAAW,UAAU,UACjD,cAEA,SAAS,gBAAgB,cACtB,gBAAgB,UAAU,oBAAoB,WAAW"}
|
package/dist/transports/ws.js
CHANGED
|
@@ -8,12 +8,22 @@ import { flushTapSync, resource } from "@assistant-ui/tap";
|
|
|
8
8
|
//#region src/transports/ws.ts
|
|
9
9
|
const OPEN = 1;
|
|
10
10
|
const WS_SUBPROTOCOL = "statewire.v1";
|
|
11
|
+
const UNAUTHORIZED_CLOSE = 4401;
|
|
11
12
|
const socketUrl = async (options, clientId) => {
|
|
12
13
|
const { url, query } = options;
|
|
13
14
|
const params = new URLSearchParams(typeof query === "function" ? await query() : query);
|
|
14
15
|
params.set(CLIENT_QUERY_PARAM, clientId);
|
|
15
16
|
return `${url.replace(/\/$/, "").replace(/^http/, "ws")}/ws?${params.toString()}`;
|
|
16
17
|
};
|
|
18
|
+
const headersFrame = async (options, stale) => {
|
|
19
|
+
const { headers } = options;
|
|
20
|
+
if (headers === void 0) return null;
|
|
21
|
+
const resolved = new Headers(typeof headers === "function" ? await headers({ staleAuth: stale }) : headers);
|
|
22
|
+
return JSON.stringify({
|
|
23
|
+
type: "headers",
|
|
24
|
+
headers: Object.fromEntries(resolved)
|
|
25
|
+
});
|
|
26
|
+
};
|
|
17
27
|
const settleOverloaded = (w, { seq, retryAfterMs }) => {
|
|
18
28
|
if (seq === void 0) throw overloadedError(retryAfterMs);
|
|
19
29
|
const entry = w.pending.find((p) => p.seq === seq);
|
|
@@ -32,7 +42,7 @@ const failPending = (w, error) => {
|
|
|
32
42
|
w.pending = [];
|
|
33
43
|
for (const entry of pending) entry.reject(error);
|
|
34
44
|
};
|
|
35
|
-
const runSocket = (c, w, ws, abort) => new Promise((resolve, reject) => {
|
|
45
|
+
const runSocket = (c, w, ws, frame, staleAuth, abort) => new Promise((resolve, reject) => {
|
|
36
46
|
let timer;
|
|
37
47
|
let timedOut = false;
|
|
38
48
|
let outcome = null;
|
|
@@ -47,6 +57,7 @@ const runSocket = (c, w, ws, abort) => new Promise((resolve, reject) => {
|
|
|
47
57
|
arm();
|
|
48
58
|
abort.signal.addEventListener("abort", () => ws.close());
|
|
49
59
|
ws.onerror = () => {};
|
|
60
|
+
if (frame !== null) ws.onopen = () => ws.send(frame);
|
|
50
61
|
const stale = () => Boolean(outcome) || closed || abort.signal.aborted;
|
|
51
62
|
const fail = (error) => {
|
|
52
63
|
clearTimeout(timer);
|
|
@@ -85,25 +96,32 @@ const runSocket = (c, w, ws, abort) => new Promise((resolve, reject) => {
|
|
|
85
96
|
fail(error);
|
|
86
97
|
}
|
|
87
98
|
};
|
|
88
|
-
ws.onclose = () => {
|
|
99
|
+
ws.onclose = (event) => {
|
|
89
100
|
closed = true;
|
|
90
101
|
clearTimeout(timer);
|
|
91
102
|
if (w.socket === ws) failPending(w, /* @__PURE__ */ new Error("network"));
|
|
92
103
|
if (outcome) resolve(outcome);
|
|
93
|
-
else
|
|
104
|
+
else if (event?.code === 4401) {
|
|
105
|
+
if (staleAuth) c.episode = { cause: "unauthorized" };
|
|
106
|
+
w.authStale = true;
|
|
107
|
+
reject(/* @__PURE__ */ new Error("unauthorized"));
|
|
108
|
+
} else reject(/* @__PURE__ */ new Error(timedOut ? "timeout" : "network"));
|
|
94
109
|
};
|
|
95
110
|
});
|
|
96
111
|
const attemptSocket = async (c, w, options, abort) => {
|
|
97
112
|
let ws = null;
|
|
98
113
|
try {
|
|
99
114
|
const url = await socketUrl(options, c.ctx.clientId);
|
|
115
|
+
const stale = w.authStale;
|
|
116
|
+
w.authStale = false;
|
|
117
|
+
const frame = await headersFrame(options, stale);
|
|
100
118
|
abort.signal.throwIfAborted();
|
|
101
119
|
const { protocols, webSocket } = options;
|
|
102
120
|
const WS = webSocket ?? globalThis.WebSocket;
|
|
103
121
|
if (!WS) throw new Error("no WebSocket implementation");
|
|
104
122
|
ws = new WS(url, [WS_SUBPROTOCOL, ...protocols === void 0 ? [] : typeof protocols === "string" ? [protocols] : protocols]);
|
|
105
123
|
w.socket = ws;
|
|
106
|
-
return await runSocket(c, w, ws, abort);
|
|
124
|
+
return await runSocket(c, w, ws, frame, stale, abort);
|
|
107
125
|
} finally {
|
|
108
126
|
if (w.socket === ws) w.socket = null;
|
|
109
127
|
}
|
|
@@ -144,7 +162,8 @@ const sendCommand = (w, { seq, command }) => {
|
|
|
144
162
|
const useWsLink = (options, ctx) => {
|
|
145
163
|
const [w] = useState(() => ({
|
|
146
164
|
socket: null,
|
|
147
|
-
pending: []
|
|
165
|
+
pending: [],
|
|
166
|
+
authStale: false
|
|
148
167
|
}));
|
|
149
168
|
const c_0 = useStatewireConn({
|
|
150
169
|
url: options.url,
|
|
@@ -163,6 +182,6 @@ const useWsLink = (options, ctx) => {
|
|
|
163
182
|
const useStatewireWS = (options) => useStatewireRemote(options, useWsLink);
|
|
164
183
|
const StatewireWS = resource(useStatewireWS);
|
|
165
184
|
//#endregion
|
|
166
|
-
export { StatewireWS, WS_SUBPROTOCOL };
|
|
185
|
+
export { StatewireWS, UNAUTHORIZED_CLOSE, WS_SUBPROTOCOL };
|
|
167
186
|
|
|
168
187
|
//# 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","OPEN","WS_SUBPROTOCOL","socketUrl","options","WSOptions","clientId","url","query","params","URLSearchParams","set","base","replace","toString","settleOverloaded","w","retryAfterMs","undefined","entry","find","p","filter","resolvePending","matches","remaining","type","push","failPending","runSocket","c","ws","abort","AbortController","Promise","timer","ReturnType","setTimeout","timedOut","outcome","closed","arm","clearTimeout","close","heartbeatTimeoutMs","signal","addEventListener","onerror","stale","Boolean","aborted","fail","processEnv","env","done","ack","res","seqs","Set","map","r","has","onmessage","event","frame","overloaded","JSON","parse","String","data","onclose","attemptSocket","ctx","throwIfAborted","protocols","webSocket","WS","globalThis","extra","sendCommand","command","readyState","send","stringify","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};\n\nconst OPEN = 1;\n\nexport const WS_SUBPROTOCOL = \"statewire.v1\";\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 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 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 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 = () => {\n closed = true;\n clearTimeout(timer);\n if (w.socket === ws) failPending(w, new Error(\"network\"));\n if (outcome) resolve(outcome);\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 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, 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((): WsChannel => ({ socket: null, pending: [] }));\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","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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"}
|
package/dist/util/statePath.js
CHANGED
|
@@ -7,12 +7,14 @@ const walkStatePath = (root, path, mode) => {
|
|
|
7
7
|
for (let depth = 0; depth < path.length; depth++) {
|
|
8
8
|
const k = path[depth];
|
|
9
9
|
const mustResolve = mode === "resolve" || mode === "parent" && depth < path.length - 1;
|
|
10
|
-
if (mode !== "read")
|
|
11
|
-
if (
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
if (mode !== "read") {
|
|
11
|
+
if (typeof k === "number") {
|
|
12
|
+
if (!Array.isArray(node) || !Number.isInteger(k) || k < 0) return;
|
|
13
|
+
if (mustResolve && k >= node.length) return void 0;
|
|
14
|
+
} else {
|
|
15
|
+
if (isUnsafeKey(k, path[depth - 1]) || typeof node !== "object" || node === null || Array.isArray(node)) return;
|
|
16
|
+
if (mustResolve && !Object.hasOwn(node, k)) return void 0;
|
|
17
|
+
}
|
|
16
18
|
}
|
|
17
19
|
parent = node;
|
|
18
20
|
key = k;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"statePath.js","names":["StatePath","isUnsafeKey","key","parent","WalkedPath","leaf","walkStatePath","root","path","mode","undefined","node","depth","length","k","mustResolve","Array","isArray","Number","isInteger","Object","hasOwn","Record","readAtPath"],"sources":["../../src/util/statePath.ts"],"sourcesContent":["export type StatePath = readonly (string | number)[];\n\nexport const isUnsafeKey = (\n key: string | number,\n parent: string | number | undefined,\n) => key === \"__proto__\" || (key === \"prototype\" && parent === \"constructor\");\n\nexport type WalkedPath = {\n /** Container of the final key; undefined when the path is empty. */\n parent: unknown;\n key: string | number;\n leaf: unknown;\n};\n\n// \"read\": lenient, never fails — missing steps yield an undefined leaf.\n// \"parent\": every step before the last must resolve; the leaf may not exist.\n// \"resolve\": every step including the last must resolve.\n// Strict modes require array+integer for number keys, plain object for string\n// keys, and reject unsafe (prototype-polluting) segments.\nexport const walkStatePath = (\n root: unknown,\n path: StatePath,\n mode: \"read\" | \"parent\" | \"resolve\",\n): WalkedPath | undefined => {\n let parent: unknown = undefined;\n let key: string | number = \"\";\n let node: unknown = root;\n for (let depth = 0; depth < path.length; depth++) {\n const k = path[depth]!;\n const mustResolve =\n mode === \"resolve\" || (mode === \"parent\" && depth < path.length - 1);\n if (mode !== \"read\") {\n if (typeof k === \"number\") {\n if (!Array.isArray(node) || !Number.isInteger(k) || k < 0) {\n return undefined;\n }\n if (mustResolve && k >= node.length) return undefined;\n } else {\n if (\n isUnsafeKey(k, path[depth - 1]) ||\n typeof node !== \"object\" ||\n node === null ||\n Array.isArray(node)\n ) {\n return undefined;\n }\n if (mustResolve && !Object.hasOwn(node, k)) return undefined;\n }\n }\n parent = node;\n key = k;\n node =\n typeof node === \"object\" && node !== null && Object.hasOwn(node, k)\n ? (node as Record<string | number, unknown>)[k]\n : undefined;\n }\n return { parent, key, leaf: node };\n};\n\nexport const readAtPath = (root: unknown, path: StatePath): unknown =>\n walkStatePath(root, path, \"read\")!.leaf;\n"],"mappings":";AAEA,MAAaC,eACXC,KACAC,WACGD,QAAQ,eAAgBA,QAAQ,eAAeC,WAAW;AAc/D,MAAaG,iBACXC,MACAC,MACAC,SAC2B;CAC3B,IAAIN,SAAkBO,KAAAA;CACtB,IAAIR,MAAuB;CAC3B,IAAIS,OAAgBJ;CACpB,KAAK,IAAIK,QAAQ,GAAGA,QAAQJ,KAAKK,QAAQD,SAAS;EAChD,MAAME,IAAIN,KAAKI;EACf,MAAMG,cACJN,SAAS,aAAcA,SAAS,YAAYG,QAAQJ,KAAKK,SAAS;EACpE,IAAIJ,SAAS,
|
|
1
|
+
{"version":3,"file":"statePath.js","names":["StatePath","isUnsafeKey","key","parent","WalkedPath","leaf","walkStatePath","root","path","mode","undefined","node","depth","length","k","mustResolve","Array","isArray","Number","isInteger","Object","hasOwn","Record","readAtPath"],"sources":["../../src/util/statePath.ts"],"sourcesContent":["export type StatePath = readonly (string | number)[];\n\nexport const isUnsafeKey = (\n key: string | number,\n parent: string | number | undefined,\n) => key === \"__proto__\" || (key === \"prototype\" && parent === \"constructor\");\n\nexport type WalkedPath = {\n /** Container of the final key; undefined when the path is empty. */\n parent: unknown;\n key: string | number;\n leaf: unknown;\n};\n\n// \"read\": lenient, never fails — missing steps yield an undefined leaf.\n// \"parent\": every step before the last must resolve; the leaf may not exist.\n// \"resolve\": every step including the last must resolve.\n// Strict modes require array+integer for number keys, plain object for string\n// keys, and reject unsafe (prototype-polluting) segments.\nexport const walkStatePath = (\n root: unknown,\n path: StatePath,\n mode: \"read\" | \"parent\" | \"resolve\",\n): WalkedPath | undefined => {\n let parent: unknown = undefined;\n let key: string | number = \"\";\n let node: unknown = root;\n for (let depth = 0; depth < path.length; depth++) {\n const k = path[depth]!;\n const mustResolve =\n mode === \"resolve\" || (mode === \"parent\" && depth < path.length - 1);\n if (mode !== \"read\") {\n if (typeof k === \"number\") {\n if (!Array.isArray(node) || !Number.isInteger(k) || k < 0) {\n return undefined;\n }\n if (mustResolve && k >= node.length) return undefined;\n } else {\n if (\n isUnsafeKey(k, path[depth - 1]) ||\n typeof node !== \"object\" ||\n node === null ||\n Array.isArray(node)\n ) {\n return undefined;\n }\n if (mustResolve && !Object.hasOwn(node, k)) return undefined;\n }\n }\n parent = node;\n key = k;\n node =\n typeof node === \"object\" && node !== null && Object.hasOwn(node, k)\n ? (node as Record<string | number, unknown>)[k]\n : undefined;\n }\n return { parent, key, leaf: node };\n};\n\nexport const readAtPath = (root: unknown, path: StatePath): unknown =>\n walkStatePath(root, path, \"read\")!.leaf;\n"],"mappings":";AAEA,MAAaC,eACXC,KACAC,WACGD,QAAQ,eAAgBA,QAAQ,eAAeC,WAAW;AAc/D,MAAaG,iBACXC,MACAC,MACAC,SAC2B;CAC3B,IAAIN,SAAkBO,KAAAA;CACtB,IAAIR,MAAuB;CAC3B,IAAIS,OAAgBJ;CACpB,KAAK,IAAIK,QAAQ,GAAGA,QAAQJ,KAAKK,QAAQD,SAAS;EAChD,MAAME,IAAIN,KAAKI;EACf,MAAMG,cACJN,SAAS,aAAcA,SAAS,YAAYG,QAAQJ,KAAKK,SAAS;EACpE,IAAIJ,SAAS,QAAQ;GACnB,IAAI,OAAOK,MAAM,UAAU;IACzB,IAAI,CAACE,MAAMC,QAAQN,IAAI,KAAK,CAACO,OAAOC,UAAUL,CAAC,KAAKA,IAAI,GACtD;IAEF,IAAIC,eAAeD,KAAKH,KAAKE,QAAQ,OAAOH,KAAAA;GAC9C,OAAO;IACL,IACET,YAAYa,GAAGN,KAAKI,QAAQ,EAAE,KAC9B,OAAOD,SAAS,YAChBA,SAAS,QACTK,MAAMC,QAAQN,IAAI,GAElB;IAEF,IAAII,eAAe,CAACK,OAAOC,OAAOV,MAAMG,CAAC,GAAG,OAAOJ,KAAAA;GACrD;EACF;EACAP,SAASQ;EACTT,MAAMY;EACNH,OACE,OAAOA,SAAS,YAAYA,SAAS,QAAQS,OAAOC,OAAOV,MAAMG,CAAC,IAC7DH,KAA0CG,KAC3CJ,KAAAA;CACR;CACA,OAAO;EAAEP;EAAQD;EAAKG,MAAMM;CAAK;AACnC;AAEA,MAAaY,cAAchB,MAAeC,SACxCF,cAAcC,MAAMC,MAAM,MAAM,CAAC,CAAEH"}
|
package/package.json
CHANGED
package/src/client/connection.ts
CHANGED
package/src/client/core.ts
CHANGED
|
@@ -159,6 +159,7 @@ const createCore = (
|
|
|
159
159
|
const connection = getConnection();
|
|
160
160
|
const connectionChanged = connection !== notifiedConnection;
|
|
161
161
|
notifiedConnection = connection;
|
|
162
|
+
if (connectionChanged) options.onConnectionChange?.(connection);
|
|
162
163
|
if (stateChanged || connectionChanged) {
|
|
163
164
|
for (const listener of [...listeners]) listener();
|
|
164
165
|
}
|
package/src/client/session.ts
CHANGED
|
@@ -147,11 +147,7 @@ const rejectedSend = (
|
|
|
147
147
|
|
|
148
148
|
type AttachState = "detached" | "attached" | "finished";
|
|
149
149
|
|
|
150
|
-
export type StatewireSessionOptions = StatewireSessionStoreOptions
|
|
151
|
-
onCommandSessionLost?:
|
|
152
|
-
| ((command: StatewireClient.SessionLostCommand) => "replay" | void)
|
|
153
|
-
| undefined;
|
|
154
|
-
};
|
|
150
|
+
export type StatewireSessionOptions = StatewireSessionStoreOptions;
|
|
155
151
|
|
|
156
152
|
type Session = {
|
|
157
153
|
record: StatewireSessionRecord;
|
|
@@ -172,7 +168,6 @@ type Session = {
|
|
|
172
168
|
transport: RemoteLink | null;
|
|
173
169
|
waiters: (() => void)[];
|
|
174
170
|
lifecycle: CommandsListener | null;
|
|
175
|
-
onCommandSessionLost: StatewireSessionOptions["onCommandSessionLost"];
|
|
176
171
|
notify: () => void;
|
|
177
172
|
emitEvent: (event: unknown) => void;
|
|
178
173
|
send: (command: StatewireWireCommand) => Promise<unknown>;
|
|
@@ -219,7 +214,6 @@ const createSession = (options: StatewireSessionOptions): Session => {
|
|
|
219
214
|
transport: null,
|
|
220
215
|
waiters: [],
|
|
221
216
|
lifecycle: null,
|
|
222
|
-
onCommandSessionLost: options.onCommandSessionLost,
|
|
223
217
|
notify: () => {},
|
|
224
218
|
emitEvent: () => {},
|
|
225
219
|
send: (command) => sendCommand(s, command),
|
|
@@ -298,25 +292,15 @@ const settleAll = (
|
|
|
298
292
|
}
|
|
299
293
|
};
|
|
300
294
|
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
// attach retry evicts again and re-offers.
|
|
295
|
+
// Claims every unsettled real command for replay under the rotated client id,
|
|
296
|
+
// in seq order; context submissions settle and the context re-sends whole.
|
|
304
297
|
const claimReplays = (s: Session): Pending[] => {
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
method: entry.inv.method,
|
|
312
|
-
params: [...entry.inv.params],
|
|
313
|
-
seq: entry.seq,
|
|
314
|
-
});
|
|
315
|
-
if (decision === "replay") claimed.add(entry);
|
|
316
|
-
}
|
|
317
|
-
if (claimed.size === 0) return [];
|
|
318
|
-
s.pending = s.pending.filter((p) => !claimed.has(p));
|
|
319
|
-
return [...claimed];
|
|
298
|
+
const claimed = s.pending.filter(
|
|
299
|
+
(entry) => entry.inv !== null && !isResolved(entry.state),
|
|
300
|
+
);
|
|
301
|
+
if (claimed.length === 0) return [];
|
|
302
|
+
s.pending = s.pending.filter((p) => !claimed.includes(p));
|
|
303
|
+
return claimed;
|
|
320
304
|
};
|
|
321
305
|
|
|
322
306
|
const evict = (
|
|
@@ -745,7 +729,6 @@ export const useStatewireSession = (
|
|
|
745
729
|
emit: (event: unknown) => void,
|
|
746
730
|
): Session => {
|
|
747
731
|
const [s] = useState(() => createSession(options));
|
|
748
|
-
s.onCommandSessionLost = options.onCommandSessionLost;
|
|
749
732
|
useEffect(() => {
|
|
750
733
|
s.emitEvent = emit;
|
|
751
734
|
return () => {
|