statewire 0.0.2 → 0.0.4

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 (48) hide show
  1. package/dist/client/connection.d.ts +1 -1
  2. package/dist/client/session.d.ts +7 -4
  3. package/dist/client/session.d.ts.map +1 -1
  4. package/dist/client/session.js +15 -8
  5. package/dist/client/session.js.map +1 -1
  6. package/dist/host/host.d.ts +3 -2
  7. package/dist/host/host.d.ts.map +1 -1
  8. package/dist/host/host.js +5 -1
  9. package/dist/host/host.js.map +1 -1
  10. package/dist/host/socket.d.ts.map +1 -1
  11. package/dist/host/socket.js +58 -6
  12. package/dist/host/socket.js.map +1 -1
  13. package/dist/host.d.ts +11 -1
  14. package/dist/host.d.ts.map +1 -1
  15. package/dist/index.d.ts +1 -1
  16. package/dist/statewire.d.ts +19 -1
  17. package/dist/statewire.d.ts.map +1 -1
  18. package/dist/statewire.js +10 -2
  19. package/dist/statewire.js.map +1 -1
  20. package/dist/transports/fixtures.d.ts +1 -1
  21. package/dist/transports/remote.d.ts +1 -1
  22. package/dist/transports/remote.d.ts.map +1 -1
  23. package/dist/transports/remote.js +3 -2
  24. package/dist/transports/remote.js.map +1 -1
  25. package/dist/transports/sse.d.ts +26 -19
  26. package/dist/transports/sse.d.ts.map +1 -1
  27. package/dist/transports/sse.js +15 -5
  28. package/dist/transports/sse.js.map +1 -1
  29. package/dist/transports/testing.d.ts +5 -4
  30. package/dist/transports/testing.d.ts.map +1 -1
  31. package/dist/transports/testing.js.map +1 -1
  32. package/dist/transports/ws.d.ts.map +1 -1
  33. package/dist/transports/ws.js +2 -3
  34. package/dist/transports/ws.js.map +1 -1
  35. package/dist/types.d.ts +2 -0
  36. package/dist/types.d.ts.map +1 -1
  37. package/dist/types.js.map +1 -1
  38. package/package.json +1 -1
  39. package/src/client/session.ts +43 -21
  40. package/src/host/host.ts +15 -4
  41. package/src/host/socket.ts +85 -9
  42. package/src/host.ts +11 -0
  43. package/src/statewire.ts +27 -2
  44. package/src/transports/remote.ts +6 -1
  45. package/src/transports/sse.ts +20 -20
  46. package/src/transports/testing.ts +6 -4
  47. package/src/transports/ws.ts +2 -8
  48. package/src/types.ts +2 -0
@@ -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","StatewireSubmitResult","type","response","message","ConnectionState","status","cause","attempt","nextRetryAt","lastError","payload","RemoteLink","connection","Connection","send","seq","command","Promise","baseUrl","options","SSEOptions","url","replace","requestHeaders","extra","Record","headers","merged","Headers","name","value","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","overloaded","ok","body","Error","getReader","aborted","cancel","catch","get","sendCommand","String","lease","stringify","lifetime","json","undefined","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;\n params: readonly unknown[];\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 =\n | { status: \"connecting\" }\n | { status: \"live\" }\n | {\n status: \"reconnecting\";\n cause: \"dropped\" | \"evicted\" | \"error\";\n attempt: number;\n nextRetryAt: number;\n lastError?: string;\n message?: string;\n }\n | { status: \"gone\"; message?: string; payload?: unknown };\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({\n method: command.method,\n ...encodeParams(command.params),\n }),\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","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,5 +1,5 @@
1
- import { StatewireEnvelope } from "../client/connection.js";
2
1
  import { Statewire } from "../types.js";
2
+ import { StatewireEnvelope } from "../client/connection.js";
3
3
  import { SocketRecorder, commandFrame, createCounterElement, createSocketRecorder, settle } from "./fixtures.js";
4
4
  import { ResourceElement } from "@assistant-ui/tap";
5
5
  //#region src/transports/testing.d.ts
@@ -10,10 +10,11 @@ declare const applyOptimistic: <TState>(state: TState, mutate: (draft: TState) =
10
10
  type MockTransport<TState> = {
11
11
  /** Pass as the `transport:` option of useStatewire / Harness. */
12
12
  transport: ResourceElement<Statewire<TState | undefined, any, any>>;
13
- /** Captured `{ method, params }` command bodies, in POST order. */
13
+ /** Captured `/commands` POST bodies, in POST order. */
14
14
  commands: {
15
- method: string;
16
- params: unknown[];
15
+ method?: string;
16
+ params?: unknown[];
17
+ context?: unknown;
17
18
  }[];
18
19
  /** Replace the replicated state and broadcast it as a root snapshot op. */
19
20
  setState: (state: TState) => Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"testing.d.ts","names":[],"sources":["../../src/transports/testing.ts"],"mappings":";;;;;;cAWa,YAAI;;cAGJ,kBAAmB,QAC9B,OAAO,QACP,SAAS,OAAO,oBACf;KAES,cAAc;;EAExB,WAAW,gBAAgB,UAAU;;EAErC;IAAY;IAAgB;;;EAE5B,WAAW,OAAO,WAAW;;EAE7B,MAAM,gBAAgB;;EAEtB,eAAe,UAAU,sBAAsB;;EAE/C,SAAS,gBAAgB,qBAAqB;WACrC;WACA;;cAUE,sBAAuB,QAClC,cAAc,WACb,cAAc"}
1
+ {"version":3,"file":"testing.d.ts","names":[],"sources":["../../src/transports/testing.ts"],"mappings":";;;;;;cAWa,YAAI;;cAGJ,kBAAmB,QAC9B,OAAO,QACP,SAAS,OAAO,oBACf;KAES,cAAc;;EAExB,WAAW,gBAAgB,UAAU;;EAErC;IAAY;IAAiB;IAAoB;;;EAEjD,WAAW,OAAO,WAAW;;EAE7B,MAAM,gBAAgB;;EAEtB,eAAe,UAAU,sBAAsB;;EAE/C,SAAS,gBAAgB,qBAAqB;WACrC;WACA;;cAUE,sBAAuB,QAClC,cAAc,WACb,cAAc"}
@@ -1 +1 @@
1
- {"version":3,"file":"testing.js","names":["ResourceElement","StatewireEnvelope","recordMutations","StatewireSSE","Statewire","openSSEStream","sseFrame","FakeStreamHandle","tick","Promise","r","setTimeout","applyOptimistic","state","TState","mutate","draft","MockTransport","transport","commands","method","params","setState","ack","seq","sendEnvelope","envelope","finish","reason","message","clientId","attached","mockCount","createMockTransport","initialState","finished","clients","Map","streams","Set","stream","send","fakeFetch","input","RequestInfo","URL","init","RequestInit","Response","TypeError","url","String","headers","Headers","id","get","endsWith","lastSeq","set","handle","ops","op","path","value","syn","lease","signal","undefined","Number","Math","max","push","JSON","parse","body","json","status","fetch","next","fin","close","size"],"sources":["../../src/transports/testing.ts"],"sourcesContent":["import type { ResourceElement } from \"@assistant-ui/tap\";\nimport type { StatewireEnvelope } from \"../client/connection\";\nimport { recordMutations } from \"../util/recordMutations\";\nimport { StatewireSSE } from \"./sse\";\nimport type { Statewire } from \"../types\";\nimport { openSSEStream, sseFrame } from \"./fake-sse\";\nimport type { FakeStreamHandle } from \"./fake-sse\";\n\nexport * from \"./fixtures\";\n\n/** Let queued microtasks and op batches drain through the statewire client. */\nexport const tick = () => new Promise<void>((r) => setTimeout(r, 0));\n\n/** Run a mutator against a recording draft, as the client does for `optimistic`. */\nexport const applyOptimistic = <TState>(\n state: TState,\n mutate: (draft: TState) => void,\n): TState => recordMutations(state, mutate).state;\n\nexport type MockTransport<TState> = {\n /** Pass as the `transport:` option of useStatewire / Harness. */\n transport: ResourceElement<Statewire<TState | undefined, any, any>>;\n /** Captured `{ method, params }` command bodies, in POST order. */\n commands: { method: string; params: unknown[] }[];\n /** Replace the replicated state and broadcast it as a root snapshot op. */\n setState: (state: TState) => Promise<void>;\n /** Deliver an ack watermark on the stream. */\n ack: (seq: number) => Promise<void>;\n /** Deliver an arbitrary envelope on the stream. */\n sendEnvelope: (envelope: StatewireEnvelope) => Promise<void>;\n /** End the stream with a fin; further attaches fail like a dead server. */\n finish: (reason: string, message?: string) => Promise<void>;\n readonly clientId: string;\n readonly attached: boolean;\n};\n\n/**\n * A mock statewire backend: serves `initialState` as the attach snapshot,\n * records command POSTs (receipt only — acks and results are delivered from\n * test code), and exposes the wire as a transport ResourceElement.\n */\nlet mockCount = 0;\n\nexport const createMockTransport = <TState>(\n initialState: TState,\n): MockTransport<TState> => {\n let state: unknown = initialState;\n let clientId = \"\";\n let finished = false;\n const clients = new Map<string, number>();\n const streams = new Set<FakeStreamHandle>();\n const commands: { method: string; params: unknown[] }[] = [];\n\n const sendEnvelope = (envelope: StatewireEnvelope) => {\n for (const stream of streams) stream.send(sseFrame(envelope));\n return tick();\n };\n\n const fakeFetch = (async (\n input: RequestInfo | URL,\n init?: RequestInit,\n ): Promise<Response> => {\n if (finished) throw new TypeError(\"fetch failed (finished)\");\n const url = String(input);\n const headers = new Headers(init?.headers);\n const id = headers.get(\"Statewire-Client-Id\") ?? \"\";\n if (url.endsWith(\"/stream\")) {\n clientId = id;\n const lastSeq = clients.get(id) ?? 0;\n clients.set(id, lastSeq);\n return openSSEStream(\n streams,\n id,\n (handle) => {\n handle.send(\n sseFrame({\n ops: [{ op: \"replace\", path: [], value: state }],\n syn: { lastSeq, lease: \"mock-lease\" },\n }),\n );\n },\n init?.signal ?? undefined,\n );\n }\n if (url.endsWith(\"/commands\")) {\n const seq = Number(headers.get(\"Statewire-Command-Seq\"));\n clients.set(id, Math.max(clients.get(id) ?? 0, seq));\n commands.push(\n JSON.parse(String(init?.body)) as { method: string; params: unknown[] },\n );\n return Response.json({});\n }\n return new Response(null, { status: 404 });\n }) as typeof fetch;\n\n return {\n transport: StatewireSSE({\n url: `http://mock-${++mockCount}/api`,\n fetch: fakeFetch,\n }),\n commands,\n setState: (next) => {\n state = next;\n return sendEnvelope({ ops: [{ op: \"replace\", path: [], value: next }] });\n },\n ack: (seq) => sendEnvelope({ ack: seq }),\n sendEnvelope,\n finish: async (reason, message) => {\n await sendEnvelope({\n fin: { reason, ...(message !== undefined && { message }) },\n });\n finished = true;\n for (const stream of [...streams]) stream.close();\n await tick();\n },\n get clientId() {\n return clientId;\n },\n get attached() {\n return streams.size > 0;\n },\n };\n};\n"],"mappings":";;;;;;AAWA,MAAaQ,aAAa,IAAIC,SAAeC,MAAMC,WAAWD,GAAG,CAAC,CAAC;;AAGnE,MAAaE,mBACXC,OACAE,WACWb,gBAAgBW,OAAOE,MAAM,CAAC,CAACF;;;;;;AAwB5C,IAAImB,YAAY;AAEhB,MAAaC,uBACXC,iBAC0B;CAC1B,IAAIrB,QAAiBqB;CACrB,IAAIJ,WAAW;CACf,IAAIK,WAAW;CACf,MAAMC,0BAAU,IAAIC,IAAoB;CACxC,MAAMC,0BAAU,IAAIC,IAAsB;CAC1C,MAAMpB,WAAoD,CAAA;CAE1D,MAAMM,gBAAgBC,aAAgC;EACpD,KAAK,MAAMc,UAAUF,SAASE,OAAOC,KAAKnC,SAASoB,QAAQ,CAAC;EAC5D,OAAOlB,KAAK;CACd;CAEA,MAAMkC,aAAa,OACjBC,OACAG,SACsB;EACtB,IAAIX,UAAU,MAAM,IAAIc,UAAU,yBAAyB;EAC3D,MAAMC,MAAMC,OAAOR,KAAK;EACxB,MAAMS,UAAU,IAAIC,QAAQP,MAAMM,OAAO;EACzC,MAAME,KAAKF,QAAQG,IAAI,qBAAqB,KAAK;EACjD,IAAIL,IAAIM,SAAS,SAAS,GAAG;GAC3B1B,WAAWwB;GACX,MAAMG,UAAUrB,QAAQmB,IAAID,EAAE,KAAK;GACnClB,QAAQsB,IAAIJ,IAAIG,OAAO;GACvB,OAAOpD,cACLiC,SACAgB,KACCK,WAAW;IACVA,OAAOlB,KACLnC,SAAS;KACPsD,KAAK,CAAC;MAAEC,IAAI;MAAWC,MAAM,CAAA;MAAIC,OAAOlD;KAAM,CAAC;KAC/CmD,KAAK;MAAEP;MAASQ,OAAO;KAAa;IACtC,CAAC,CACH;GACF,GACAnB,MAAMoB,UAAUC,KAAAA,CAClB;EACF;EACA,IAAIjB,IAAIM,SAAS,WAAW,GAAG;GAC7B,MAAMhC,MAAM4C,OAAOhB,QAAQG,IAAI,uBAAuB,CAAC;GACvDnB,QAAQsB,IAAIJ,IAAIe,KAAKC,IAAIlC,QAAQmB,IAAID,EAAE,KAAK,GAAG9B,GAAG,CAAC;GACnDL,SAASoD,KACPC,KAAKC,MAAMtB,OAAOL,MAAM4B,IAAI,CAAC,CAC/B;GACA,OAAO1B,SAAS2B,KAAK,CAAC,CAAC;EACzB;EACA,OAAO,IAAI3B,SAAS,MAAM,EAAE4B,QAAQ,IAAI,CAAC;CAC3C;CAEA,OAAO;EACL1D,WAAWf,aAAa;GACtB+C,KAAK,eAAe,EAAElB,UAAS;GAC/B6C,OAAOnC;EACT,CAAC;EACDvB;EACAG,WAAWwD,SAAS;GAClBjE,QAAQiE;GACR,OAAOrD,aAAa,EAAEmC,KAAK,CAAC;IAAEC,IAAI;IAAWC,MAAM,CAAA;IAAIC,OAAOe;GAAK,CAAC,EAAE,CAAC;EACzE;EACAvD,MAAMC,QAAQC,aAAa,EAAEF,KAAKC,IAAI,CAAC;EACvCC;EACAE,QAAQ,OAAOC,QAAQC,YAAY;GACjC,MAAMJ,aAAa,EACjBsD,KAAK;IAAEnD;IAAQ,GAAIC,YAAYsC,KAAAA,KAAa,EAAEtC,QAAQ;GAAG,EAC3D,CAAC;GACDM,WAAW;GACX,KAAK,MAAMK,UAAU,CAAC,GAAGF,OAAO,GAAGE,OAAOwC,MAAM;GAChD,MAAMxE,KAAK;EACb;EACA,IAAIsB,WAAW;GACb,OAAOA;EACT;EACA,IAAIC,WAAW;GACb,OAAOO,QAAQ2C,OAAO;EACxB;CACF;AACF"}
1
+ {"version":3,"file":"testing.js","names":["ResourceElement","StatewireEnvelope","recordMutations","StatewireSSE","Statewire","openSSEStream","sseFrame","FakeStreamHandle","tick","Promise","r","setTimeout","applyOptimistic","state","TState","mutate","draft","MockTransport","transport","commands","method","params","context","setState","ack","seq","sendEnvelope","envelope","finish","reason","message","clientId","attached","mockCount","createMockTransport","initialState","finished","clients","Map","streams","Set","stream","send","fakeFetch","input","RequestInfo","URL","init","RequestInit","Response","TypeError","url","String","headers","Headers","id","get","endsWith","lastSeq","set","handle","ops","op","path","value","syn","lease","signal","undefined","Number","Math","max","push","JSON","parse","body","json","status","fetch","next","fin","close","size"],"sources":["../../src/transports/testing.ts"],"sourcesContent":["import type { ResourceElement } from \"@assistant-ui/tap\";\nimport type { StatewireEnvelope } from \"../client/connection\";\nimport { recordMutations } from \"../util/recordMutations\";\nimport { StatewireSSE } from \"./sse\";\nimport type { Statewire } from \"../types\";\nimport { openSSEStream, sseFrame } from \"./fake-sse\";\nimport type { FakeStreamHandle } from \"./fake-sse\";\n\nexport * from \"./fixtures\";\n\n/** Let queued microtasks and op batches drain through the statewire client. */\nexport const tick = () => new Promise<void>((r) => setTimeout(r, 0));\n\n/** Run a mutator against a recording draft, as the client does for `optimistic`. */\nexport const applyOptimistic = <TState>(\n state: TState,\n mutate: (draft: TState) => void,\n): TState => recordMutations(state, mutate).state;\n\nexport type MockTransport<TState> = {\n /** Pass as the `transport:` option of useStatewire / Harness. */\n transport: ResourceElement<Statewire<TState | undefined, any, any>>;\n /** Captured `/commands` POST bodies, in POST order. */\n commands: { method?: string; params?: unknown[]; context?: unknown }[];\n /** Replace the replicated state and broadcast it as a root snapshot op. */\n setState: (state: TState) => Promise<void>;\n /** Deliver an ack watermark on the stream. */\n ack: (seq: number) => Promise<void>;\n /** Deliver an arbitrary envelope on the stream. */\n sendEnvelope: (envelope: StatewireEnvelope) => Promise<void>;\n /** End the stream with a fin; further attaches fail like a dead server. */\n finish: (reason: string, message?: string) => Promise<void>;\n readonly clientId: string;\n readonly attached: boolean;\n};\n\n/**\n * A mock statewire backend: serves `initialState` as the attach snapshot,\n * records command POSTs (receipt only — acks and results are delivered from\n * test code), and exposes the wire as a transport ResourceElement.\n */\nlet mockCount = 0;\n\nexport const createMockTransport = <TState>(\n initialState: TState,\n): MockTransport<TState> => {\n let state: unknown = initialState;\n let clientId = \"\";\n let finished = false;\n const clients = new Map<string, number>();\n const streams = new Set<FakeStreamHandle>();\n const commands: MockTransport<unknown>[\"commands\"] = [];\n\n const sendEnvelope = (envelope: StatewireEnvelope) => {\n for (const stream of streams) stream.send(sseFrame(envelope));\n return tick();\n };\n\n const fakeFetch = (async (\n input: RequestInfo | URL,\n init?: RequestInit,\n ): Promise<Response> => {\n if (finished) throw new TypeError(\"fetch failed (finished)\");\n const url = String(input);\n const headers = new Headers(init?.headers);\n const id = headers.get(\"Statewire-Client-Id\") ?? \"\";\n if (url.endsWith(\"/stream\")) {\n clientId = id;\n const lastSeq = clients.get(id) ?? 0;\n clients.set(id, lastSeq);\n return openSSEStream(\n streams,\n id,\n (handle) => {\n handle.send(\n sseFrame({\n ops: [{ op: \"replace\", path: [], value: state }],\n syn: { lastSeq, lease: \"mock-lease\" },\n }),\n );\n },\n init?.signal ?? undefined,\n );\n }\n if (url.endsWith(\"/commands\")) {\n const seq = Number(headers.get(\"Statewire-Command-Seq\"));\n clients.set(id, Math.max(clients.get(id) ?? 0, seq));\n commands.push(\n JSON.parse(\n String(init?.body),\n ) as MockTransport<unknown>[\"commands\"][number],\n );\n return Response.json({});\n }\n return new Response(null, { status: 404 });\n }) as typeof fetch;\n\n return {\n transport: StatewireSSE({\n url: `http://mock-${++mockCount}/api`,\n fetch: fakeFetch,\n }),\n commands,\n setState: (next) => {\n state = next;\n return sendEnvelope({ ops: [{ op: \"replace\", path: [], value: next }] });\n },\n ack: (seq) => sendEnvelope({ ack: seq }),\n sendEnvelope,\n finish: async (reason, message) => {\n await sendEnvelope({\n fin: { reason, ...(message !== undefined && { message }) },\n });\n finished = true;\n for (const stream of [...streams]) stream.close();\n await tick();\n },\n get clientId() {\n return clientId;\n },\n get attached() {\n return streams.size > 0;\n },\n };\n};\n"],"mappings":";;;;;;AAWA,MAAaQ,aAAa,IAAIC,SAAeC,MAAMC,WAAWD,GAAG,CAAC,CAAC;;AAGnE,MAAaE,mBACXC,OACAE,WACWb,gBAAgBW,OAAOE,MAAM,CAAC,CAACF;;;;;;AAwB5C,IAAIoB,YAAY;AAEhB,MAAaC,uBACXC,iBAC0B;CAC1B,IAAItB,QAAiBsB;CACrB,IAAIJ,WAAW;CACf,IAAIK,WAAW;CACf,MAAMC,0BAAU,IAAIC,IAAoB;CACxC,MAAMC,0BAAU,IAAIC,IAAsB;CAC1C,MAAMrB,WAA+C,CAAA;CAErD,MAAMO,gBAAgBC,aAAgC;EACpD,KAAK,MAAMc,UAAUF,SAASE,OAAOC,KAAKpC,SAASqB,QAAQ,CAAC;EAC5D,OAAOnB,KAAK;CACd;CAEA,MAAMmC,aAAa,OACjBC,OACAG,SACsB;EACtB,IAAIX,UAAU,MAAM,IAAIc,UAAU,yBAAyB;EAC3D,MAAMC,MAAMC,OAAOR,KAAK;EACxB,MAAMS,UAAU,IAAIC,QAAQP,MAAMM,OAAO;EACzC,MAAME,KAAKF,QAAQG,IAAI,qBAAqB,KAAK;EACjD,IAAIL,IAAIM,SAAS,SAAS,GAAG;GAC3B1B,WAAWwB;GACX,MAAMG,UAAUrB,QAAQmB,IAAID,EAAE,KAAK;GACnClB,QAAQsB,IAAIJ,IAAIG,OAAO;GACvB,OAAOrD,cACLkC,SACAgB,KACCK,WAAW;IACVA,OAAOlB,KACLpC,SAAS;KACPuD,KAAK,CAAC;MAAEC,IAAI;MAAWC,MAAM,CAAA;MAAIC,OAAOnD;KAAM,CAAC;KAC/CoD,KAAK;MAAEP;MAASQ,OAAO;KAAa;IACtC,CAAC,CACH;GACF,GACAnB,MAAMoB,UAAUC,KAAAA,CAClB;EACF;EACA,IAAIjB,IAAIM,SAAS,WAAW,GAAG;GAC7B,MAAMhC,MAAM4C,OAAOhB,QAAQG,IAAI,uBAAuB,CAAC;GACvDnB,QAAQsB,IAAIJ,IAAIe,KAAKC,IAAIlC,QAAQmB,IAAID,EAAE,KAAK,GAAG9B,GAAG,CAAC;GACnDN,SAASqD,KACPC,KAAKC,MACHtB,OAAOL,MAAM4B,IAAI,CACnB,CACF;GACA,OAAO1B,SAAS2B,KAAK,CAAC,CAAC;EACzB;EACA,OAAO,IAAI3B,SAAS,MAAM,EAAE4B,QAAQ,IAAI,CAAC;CAC3C;CAEA,OAAO;EACL3D,WAAWf,aAAa;GACtBgD,KAAK,eAAe,EAAElB,UAAS;GAC/B6C,OAAOnC;EACT,CAAC;EACDxB;EACAI,WAAWwD,SAAS;GAClBlE,QAAQkE;GACR,OAAOrD,aAAa,EAAEmC,KAAK,CAAC;IAAEC,IAAI;IAAWC,MAAM,CAAA;IAAIC,OAAOe;GAAK,CAAC,EAAE,CAAC;EACzE;EACAvD,MAAMC,QAAQC,aAAa,EAAEF,KAAKC,IAAI,CAAC;EACvCC;EACAE,QAAQ,OAAOC,QAAQC,YAAY;GACjC,MAAMJ,aAAa,EACjBsD,KAAK;IAAEnD;IAAQ,GAAIC,YAAYsC,KAAAA,KAAa,EAAEtC,QAAQ;GAAG,EAC3D,CAAC;GACDM,WAAW;GACX,KAAK,MAAMK,UAAU,CAAC,GAAGF,OAAO,GAAGE,OAAOwC,MAAM;GAChD,MAAMzE,KAAK;EACb;EACA,IAAIuB,WAAW;GACb,OAAOA;EACT;EACA,IAAIC,WAAW;GACb,OAAOO,QAAQ2C,OAAO;EACxB;CACF;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"ws.d.ts","names":[],"sources":["../../src/transports/ws.ts"],"mappings":";;;;cAmCa;cAsNA,cACX,cACA,kBAAkB,UAAU,WAAW,UAAU,UACjD,cAEA,SAAS,gBAAgB,cACtB,gBAAgB,UAAU,oBAAoB,WAAW"}
1
+ {"version":3,"file":"ws.d.ts","names":[],"sources":["../../src/transports/ws.ts"],"mappings":";;;;cAmCa;cAgNA,cACX,cACA,kBAAkB,UAAU,WAAW,UAAU,UACjD,cAEA,SAAS,gBAAgB,cACtB,gBAAgB,UAAU,oBAAoB,WAAW"}
@@ -1,8 +1,8 @@
1
- import { encodeParams } from "../wire/meta.js";
2
1
  import { CLIENT_QUERY_PARAM } from "../host/adapter.js";
3
2
  import { timing } from "../client/timing.js";
4
3
  import { handleEnvelope, overloadedError, useStatewireConn } from "../client/connection.js";
5
4
  import { useStatewireRemote } from "./remote.js";
5
+ import { encodeWireCommand } from "./sse.js";
6
6
  import { useState } from "@assistant-ui/tap/react-shim";
7
7
  import { flushTapSync, resource } from "@assistant-ui/tap";
8
8
  //#region src/transports/ws.ts
@@ -131,8 +131,7 @@ const sendCommand = (w, { seq, command }) => {
131
131
  w.pending.push(entry);
132
132
  try {
133
133
  ws.send(JSON.stringify({
134
- method: command.method,
135
- ...encodeParams(command.params),
134
+ ...encodeWireCommand(command),
136
135
  seq
137
136
  }));
138
137
  } catch {
@@ -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.4",
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 = {