statewire 0.0.2 → 0.0.3

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.
Files changed (44) hide show
  1. package/dist/client/session.d.ts +5 -2
  2. package/dist/client/session.d.ts.map +1 -1
  3. package/dist/client/session.js +15 -8
  4. package/dist/client/session.js.map +1 -1
  5. package/dist/host/host.d.ts +2 -1
  6. package/dist/host/host.d.ts.map +1 -1
  7. package/dist/host/host.js +5 -1
  8. package/dist/host/host.js.map +1 -1
  9. package/dist/host/socket.d.ts.map +1 -1
  10. package/dist/host/socket.js +58 -6
  11. package/dist/host/socket.js.map +1 -1
  12. package/dist/host.d.ts +10 -0
  13. package/dist/host.d.ts.map +1 -1
  14. package/dist/statewire.d.ts +3 -0
  15. package/dist/statewire.d.ts.map +1 -1
  16. package/dist/statewire.js +10 -2
  17. package/dist/statewire.js.map +1 -1
  18. package/dist/transports/remote.d.ts.map +1 -1
  19. package/dist/transports/remote.js +3 -2
  20. package/dist/transports/remote.js.map +1 -1
  21. package/dist/transports/sse.d.ts +23 -1
  22. package/dist/transports/sse.d.ts.map +1 -1
  23. package/dist/transports/sse.js +15 -5
  24. package/dist/transports/sse.js.map +1 -1
  25. package/dist/transports/testing.d.ts +4 -3
  26. package/dist/transports/testing.d.ts.map +1 -1
  27. package/dist/transports/testing.js.map +1 -1
  28. package/dist/transports/ws.d.ts.map +1 -1
  29. package/dist/transports/ws.js +2 -3
  30. package/dist/transports/ws.js.map +1 -1
  31. package/dist/types.d.ts +2 -0
  32. package/dist/types.d.ts.map +1 -1
  33. package/dist/types.js.map +1 -1
  34. package/package.json +1 -1
  35. package/src/client/session.ts +43 -21
  36. package/src/host/host.ts +15 -4
  37. package/src/host/socket.ts +85 -9
  38. package/src/host.ts +11 -0
  39. package/src/statewire.ts +15 -1
  40. package/src/transports/remote.ts +6 -1
  41. package/src/transports/sse.ts +19 -8
  42. package/src/transports/testing.ts +6 -4
  43. package/src/transports/ws.ts +2 -8
  44. package/src/types.ts +2 -0
@@ -1 +1 @@
1
- {"version":3,"file":"ws.js","names":["useState","flushTapSync","resource","ResourceElement","encodeParams","timing","handleEnvelope","overloadedError","useStatewireConn","Conn","StatewireEnvelope","Statewire","StatewireClient","useStatewireRemote","CLIENT_QUERY_PARAM","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","method","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 { encodeParams } from \"../wire/meta\";\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 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(\n JSON.stringify({\n method: command.method,\n ...encodeParams(command.params),\n seq,\n }),\n );\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","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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"}
package/dist/types.d.ts CHANGED
@@ -50,6 +50,8 @@ interface Statewire<TState = any, TCommands extends Statewire.Commands = Statewi
50
50
  emit?: (event: TEvent) => void;
51
51
  connection?: StatewireClient.Connection;
52
52
  source?: Statewire.StateSource;
53
+ /** Replace this client's ambient context on the server; absent on transports without a client context channel. */
54
+ setContext?: (value: Record<string, unknown>) => Promise<unknown>;
53
55
  }
54
56
  declare class StatewireSendError extends Error {
55
57
  readonly failure: StatewireClient.SendFailure;
@@ -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;;IACpB;IAAa;;OAEP;IACV,YAAY;IACZ,eAAe;;;UAIF,UACf,cACA,kBAAkB,UAAU,WAAW,UAAU,UACjD;EAEA,OAAO;EACP,UAAU,UAAU,cAAc;EAClC,QAAQ,OAAO;EACf,aAAa,gBAAgB;EAC7B,SAAS,UAAU;;cAsBR,2BAA2B;WAC7B,SAAS,gBAAgB;EAEtB,YAAA,SAAS,gBAAgB"}
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;;IACpB;IAAa;;OAEP;IACV,YAAY;IACZ,eAAe;;;UAIF,UACf,cACA,kBAAkB,UAAU,WAAW,UAAU,UACjD;EAEA,OAAO;EACP,UAAU,UAAU,cAAc;EAClC,QAAQ,OAAO;EACf,aAAa,gBAAgB;EAC7B,SAAS,UAAU;;EAEnB,cAAc,OAAO,4BAA4B;;cAsBtC,2BAA2B;WAC7B,SAAS,gBAAgB;EAEtB,YAAA,SAAS,gBAAgB"}
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","emit","event","TEvent","connection","Connection","source","describe","fate","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\" | \"acked\" | \"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 | { op: \"event\"; value: object };\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 TEvent = any,\n> {\n state: TState;\n commands: Statewire.CommandsProxy<TCommands>;\n emit?: (event: TEvent) => void;\n connection?: StatewireClient.Connection;\n source?: Statewire.StateSource;\n}\n\nconst describe = (failure: StatewireClient.SendFailure): string => {\n switch (failure.fate) {\n case \"rejected\":\n return `statewire: command rejected: ${failure.message}`;\n case \"unknown-command\":\n return \"statewire: unknown command\";\n case \"invalid-params\":\n return `statewire: invalid params: ${failure.message}`;\n case \"aborted\":\n return `statewire: command aborted (${failure.cause})`;\n case \"lost\":\n return `statewire: command lost (${failure.cause})`;\n case \"crashed\":\n return \"statewire: command crashed\";\n case \"result-unavailable\":\n return `statewire: command result unavailable (${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":";AA4DA,MAAM2C,YAAYvB,YAAiD;CACjE,QAAQA,QAAQwB,MAAhB;EACE,KAAK,YACH,OAAO,gCAAgCxB,QAAQyB;EACjD,KAAK,mBACH,OAAO;EACT,KAAK,kBACH,OAAO,8BAA8BzB,QAAQyB;EAC/C,KAAK,WACH,OAAO,+BAA+BzB,QAAQ0B,MAAK;EACrD,KAAK,QACH,OAAO,4BAA4B1B,QAAQ0B,MAAK;EAClD,KAAK,WACH,OAAO;EACT,KAAK,sBACH,OAAO,0CAA0C1B,QAAQ0B,MAAK;CAClE;AACF;AAEA,IAAaC,qBAAb,cAAwCC,MAAM;CAC5C;CAEAC,YAAY7B,SAAsC;EAChD,MAAMuB,SAASvB,OAAO,CAAC;EACvB,KAAK8B,OAAO;EACZ,KAAK9B,UAAUA;CACjB;AACF"}
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","emit","event","TEvent","connection","Connection","source","setContext","describe","fate","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\" | \"acked\" | \"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 | { op: \"event\"; value: object };\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 TEvent = any,\n> {\n state: TState;\n commands: Statewire.CommandsProxy<TCommands>;\n emit?: (event: TEvent) => void;\n connection?: StatewireClient.Connection;\n source?: Statewire.StateSource;\n /** Replace this client's ambient context on the server; absent on transports without a client context channel. */\n setContext?: (value: Record<string, unknown>) => Promise<unknown>;\n}\n\nconst describe = (failure: StatewireClient.SendFailure): string => {\n switch (failure.fate) {\n case \"rejected\":\n return `statewire: command rejected: ${failure.message}`;\n case \"unknown-command\":\n return \"statewire: unknown command\";\n case \"invalid-params\":\n return `statewire: invalid params: ${failure.message}`;\n case \"aborted\":\n return `statewire: command aborted (${failure.cause})`;\n case \"lost\":\n return `statewire: command lost (${failure.cause})`;\n case \"crashed\":\n return \"statewire: command crashed\";\n case \"result-unavailable\":\n return `statewire: command result unavailable (${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":";AA8DA,MAAM4C,YAAYxB,YAAiD;CACjE,QAAQA,QAAQyB,MAAhB;EACE,KAAK,YACH,OAAO,gCAAgCzB,QAAQ0B;EACjD,KAAK,mBACH,OAAO;EACT,KAAK,kBACH,OAAO,8BAA8B1B,QAAQ0B;EAC/C,KAAK,WACH,OAAO,+BAA+B1B,QAAQ2B,MAAK;EACrD,KAAK,QACH,OAAO,4BAA4B3B,QAAQ2B,MAAK;EAClD,KAAK,WACH,OAAO;EACT,KAAK,sBACH,OAAO,0CAA0C3B,QAAQ2B,MAAK;CAClE;AACF;AAEA,IAAaC,qBAAb,cAAwCC,MAAM;CAC5C;CAEAC,YAAY9B,SAAsC;EAChD,MAAMwB,SAASxB,OAAO,CAAC;EACvB,KAAK+B,OAAO;EACZ,KAAK/B,UAAUA;CACjB;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "statewire",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "State over the wire: snapshot-first delta streaming and command submission for any state and any command",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -40,7 +40,8 @@ type PendingState =
40
40
  type Pending = {
41
41
  readonly seq: number;
42
42
  readonly command: StatewireWireCommand;
43
- readonly inv: CommandsInvocation;
43
+ /** null for context submissions — they carry no command lifecycle. */
44
+ readonly inv: CommandsInvocation | null;
44
45
  state: PendingState;
45
46
  resolve: (payload: unknown) => void;
46
47
  reject: (error: StatewireSendError) => void;
@@ -102,20 +103,22 @@ const transition = (
102
103
  }
103
104
  const covers = !isStateSettled(prev) && isStateSettled(next);
104
105
  entry.state = next;
105
- if (covers) s.lifecycle?.onCovered?.(entry.inv);
106
+ if (covers && entry.inv) s.lifecycle?.onCovered?.(entry.inv);
106
107
  if (settles && outcome !== undefined) {
107
108
  if (outcome.ok) {
108
109
  entry.resolve(outcome.payload);
109
- s.lifecycle?.onSettle(entry.inv, {
110
- type: "accepted",
111
- payload: outcome.payload,
112
- });
110
+ if (entry.inv)
111
+ s.lifecycle?.onSettle(entry.inv, {
112
+ type: "accepted",
113
+ payload: outcome.payload,
114
+ });
113
115
  } else {
114
116
  entry.reject(new StatewireSendError(outcome.failure));
115
- s.lifecycle?.onSettle(entry.inv, {
116
- type: "rejected",
117
- error: new StatewireSendError(outcome.failure),
118
- });
117
+ if (entry.inv)
118
+ s.lifecycle?.onSettle(entry.inv, {
119
+ type: "rejected",
120
+ error: new StatewireSendError(outcome.failure),
121
+ });
119
122
  }
120
123
  }
121
124
  if (next.at === "done") {
@@ -150,6 +153,7 @@ type Session = {
150
153
  recordPersisted: boolean;
151
154
  store: StatewireSessionStore;
152
155
  lastAck: number;
156
+ context: Record<string, unknown> | undefined;
153
157
  pending: Pending[];
154
158
  canonical: unknown;
155
159
  lastPublished: unknown;
@@ -185,6 +189,7 @@ const createSession = (options: StatewireSessionStoreOptions): Session => {
185
189
  recordPersisted: stored !== null,
186
190
  store,
187
191
  lastAck: -1,
192
+ context: undefined,
188
193
  pending: [],
189
194
  canonical: undefined,
190
195
  lastPublished: undefined,
@@ -294,6 +299,9 @@ const evict = (
294
299
  s.record = { clientId: newClientId(), nextSeq: 1 };
295
300
  s.store.save(s.record);
296
301
  s.attach = "detached";
302
+ // The server dropped this client's record and its stored context with it:
303
+ // the new session opens with a fresh full root replace.
304
+ if (s.context !== undefined) void s.send({ context: s.context });
297
305
  publish(s);
298
306
  wake(s);
299
307
  };
@@ -301,7 +309,7 @@ const evict = (
301
309
  const noteAcked = (s: Session, seq: number) => {
302
310
  for (const entry of s.pending) {
303
311
  if (entry.seq > seq || isStateSettled(entry.state)) continue;
304
- s.lifecycle?.onAck(entry.inv);
312
+ if (entry.inv) s.lifecycle?.onAck(entry.inv);
305
313
  }
306
314
  };
307
315
 
@@ -546,7 +554,7 @@ const pump = async (s: Session) => {
546
554
  ) {
547
555
  transition(s, entry, { ...state, posted: true });
548
556
  }
549
- s.lifecycle?.onPosted?.(entry.inv);
557
+ if (entry.inv) s.lifecycle?.onPosted?.(entry.inv);
550
558
  if (response !== undefined) {
551
559
  applyResponse(s, response);
552
560
  publish(s);
@@ -653,13 +661,16 @@ const sendCommand = (
653
661
  const entry: Pending = {
654
662
  seq,
655
663
  command,
656
- inv: {
657
- method: command.method,
658
- params: command.params,
659
- ts: Date.now(),
660
- seq,
661
- clientId: s.record.clientId,
662
- },
664
+ inv:
665
+ command.method === undefined
666
+ ? null
667
+ : {
668
+ method: command.method,
669
+ params: command.params,
670
+ ts: Date.now(),
671
+ seq,
672
+ clientId: s.record.clientId,
673
+ },
663
674
  state: { at: "queued", pendingMarked: false },
664
675
  resolve: () => {},
665
676
  reject: () => {},
@@ -670,11 +681,22 @@ const sendCommand = (
670
681
  });
671
682
  result.catch(() => {});
672
683
  s.pending.push(entry);
673
- s.lifecycle?.onStart(entry.inv);
684
+ if (entry.inv) s.lifecycle?.onStart(entry.inv);
674
685
  void pump(s);
675
686
  return result;
676
687
  };
677
688
 
689
+ const setContext = (
690
+ s: Session,
691
+ value: Record<string, unknown>,
692
+ ): Promise<unknown> => {
693
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
694
+ throw new Error("statewire: context must be a plain object");
695
+ }
696
+ s.context = value;
697
+ return s.send({ context: value });
698
+ };
699
+
678
700
  export const useStatewireSession = (
679
701
  options: StatewireSessionStoreOptions,
680
702
  emit: (event: unknown) => void,
@@ -693,4 +715,4 @@ export const useStatewireSession = (
693
715
  return s;
694
716
  };
695
717
 
696
- export { setTransport };
718
+ export { setContext, setTransport };
package/src/host/host.ts CHANGED
@@ -203,6 +203,7 @@ export type CommandsSubmission = {
203
203
  method: string;
204
204
  params: readonly unknown[];
205
205
  clientId?: string;
206
+ caller?: StatewireHost.Caller;
206
207
  onAck?: () => void;
207
208
  onSettle?: (outcome: CommandsOutcome) => void;
208
209
  };
@@ -247,6 +248,12 @@ type CommandsCell = {
247
248
 
248
249
  const STALL_WARN_MS = 5000;
249
250
 
251
+ // In-process dispatch has no wire client: never any ambient context, trivially attached.
252
+ const LOCAL_CALLER: StatewireHost.Caller = Object.freeze({
253
+ context: null,
254
+ isConnected: true,
255
+ });
256
+
250
257
  const toSendError = (error: unknown): StatewireSendError =>
251
258
  error instanceof StatewireSendError
252
259
  ? error
@@ -324,9 +331,9 @@ const pump = (cell: CommandsCell) => {
324
331
  cell.listener?.onCovered?.(inv);
325
332
  void (async () => {
326
333
  try {
327
- const value = await (handler as (...args: readonly unknown[]) => unknown)(
328
- ...call.params,
329
- );
334
+ const value = await (
335
+ handler as (...args: readonly unknown[]) => unknown
336
+ ).call(call.origin?.caller ?? LOCAL_CALLER, ...call.params);
330
337
  const execution =
331
338
  typeof value === "object" && value !== null
332
339
  ? executionStates.get(value)
@@ -367,7 +374,11 @@ const watchStall = (cell: CommandsCell) => {
367
374
  };
368
375
 
369
376
  export const useStatewireCommands = <THandlers extends Statewire.Commands>(
370
- commands: THandlers | undefined | null | false,
377
+ commands:
378
+ | (THandlers & ThisType<StatewireHost.Caller>)
379
+ | undefined
380
+ | null
381
+ | false,
371
382
  ): Statewire.CommandsProxy<StatewireHost.HandledCommands<THandlers>> => {
372
383
  const [built] = useState(() => {
373
384
  const cell: CommandsCell = {
@@ -40,6 +40,7 @@ type Admission = {
40
40
  type ClientRec = {
41
41
  id: string;
42
42
  lastSeq: number;
43
+ context: Record<string, unknown> | null;
43
44
  results: Map<number, { ts: number; res: StatewireResponse }>;
44
45
  inflight: Map<number, Admission>;
45
46
  };
@@ -51,7 +52,12 @@ type Conn = {
51
52
  lastSentAt: number;
52
53
  };
53
54
 
54
- type Frame = { method: string; seq: number; params: unknown[] };
55
+ type Frame = {
56
+ method?: string;
57
+ seq: number;
58
+ params: unknown[];
59
+ context?: unknown;
60
+ };
55
61
 
56
62
  const parseFrame = (raw: string): Frame | null => {
57
63
  let frame: unknown;
@@ -63,9 +69,16 @@ const parseFrame = (raw: string): Frame | null => {
63
69
  if (typeof frame !== "object" || frame === null || Array.isArray(frame)) {
64
70
  return null;
65
71
  }
66
- const { method, seq, params, meta } = frame as Record<string, unknown>;
67
- if (typeof method !== "string") return null;
72
+ const { method, seq, params, meta, context } = frame as Record<
73
+ string,
74
+ unknown
75
+ >;
76
+ if (method === undefined && context === undefined) return null;
68
77
  if (typeof seq !== "number" || !Number.isInteger(seq) || seq < 1) return null;
78
+ if (method === undefined) {
79
+ return { seq, params: [], ...(context !== undefined && { context }) };
80
+ }
81
+ if (typeof method !== "string") return null;
69
82
  if (!Array.isArray(params)) return null;
70
83
  if (meta !== undefined) {
71
84
  try {
@@ -74,7 +87,29 @@ const parseFrame = (raw: string): Frame | null => {
74
87
  return null;
75
88
  }
76
89
  }
77
- return { method, seq, params };
90
+ return { method, seq, params, ...(context !== undefined && { context }) };
91
+ };
92
+
93
+ /** v1 context submissions are exactly one root replace with an object value. */
94
+ const contextValue = (context: unknown): Record<string, unknown> => {
95
+ if (!Array.isArray(context) || context.length !== 1) {
96
+ throw new Error("context must be a single-op array");
97
+ }
98
+ const op = context[0] as Record<string, unknown> | null;
99
+ if (
100
+ typeof op !== "object" ||
101
+ op === null ||
102
+ op["op"] !== "replace" ||
103
+ !Array.isArray(op["path"]) ||
104
+ op["path"].length !== 0
105
+ ) {
106
+ throw new Error("context op must be a root replace");
107
+ }
108
+ const value = op["value"];
109
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
110
+ throw new Error("context value must be an object");
111
+ }
112
+ return value as Record<string, unknown>;
78
113
  };
79
114
 
80
115
  const encodePayload = (
@@ -321,15 +356,14 @@ const createSocketHost = (
321
356
  return { seq, type: "result-unavailable" };
322
357
  };
323
358
 
324
- const settleWire = (
359
+ const settleResponse = (
325
360
  rec: ClientRec,
326
361
  adm: Admission,
327
- outcome: CommandsOutcome,
362
+ response: StatewireResponse,
328
363
  ) => {
329
364
  if (adm.settled) return;
330
365
  adm.settled = true;
331
366
  rec.inflight.delete(adm.seq);
332
- const response = responseOf(adm, outcome);
333
367
  retain(rec, response);
334
368
  if (!adm.acked) {
335
369
  adm.acked = true;
@@ -346,6 +380,25 @@ const createSocketHost = (
346
380
  adm.release();
347
381
  };
348
382
 
383
+ const settleWire = (
384
+ rec: ClientRec,
385
+ adm: Admission,
386
+ outcome: CommandsOutcome,
387
+ ) => settleResponse(rec, adm, responseOf(adm, outcome));
388
+
389
+ const callerOf = (rec: ClientRec): StatewireHost.Caller => ({
390
+ get context() {
391
+ return rec.context;
392
+ },
393
+ get isConnected() {
394
+ if (clients.get(rec.id) !== rec) return false;
395
+ for (const conn of conns) {
396
+ if (conn.clientId === rec.id && conn.open) return true;
397
+ }
398
+ return false;
399
+ },
400
+ });
401
+
349
402
  const admit = (conn: Conn, frame: Frame): Promise<void> | void => {
350
403
  if (!conn.open || disposed || finished !== null) return;
351
404
  const rec = clients.get(conn.clientId)!;
@@ -364,20 +417,42 @@ const createSocketHost = (
364
417
  }
365
418
  const adm: Admission = {
366
419
  seq: frame.seq,
367
- method: frame.method,
420
+ method: frame.method ?? "",
368
421
  acked: false,
369
422
  pendingSent: false,
370
423
  settled: false,
371
424
  release: () => {},
372
425
  };
426
+ if (frame.context !== undefined) {
427
+ let value: Record<string, unknown>;
428
+ try {
429
+ value = contextValue(frame.context);
430
+ } catch (error) {
431
+ rec.inflight.set(frame.seq, adm);
432
+ settleResponse(rec, adm, {
433
+ seq: frame.seq,
434
+ type: "rejected",
435
+ code: "invalid-context",
436
+ message: `invalid context: ${(error as Error).message}`,
437
+ });
438
+ return;
439
+ }
440
+ rec.context = value;
441
+ if (frame.method === undefined) {
442
+ rec.inflight.set(frame.seq, adm);
443
+ settleResponse(rec, adm, { seq: frame.seq, type: "accepted" });
444
+ return;
445
+ }
446
+ }
373
447
  const released = new Promise<void>((resolve) => {
374
448
  adm.release = resolve;
375
449
  });
376
450
  rec.inflight.set(frame.seq, adm);
377
451
  commandsHost!.submit!({
378
- method: frame.method,
452
+ method: frame.method!,
379
453
  params: frame.params,
380
454
  clientId: rec.id,
455
+ caller: callerOf(rec),
381
456
  onAck: () => {
382
457
  if (adm.settled || adm.acked) return;
383
458
  adm.acked = true;
@@ -437,6 +512,7 @@ const createSocketHost = (
437
512
  const created: ClientRec = {
438
513
  id: clientId,
439
514
  lastSeq: 0,
515
+ context: null,
440
516
  results: new Map(),
441
517
  inflight: new Map(),
442
518
  };
package/src/host.ts CHANGED
@@ -46,6 +46,17 @@ export namespace StatewireHost {
46
46
  reject: (error: unknown) => void;
47
47
  };
48
48
 
49
+ /**
50
+ * The submitting client, bound as `this` of a command handler. Holdable
51
+ * across a long run: `context` reads the client's current ambient context
52
+ * live (null until the client sends one), `isConnected` reports whether the
53
+ * client has an active stream attachment.
54
+ */
55
+ export type Caller = {
56
+ readonly context: Record<string, unknown> | null;
57
+ readonly isConnected: boolean;
58
+ };
59
+
49
60
  export type HandledCommands<THandlers extends Statewire.Commands> = {
50
61
  [K in keyof THandlers]: (
51
62
  ...args: Parameters<THandlers[K]>
package/src/statewire.ts CHANGED
@@ -41,8 +41,16 @@ export const useStatewire = <
41
41
  getState: core.getState as () => TState,
42
42
  connection,
43
43
  commands: core.commands as Statewire.CommandsProxy<TCommands>,
44
+ setContext: (contextValue: Record<string, unknown>) => {
45
+ if (!value.setContext) {
46
+ throw new Error(
47
+ "statewire: this transport has no client context channel",
48
+ );
49
+ }
50
+ return value.setContext(contextValue);
51
+ },
44
52
  }),
45
- [state, connection, core],
53
+ [state, connection, core, value],
46
54
  );
47
55
  };
48
56
 
@@ -80,6 +88,10 @@ export class StatewireClient<
80
88
  return this.root.getValue().commands;
81
89
  }
82
90
 
91
+ setContext(value: Record<string, unknown>): Promise<unknown> {
92
+ return this.root.getValue().setContext(value);
93
+ }
94
+
83
95
  subscribe(listener: () => void): () => void {
84
96
  return this.root.subscribe(listener);
85
97
  }
@@ -120,6 +132,8 @@ export namespace StatewireClient {
120
132
  getState: () => TState;
121
133
  connection: Connection;
122
134
  commands: Statewire.CommandsProxy<TCommands>;
135
+ /** Replace this client's ambient context on the server; throws on transports without a client context channel. */
136
+ setContext: (value: Record<string, unknown>) => Promise<unknown>;
123
137
  };
124
138
 
125
139
  export type Connection = ConnectionState & { reconnect: () => void };
@@ -1,7 +1,11 @@
1
1
  import { useEffect } from "react";
2
2
  import { useStatewireEmit } from "../host/host";
3
3
  import type { Statewire } from "../types";
4
- import { setTransport, useStatewireSession } from "../client/session";
4
+ import {
5
+ setContext,
6
+ setTransport,
7
+ useStatewireSession,
8
+ } from "../client/session";
5
9
  import type { StatewireSessionStoreOptions } from "../client/sessionStore";
6
10
  import type { StatewireClient } from "../statewire";
7
11
  import type { RemoteLink, StatewireRemoteProps } from "./sse";
@@ -26,5 +30,6 @@ export const useStatewireRemote = <
26
30
  emit,
27
31
  connection: conn.connection,
28
32
  source: s.source,
33
+ setContext: (value) => setContext(s, value),
29
34
  };
30
35
  };
@@ -32,10 +32,24 @@ export type StatewireRemoteProps = {
32
32
  onFinish: (args: StatewireHost.Fin) => void;
33
33
  };
34
34
 
35
- export type StatewireWireCommand = {
36
- method: string;
37
- params: readonly unknown[];
38
- };
35
+ export type StatewireWireCommand =
36
+ | { method: string; params: readonly unknown[]; context?: never }
37
+ | { method?: never; params?: never; context: Record<string, unknown> };
38
+
39
+ /** The wire shape of a client context submission: one root replace op. */
40
+ export const contextOps = (value: Record<string, unknown>) => [
41
+ { op: "replace", path: [], value },
42
+ ];
43
+
44
+ export const encodeWireCommand = (command: StatewireWireCommand) => ({
45
+ ...(command.method !== undefined && {
46
+ method: command.method,
47
+ ...encodeParams(command.params),
48
+ }),
49
+ ...(command.context !== undefined && {
50
+ context: contextOps(command.context),
51
+ }),
52
+ });
39
53
 
40
54
  export type StatewireSubmitResult =
41
55
  | { type: "receipt"; response?: StatewireResponse }
@@ -184,10 +198,7 @@ const sendCommand = async (
184
198
  "Statewire-Command-Seq": String(seq),
185
199
  ...(c.lease !== null && { "Statewire-Lease": c.lease }),
186
200
  }),
187
- body: JSON.stringify({
188
- method: command.method,
189
- ...encodeParams(command.params),
190
- }),
201
+ body: JSON.stringify(encodeWireCommand(command)),
191
202
  signal: AbortSignal.any([
192
203
  c.lifetime?.signal ?? AbortSignal.abort(),
193
204
  timeout,
@@ -20,8 +20,8 @@ export const applyOptimistic = <TState>(
20
20
  export type MockTransport<TState> = {
21
21
  /** Pass as the `transport:` option of useStatewire / Harness. */
22
22
  transport: ResourceElement<Statewire<TState | undefined, any, any>>;
23
- /** Captured `{ method, params }` command bodies, in POST order. */
24
- commands: { method: string; params: unknown[] }[];
23
+ /** Captured `/commands` POST bodies, in POST order. */
24
+ commands: { method?: string; params?: unknown[]; context?: unknown }[];
25
25
  /** Replace the replicated state and broadcast it as a root snapshot op. */
26
26
  setState: (state: TState) => Promise<void>;
27
27
  /** Deliver an ack watermark on the stream. */
@@ -49,7 +49,7 @@ export const createMockTransport = <TState>(
49
49
  let finished = false;
50
50
  const clients = new Map<string, number>();
51
51
  const streams = new Set<FakeStreamHandle>();
52
- const commands: { method: string; params: unknown[] }[] = [];
52
+ const commands: MockTransport<unknown>["commands"] = [];
53
53
 
54
54
  const sendEnvelope = (envelope: StatewireEnvelope) => {
55
55
  for (const stream of streams) stream.send(sseFrame(envelope));
@@ -86,7 +86,9 @@ export const createMockTransport = <TState>(
86
86
  const seq = Number(headers.get("Statewire-Command-Seq"));
87
87
  clients.set(id, Math.max(clients.get(id) ?? 0, seq));
88
88
  commands.push(
89
- JSON.parse(String(init?.body)) as { method: string; params: unknown[] },
89
+ JSON.parse(
90
+ String(init?.body),
91
+ ) as MockTransport<unknown>["commands"][number],
90
92
  );
91
93
  return Response.json({});
92
94
  }
@@ -1,7 +1,6 @@
1
1
  import { useState } from "react";
2
2
  import { flushTapSync, resource } from "@assistant-ui/tap";
3
3
  import type { ResourceElement } from "@assistant-ui/tap";
4
- import { encodeParams } from "../wire/meta";
5
4
  import { timing } from "../client/timing";
6
5
  import {
7
6
  handleEnvelope,
@@ -13,6 +12,7 @@ import type { Statewire } from "../types";
13
12
  import type { StatewireClient } from "../statewire";
14
13
  import { useStatewireRemote } from "./remote";
15
14
  import { CLIENT_QUERY_PARAM } from "../host/adapter";
15
+ import { encodeWireCommand } from "./sse";
16
16
  import type {
17
17
  RemoteLink,
18
18
  StatewireSubmitResult,
@@ -206,13 +206,7 @@ const sendCommand = (
206
206
  }, timing.heartbeatTimeoutMs);
207
207
  w.pending.push(entry);
208
208
  try {
209
- ws.send(
210
- JSON.stringify({
211
- method: command.method,
212
- ...encodeParams(command.params),
213
- seq,
214
- }),
215
- );
209
+ ws.send(JSON.stringify({ ...encodeWireCommand(command), seq }));
216
210
  } catch {
217
211
  clearTimeout(timer);
218
212
  w.pending = w.pending.filter((p) => p !== entry);
package/src/types.ts CHANGED
@@ -56,6 +56,8 @@ export interface Statewire<
56
56
  emit?: (event: TEvent) => void;
57
57
  connection?: StatewireClient.Connection;
58
58
  source?: Statewire.StateSource;
59
+ /** Replace this client's ambient context on the server; absent on transports without a client context channel. */
60
+ setContext?: (value: Record<string, unknown>) => Promise<unknown>;
59
61
  }
60
62
 
61
63
  const describe = (failure: StatewireClient.SendFailure): string => {