statewire 0.0.1 → 0.0.2

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 (148) hide show
  1. package/README.md +14 -13
  2. package/dist/client/changes.d.ts +2 -2
  3. package/dist/client/changes.d.ts.map +1 -1
  4. package/dist/client/changes.js +3 -10
  5. package/dist/client/changes.js.map +1 -1
  6. package/dist/client/connection.d.ts +18 -39
  7. package/dist/client/connection.d.ts.map +1 -1
  8. package/dist/client/connection.js +61 -54
  9. package/dist/client/connection.js.map +1 -1
  10. package/dist/client/core.d.ts +7 -11
  11. package/dist/client/core.d.ts.map +1 -1
  12. package/dist/client/core.js +58 -55
  13. package/dist/client/core.js.map +1 -1
  14. package/dist/client/session.d.ts +8 -25
  15. package/dist/client/session.d.ts.map +1 -1
  16. package/dist/client/session.js +56 -36
  17. package/dist/client/session.js.map +1 -1
  18. package/dist/client/sessionStore.d.ts +1 -1
  19. package/dist/client/sessionStore.js.map +1 -1
  20. package/dist/client/timing.d.ts +2 -1
  21. package/dist/client/timing.d.ts.map +1 -1
  22. package/dist/client/timing.js +5 -1
  23. package/dist/client/timing.js.map +1 -1
  24. package/dist/host/adapter.d.ts +8 -0
  25. package/dist/host/adapter.d.ts.map +1 -0
  26. package/dist/host/adapter.js +23 -0
  27. package/dist/host/adapter.js.map +1 -0
  28. package/dist/host/draft.d.ts +21 -7
  29. package/dist/host/draft.d.ts.map +1 -1
  30. package/dist/host/draft.js +190 -103
  31. package/dist/host/draft.js.map +1 -1
  32. package/dist/host/host.d.ts +18 -35
  33. package/dist/host/host.d.ts.map +1 -1
  34. package/dist/host/host.js +81 -53
  35. package/dist/host/host.js.map +1 -1
  36. package/dist/host/socket.d.ts +14 -0
  37. package/dist/host/socket.d.ts.map +1 -0
  38. package/dist/host/socket.js +459 -0
  39. package/dist/host/socket.js.map +1 -0
  40. package/dist/host-internal.d.ts +27 -0
  41. package/dist/host-internal.d.ts.map +1 -0
  42. package/dist/host-internal.js +3 -0
  43. package/dist/host.d.ts +44 -0
  44. package/dist/host.d.ts.map +1 -0
  45. package/dist/host.js +4 -0
  46. package/dist/index.d.ts +5 -28
  47. package/dist/index.js +2 -3
  48. package/dist/statewire.d.ts +94 -10
  49. package/dist/statewire.d.ts.map +1 -1
  50. package/dist/statewire.js +23 -35
  51. package/dist/statewire.js.map +1 -1
  52. package/dist/transports/fake-sse.d.ts +12 -0
  53. package/dist/transports/fake-sse.d.ts.map +1 -0
  54. package/dist/transports/fake-sse.js +39 -0
  55. package/dist/transports/fake-sse.js.map +1 -0
  56. package/dist/transports/fixtures.d.ts +33 -0
  57. package/dist/transports/fixtures.d.ts.map +1 -0
  58. package/dist/transports/fixtures.js +50 -0
  59. package/dist/transports/fixtures.js.map +1 -0
  60. package/dist/transports/remote.d.ts +6 -13
  61. package/dist/transports/remote.d.ts.map +1 -1
  62. package/dist/transports/remote.js +5 -24
  63. package/dist/transports/remote.js.map +1 -1
  64. package/dist/transports/sse.d.ts +10 -20
  65. package/dist/transports/sse.d.ts.map +1 -1
  66. package/dist/transports/sse.js +42 -41
  67. package/dist/transports/sse.js.map +1 -1
  68. package/dist/transports/testing.d.ts +4 -3
  69. package/dist/transports/testing.d.ts.map +1 -1
  70. package/dist/transports/testing.js +19 -46
  71. package/dist/transports/testing.js.map +1 -1
  72. package/dist/transports/ws.d.ts +4 -13
  73. package/dist/transports/ws.d.ts.map +1 -1
  74. package/dist/transports/ws.js +57 -60
  75. package/dist/transports/ws.js.map +1 -1
  76. package/dist/types.d.ts +53 -100
  77. package/dist/types.d.ts.map +1 -1
  78. package/dist/types.js.map +1 -1
  79. package/dist/util/draftTraps.d.ts +6 -0
  80. package/dist/util/draftTraps.d.ts.map +1 -0
  81. package/dist/util/draftTraps.js +25 -0
  82. package/dist/util/draftTraps.js.map +1 -0
  83. package/dist/util/macrotask.d.ts +5 -0
  84. package/dist/util/macrotask.d.ts.map +1 -0
  85. package/dist/util/macrotask.js +25 -0
  86. package/dist/util/macrotask.js.map +1 -0
  87. package/dist/util/methodProxy.d.ts +5 -0
  88. package/dist/util/methodProxy.d.ts.map +1 -0
  89. package/dist/util/methodProxy.js +17 -0
  90. package/dist/util/methodProxy.js.map +1 -0
  91. package/dist/util/randomId.d.ts +5 -0
  92. package/dist/util/randomId.d.ts.map +1 -0
  93. package/dist/util/randomId.js +6 -0
  94. package/dist/util/randomId.js.map +1 -0
  95. package/dist/{client/optimistic.d.ts → util/recordMutations.d.ts} +2 -2
  96. package/dist/util/recordMutations.d.ts.map +1 -0
  97. package/dist/{client/optimistic.js → util/recordMutations.js} +5 -24
  98. package/dist/util/recordMutations.js.map +1 -0
  99. package/dist/util/statePath.d.ts +14 -0
  100. package/dist/util/statePath.d.ts.map +1 -0
  101. package/dist/util/statePath.js +31 -0
  102. package/dist/util/statePath.js.map +1 -0
  103. package/dist/wire/meta.d.ts +6 -1
  104. package/dist/wire/meta.d.ts.map +1 -1
  105. package/dist/wire/meta.js +30 -28
  106. package/dist/wire/meta.js.map +1 -1
  107. package/dist/wire/ops.d.ts +5 -26
  108. package/dist/wire/ops.d.ts.map +1 -1
  109. package/dist/wire/ops.js +67 -80
  110. package/dist/wire/ops.js.map +1 -1
  111. package/package.json +15 -11
  112. package/src/client/changes.ts +5 -15
  113. package/src/client/connection.ts +87 -113
  114. package/src/client/core.ts +57 -91
  115. package/src/client/session.ts +87 -70
  116. package/src/client/sessionStore.ts +1 -1
  117. package/src/client/timing.ts +6 -0
  118. package/src/host/adapter.ts +33 -0
  119. package/src/host/draft.ts +293 -125
  120. package/src/host/host.ts +107 -106
  121. package/src/host/socket.ts +542 -0
  122. package/src/host-internal.ts +28 -0
  123. package/src/host.ts +61 -0
  124. package/src/index.ts +2 -62
  125. package/src/statewire.ts +154 -61
  126. package/src/transports/fake-sse.ts +51 -0
  127. package/src/transports/fixtures.ts +60 -0
  128. package/src/transports/remote.ts +12 -54
  129. package/src/transports/sse.ts +68 -81
  130. package/src/transports/testing.ts +27 -59
  131. package/src/transports/ws.ts +76 -92
  132. package/src/types.ts +50 -131
  133. package/src/util/draftTraps.ts +32 -0
  134. package/src/util/macrotask.ts +25 -0
  135. package/src/util/methodProxy.ts +16 -0
  136. package/src/util/randomId.ts +4 -0
  137. package/src/{client/optimistic.ts → util/recordMutations.ts} +0 -0
  138. package/src/util/statePath.ts +61 -0
  139. package/src/wire/meta.ts +42 -47
  140. package/src/wire/ops.ts +107 -106
  141. package/LICENSE +0 -21
  142. package/dist/client/optimistic.d.ts.map +0 -1
  143. package/dist/client/optimistic.js.map +0 -1
  144. package/dist/index.d.ts.map +0 -1
  145. package/dist/server.d.ts +0 -15
  146. package/dist/server.d.ts.map +0 -1
  147. package/dist/server.js +0 -3
  148. package/src/server.ts +0 -32
@@ -1,41 +1,21 @@
1
1
  import { encodeParams, reviveResponse } from "../wire/meta.js";
2
2
  import { createSseParser } from "./sse-parser.js";
3
3
  import { timing } from "../client/timing.js";
4
- import { classify, createGate, handleEnvelope, overloadedError, parseRetryAfter, reconnectConn } from "../client/connection.js";
4
+ import { classify, handleEnvelope, overloadedError, parseRetryAfter, useStatewireConn } from "../client/connection.js";
5
5
  import { useStatewireRemote } from "./remote.js";
6
+ import { useRef, useState } from "@assistant-ui/tap/react-shim";
6
7
  import { flushTapSync, resource } from "@assistant-ui/tap";
7
8
  //#region src/transports/sse.ts
8
- const createConn = (latest, notify) => {
9
- const c = {
10
- hasSnapshot: false,
11
- lease: null,
12
- attempt: 0,
13
- episode: null,
14
- abort: null,
15
- gate: createGate(),
16
- lifetime: null,
17
- latest,
18
- notify,
19
- reconnect: () => reconnectConn(c),
20
- attemptOnce: (abort) => attemptStream(c, abort),
21
- send: (args) => sendCommand(c, args),
22
- connection: {
23
- status: "connecting",
24
- reconnect: () => c.reconnect()
25
- }
26
- };
27
- return c;
28
- };
29
- const baseUrl = (c) => c.latest.current.options.url.replace(/\/$/, "");
30
- const requestHeaders = async (c, extra) => {
31
- const { headers } = c.latest.current.options;
9
+ const baseUrl = (options) => options.url.replace(/\/$/, "");
10
+ const requestHeaders = async (options, extra) => {
11
+ const { headers } = options;
32
12
  const merged = new Headers(typeof headers === "function" ? await headers() : headers);
33
13
  for (const [name, value] of Object.entries(extra)) merged.set(name, value);
34
14
  return merged;
35
15
  };
36
- const timedFetch = async (c, input, init, timeout) => {
16
+ const timedFetch = async (options, input, init, timeout) => {
37
17
  try {
38
- return await (c.latest.current.options.fetch ?? globalThis.fetch)(input, init);
18
+ return await (options.fetch ?? globalThis.fetch)(input, init);
39
19
  } catch {
40
20
  throw classify(timeout);
41
21
  }
@@ -43,7 +23,6 @@ const timedFetch = async (c, input, init, timeout) => {
43
23
  const consume = async (c, reader, heartbeat, dead) => {
44
24
  const decoder = new TextDecoder();
45
25
  const parse = createSseParser();
46
- let attached = false;
47
26
  for (;;) {
48
27
  const timer = setTimeout(() => heartbeat.abort(), timing.heartbeatTimeoutMs);
49
28
  let result;
@@ -58,22 +37,21 @@ const consume = async (c, reader, heartbeat, dead) => {
58
37
  for (const data of parse(decoder.decode(result.value, { stream: true }))) {
59
38
  if (dead()) return "ended";
60
39
  const env = JSON.parse(data);
61
- const done = flushTapSync(() => handleEnvelope(c, env, attached));
62
- attached = true;
40
+ const done = flushTapSync(() => handleEnvelope(c, env));
63
41
  if (done) return done;
64
42
  }
65
43
  }
66
44
  };
67
- const attemptStream = async (c, abort) => {
68
- const headers = await requestHeaders(c, {
69
- "Statewire-Client-Id": c.latest.current.ctx.clientId,
45
+ const attemptStream = async (c, options, abort) => {
46
+ const headers = await requestHeaders(options, {
47
+ "Statewire-Client-Id": c.ctx.clientId,
70
48
  accept: "text/event-stream"
71
49
  });
72
50
  const heartbeat = new AbortController();
73
51
  const headerTimer = setTimeout(() => heartbeat.abort(), timing.heartbeatTimeoutMs);
74
52
  let res;
75
53
  try {
76
- res = await timedFetch(c, `${baseUrl(c)}/stream`, {
54
+ res = await timedFetch(options, `${baseUrl(options)}/stream`, {
77
55
  headers,
78
56
  cache: "no-store",
79
57
  signal: AbortSignal.any([abort.signal, heartbeat.signal])
@@ -91,13 +69,13 @@ const attemptStream = async (c, abort) => {
91
69
  }
92
70
  };
93
71
  const overloaded = (res) => overloadedError(parseRetryAfter(res.headers.get("Retry-After")));
94
- const sendCommand = async (c, { seq, command }) => {
72
+ const sendCommand = async (c, options, { seq, command }) => {
95
73
  const timeout = AbortSignal.timeout(timing.heartbeatTimeoutMs);
96
- const res = await timedFetch(c, `${baseUrl(c)}/commands`, {
74
+ const res = await timedFetch(options, `${baseUrl(options)}/commands`, {
97
75
  method: "POST",
98
- headers: await requestHeaders(c, {
76
+ headers: await requestHeaders(options, {
99
77
  "content-type": "application/json",
100
- "Statewire-Client-Id": c.latest.current.ctx.clientId,
78
+ "Statewire-Client-Id": c.ctx.clientId,
101
79
  "Statewire-Command-Seq": String(seq),
102
80
  ...c.lease !== null && { "Statewire-Lease": c.lease }
103
81
  }),
@@ -108,7 +86,15 @@ const sendCommand = async (c, { seq, command }) => {
108
86
  signal: AbortSignal.any([c.lifetime?.signal ?? AbortSignal.abort(), timeout])
109
87
  }, timeout);
110
88
  if (res.ok) {
111
- const body = await res.json().catch(() => null);
89
+ let body;
90
+ try {
91
+ body = await res.json();
92
+ } catch {
93
+ return {
94
+ type: "rejected",
95
+ message: "statewire: protocol error: malformed /commands receipt body"
96
+ };
97
+ }
112
98
  return body?.res !== void 0 ? {
113
99
  type: "receipt",
114
100
  response: reviveResponse(body.res)
@@ -126,9 +112,24 @@ const sendCommand = async (c, { seq, command }) => {
126
112
  };
127
113
  throw new Error(`HTTP ${res.status}`);
128
114
  };
129
- const useStatewireSSE = (options) => {
130
- return useStatewireRemote(options, createConn);
115
+ const useSseLink = (options, ctx) => {
116
+ const latest = useRef(options);
117
+ latest.current = options;
118
+ const c_0 = useStatewireConn({
119
+ url: options.url,
120
+ onChange: options.onChange,
121
+ ctx,
122
+ attemptOnce: (c, abort) => attemptStream(c, options, abort)
123
+ });
124
+ const [link] = useState(() => ({
125
+ get connection() {
126
+ return c_0.connection;
127
+ },
128
+ send: (args) => sendCommand(c_0, latest.current, args)
129
+ }));
130
+ return link;
131
131
  };
132
+ const useStatewireSSE = (options) => useStatewireRemote(options, useSseLink);
132
133
  const StatewireSSE = resource(useStatewireSSE);
133
134
  //#endregion
134
135
  export { StatewireSSE };
@@ -1 +1 @@
1
- {"version":3,"file":"sse.js","names":["flushTapSync","resource","ResourceElement","encodeParams","reviveResponse","createSseParser","timing","classify","createGate","handleEnvelope","overloadedError","parseRetryAfter","reconnectConn","ConnBase","StatewireEnvelope","StatewireFinish","StatewireResponse","StatewireTransport","useStatewireRemote","StatewireSessionStore","StatewireCommands","StatewireTransportProps","clientId","onSnapshot","args","ops","lastSeq","onOps","res","ack","onFinish","StatewireWireCommand","method","params","StatewireSubmitResult","type","response","message","payload","ConnectionState","status","cause","attempt","nextRetryAt","lastError","StatewireConnection","reconnect","RemoteLink","connection","send","seq","command","Promise","StatewireSSEOptions","url","headers","HeadersInit","fetch","globalThis","onChange","sessionStore","Latest","options","ctx","Conn","latest","current","createConn","notify","c","hasSnapshot","lease","episode","abort","gate","lifetime","attemptOnce","attemptStream","sendCommand","baseUrl","replace","requestHeaders","extra","Record","merged","Headers","name","value","Object","entries","set","timedFetch","input","init","RequestInit","signal","AbortSignal","timeout","Response","consume","reader","ReadableStreamDefaultReader","Uint8Array","heartbeat","AbortController","dead","decoder","TextDecoder","parse","attached","timer","setTimeout","heartbeatTimeoutMs","result","ReadableStreamReadResult","read","clearTimeout","done","data","decode","stream","env","JSON","accept","headerTimer","cache","any","overloaded","ok","body","Error","getReader","aborted","cancel","catch","get","String","stringify","json","undefined","verdict","const","error","useStatewireSSE","StatewireSSE","TState","TCommands","TEvent"],"sources":["../../src/transports/sse.ts"],"sourcesContent":["import { 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 createGate,\n handleEnvelope,\n overloadedError,\n parseRetryAfter,\n reconnectConn,\n} from \"../client/connection\";\nimport type {\n ConnBase,\n StatewireEnvelope,\n StatewireFinish,\n StatewireResponse,\n} from \"../client/connection\";\nimport type { StatewireTransport } from \"../types\";\nimport { useStatewireRemote } from \"./remote\";\nimport type { StatewireSessionStore } from \"../client/sessionStore\";\nimport type { StatewireCommands } from \"../types\";\n\nexport type StatewireTransportProps = {\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: StatewireFinish) => 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; payload?: unknown }\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\" | \"ended\" | \"error\";\n attempt: number;\n nextRetryAt: number;\n lastError?: string;\n message?: string;\n }\n | { status: \"gone\"; message?: string; payload?: unknown };\n\nexport type StatewireConnection = ConnectionState & { reconnect: () => void };\n\n/** The live wire a remote session drives: connection status plus command POST. */\nexport type RemoteLink = {\n connection: StatewireConnection;\n send: (args: {\n seq: number;\n command: StatewireWireCommand;\n }) => Promise<StatewireSubmitResult>;\n};\n\nexport type StatewireSSEOptions = {\n url: string;\n headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);\n fetch?: typeof globalThis.fetch;\n onChange?: (connection: StatewireConnection) => void;\n sessionStore?: StatewireSessionStore;\n};\n\ntype Latest = {\n options: StatewireSSEOptions;\n ctx: StatewireTransportProps;\n};\n\ntype Conn = ConnBase & {\n latest: { readonly current: Latest };\n send: RemoteLink[\"send\"];\n};\n\nconst createConn = (\n latest: { readonly current: Latest },\n notify: (connection: StatewireConnection) => void,\n): Conn => {\n const c: Conn = {\n hasSnapshot: false,\n lease: null,\n attempt: 0,\n episode: null,\n abort: null,\n gate: createGate(),\n lifetime: null,\n latest,\n notify,\n reconnect: () => reconnectConn(c),\n attemptOnce: (abort) => attemptStream(c, abort),\n send: (args) => sendCommand(c, args),\n connection: { status: \"connecting\", reconnect: () => c.reconnect() },\n };\n return c;\n};\n\nconst baseUrl = (c: Conn) => c.latest.current.options.url.replace(/\\/$/, \"\");\n\nconst requestHeaders = async (c: Conn, extra: Record<string, string>) => {\n const { headers } = c.latest.current.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 c: Conn,\n input: string,\n init: RequestInit & { signal: AbortSignal },\n timeout: AbortSignal,\n): Promise<Response> => {\n try {\n return await (c.latest.current.options.fetch ?? globalThis.fetch)(\n input,\n init,\n );\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 let attached = false;\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, attached));\n attached = true;\n if (done) return done;\n }\n }\n};\n\nconst attemptStream = async (\n c: Conn,\n abort: AbortController,\n): Promise<\"stop\" | \"reattach\" | \"ended\"> => {\n const headers = await requestHeaders(c, {\n \"Statewire-Client-Id\": c.latest.current.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 c,\n `${baseUrl(c)}/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 { seq, command }: { seq: number; command: StatewireWireCommand },\n): Promise<StatewireSubmitResult> => {\n const timeout = AbortSignal.timeout(timing.heartbeatTimeoutMs);\n const res = await timedFetch(\n c,\n `${baseUrl(c)}/commands`,\n {\n method: \"POST\",\n headers: await requestHeaders(c, {\n \"content-type\": \"application/json\",\n \"Statewire-Client-Id\": c.latest.current.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 const body = (await res.json().catch(() => null)) as {\n res?: StatewireResponse;\n } | null;\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 useStatewireSSE = (\n options: StatewireSSEOptions,\n): StatewireTransport<any, any, any> => useStatewireRemote(options, createConn);\n\nexport const StatewireSSE = resource(useStatewireSSE) as <\n TState = any,\n TCommands extends StatewireCommands = StatewireCommands,\n TEvent = any,\n>(\n options: StatewireSSEOptions,\n) => ResourceElement<StatewireTransport<TState | undefined, TCommands, TEvent>>;\n"],"mappings":";;;;;;;AAyFA,MAAMmE,cACJF,QACAG,WACS;CACT,MAAMC,IAAU;EACdC,aAAa;EACbC,OAAO;EACP7B,SAAS;EACT8B,SAAS;EACTC,OAAO;EACPC,MAAMlE,WAAW;EACjBmE,UAAU;EACVV;EACAG;EACAtB,iBAAiBlC,cAAcyD,CAAC;EAChCO,cAAcH,UAAUI,cAAcR,GAAGI,KAAK;EAC9CxB,OAAOzB,SAASsD,YAAYT,GAAG7C,IAAI;EACnCwB,YAAY;GAAER,QAAQ;GAAcM,iBAAiBuB,EAAEvB,UAAU;EAAE;CACrE;CACA,OAAOuB;AACT;AAEA,MAAMU,WAAWV,MAAYA,EAAEJ,OAAOC,QAAQJ,QAAQR,IAAI0B,QAAQ,OAAO,EAAE;AAE3E,MAAMC,iBAAiB,OAAOZ,GAASa,UAAkC;CACvE,MAAM,EAAE3B,YAAYc,EAAEJ,OAAOC,QAAQJ;CACrC,MAAMsB,SAAS,IAAIC,QACjB,OAAO9B,YAAY,aAAa,MAAMA,QAAQ,IAAIA,OACpD;CACA,KAAK,MAAM,CAAC+B,MAAMC,UAAUC,OAAOC,QAAQP,KAAK,GAAGE,OAAOM,IAAIJ,MAAMC,KAAK;CACzE,OAAOH;AACT;AAEA,MAAMO,aAAa,OACjBtB,GACAuB,OACAC,MACAI,YACsB;CACtB,IAAI;EACF,OAAO,OAAO5B,EAAEJ,OAAOC,QAAQJ,QAAQL,SAASC,WAAWD,MAAAA,CACzDmC,OACAC,IACF;CACF,QAAQ;EACN,MAAMtF,SAAS0F,OAAO;CACxB;AACF;AAEA,MAAME,UAAU,OACd9B,GACA+B,QACAG,WACAE,SAC2C;CAC3C,MAAMC,UAAU,IAAIC,YAAY;CAChC,MAAMC,QAAQvG,gBAAgB;CAC9B,IAAIwG,WAAW;CACf,SAAS;EACP,MAAMC,QAAQC,iBACNR,UAAU9B,MAAM,GACtBnE,OAAO0G,kBACT;EACA,IAAIC;EACJ,IAAI;GACFA,SAAS,MAAMb,OAAOe,KAAK;EAC7B,QAAQ;GACN,MAAM5G,SAASgG,UAAUR,MAAM;EACjC,UAAU;GACRqB,aAAaN,KAAK;EACpB;EACA,IAAIG,OAAOI,MAAM,OAAO;EACxB,KAAK,MAAMC,QAAQV,MAAMF,QAAQa,OAAON,OAAO1B,OAAO,EAAEiC,QAAQ,KAAK,CAAC,CAAC,GAAG;GACxE,IAAIf,KAAK,GAAG,OAAO;GACnB,MAAMgB,MAAMC,KAAKd,MAAMU,IAAI;GAC3B,MAAMD,OAAOrH,mBAAmBS,eAAe4D,GAAGoD,KAAKZ,QAAQ,CAAC;GAChEA,WAAW;GACX,IAAIQ,MAAM,OAAOA;EACnB;CACF;AACF;AAEA,MAAMxC,gBAAgB,OACpBR,GACAI,UAC2C;CAC3C,MAAMlB,UAAU,MAAM0B,eAAeZ,GAAG;EACtC,uBAAuBA,EAAEJ,OAAOC,QAAQH,IAAIzC;EAC5CqG,QAAQ;CACV,CAAC;CACD,MAAMpB,YAAY,IAAIC,gBAAgB;CACtC,MAAMoB,cAAcb,iBACZR,UAAU9B,MAAM,GACtBnE,OAAO0G,kBACT;CACA,IAAIpF;CACJ,IAAI;EACFA,MAAM,MAAM+D,WACVtB,GACA,GAAGU,QAAQV,CAAC,EAAC,UACb;GACEd;GACAsE,OAAO;GACP9B,QAAQC,YAAY8B,IAAI,CAACrD,MAAMsB,QAAQQ,UAAUR,MAAM,CAAC;EAC1D,GACAQ,UAAUR,MACZ;CACF,UAAU;EACRqB,aAAaQ,WAAW;CAC1B;CACA,IAAIhG,IAAIY,WAAW,OAAOZ,IAAIY,WAAW,KAAK,MAAMuF,WAAWnG,GAAG;CAClE,IAAI,CAACA,IAAIoG,MAAM,CAACpG,IAAIqG,MAAM,MAAM,IAAIC,MAAM,QAAQtG,IAAIY,QAAQ;CAC9D,MAAM4D,SAASxE,IAAIqG,KAAKE,UAAU;CAClC,IAAI;EACF,OAAO,MAAMhC,QAAQ9B,GAAG+B,QAAQG,iBAAiB9B,MAAMsB,OAAOqC,OAAO;CACvE,UAAU;EACRhC,OAAOiC,OAAO,CAAC,CAACC,YAAY,CAAC,CAAC;CAChC;AACF;AAEA,MAAMP,cAAcnG,QAClBlB,gBAAgBC,gBAAgBiB,IAAI2B,QAAQgF,IAAI,aAAa,CAAC,CAAC;AAEjE,MAAMzD,cAAc,OAClBT,GACA,EAAEnB,KAAKC,cAC4B;CACnC,MAAM8C,UAAUD,YAAYC,QAAQ3F,OAAO0G,kBAAkB;CAC7D,MAAMpF,MAAM,MAAM+D,WAChBtB,GACA,GAAGU,QAAQV,CAAC,EAAC,YACb;EACErC,QAAQ;EACRuB,SAAS,MAAM0B,eAAeZ,GAAG;GAC/B,gBAAgB;GAChB,uBAAuBA,EAAEJ,OAAOC,QAAQH,IAAIzC;GAC5C,yBAAyBkH,OAAOtF,GAAG;GACnC,GAAImB,EAAEE,UAAU,QAAQ,EAAE,mBAAmBF,EAAEE,MAAM;EACvD,CAAC;EACD0D,MAAMP,KAAKe,UAAU;GACnBzG,QAAQmB,QAAQnB;GAChB,GAAG7B,aAAagD,QAAQlB,MAAM;EAChC,CAAC;EACD8D,QAAQC,YAAY8B,IAAI,CACtBzD,EAAEM,UAAUoB,UAAUC,YAAYvB,MAAM,GACxCwB,OAAO,CACR;CACH,GACAA,OACF;CACA,IAAIrE,IAAIoG,IAAI;EACV,MAAMC,OAAQ,MAAMrG,IAAI8G,KAAK,CAAC,CAACJ,YAAY,IAAI;EAG/C,OAAOL,MAAMrG,QAAQ+G,KAAAA,IACjB;GAAExG,MAAM;GAAWC,UAAUhC,eAAe6H,KAAKrG,GAAG;EAAE,IACtD,EAAEO,MAAM,UAAU;CACxB;CACA,IAAIP,IAAIY,WAAW,OAAOZ,IAAIY,WAAW,OAAOZ,IAAIY,WAAW,KAAK;EAGlE,MAAMoG,UACJhH,IAAIY,WAAW,MACV,YACDZ,IAAIY,WAAW,MACZ,mBACA;EAIT,KAAIyF,MAHgBrG,IAAI8G,KAAK,CAAC,CAACJ,YAAY,IAAI,EAAA,EAGrCQ,UAAUF,SAAS,OAAO,EAAEzG,MAAMyG,QAAQ;EACpD,MAAM,IAAIV,MAAM,QAAQtG,IAAIY,QAAQ;CACtC;CACA,IAAIZ,IAAIY,WAAW,OAAOZ,IAAIY,WAAW,KAAK,MAAMuF,WAAWnG,GAAG;CAClE,IAAIA,IAAIY,UAAU,OAAOZ,IAAIY,SAAS,KACpC,OAAO;EAAEL,MAAM;EAAYE,SAAS,QAAQT,IAAIY;CAAS;CAE3D,MAAM,IAAI0F,MAAM,QAAQtG,IAAIY,QAAQ;AACtC;AAEA,MAAMuG,mBAAkBjF,YAAA;CAAA,OAEgB5C,mBAAmB4C,SAASK,UAAU;AAAC;AAE/E,MAAa6E,eAAe/I,SAAS8I,eAAe"}
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,5 +1,6 @@
1
1
  import { StatewireEnvelope } from "../client/connection.js";
2
- import { StatewireTransport } from "../types.js";
2
+ import { Statewire } from "../types.js";
3
+ import { SocketRecorder, commandFrame, createCounterElement, createSocketRecorder, settle } from "./fixtures.js";
3
4
  import { ResourceElement } from "@assistant-ui/tap";
4
5
  //#region src/transports/testing.d.ts
5
6
  /** Let queued microtasks and op batches drain through the statewire client. */
@@ -8,7 +9,7 @@ declare const tick: () => Promise<void>;
8
9
  declare const applyOptimistic: <TState>(state: TState, mutate: (draft: TState) => void) => TState;
9
10
  type MockTransport<TState> = {
10
11
  /** Pass as the `transport:` option of useStatewire / Harness. */
11
- transport: ResourceElement<StatewireTransport<TState | undefined, any, any>>;
12
+ transport: ResourceElement<Statewire<TState | undefined, any, any>>;
12
13
  /** Captured `{ method, params }` command bodies, in POST order. */
13
14
  commands: {
14
15
  method: string;
@@ -27,5 +28,5 @@ type MockTransport<TState> = {
27
28
  };
28
29
  declare const createMockTransport: <TState>(initialState: TState) => MockTransport<TState>;
29
30
  //#endregion
30
- export { MockTransport, applyOptimistic, createMockTransport, tick };
31
+ export { MockTransport, SocketRecorder, applyOptimistic, commandFrame, createCounterElement, createMockTransport, createSocketRecorder, settle, tick };
31
32
  //# sourceMappingURL=testing.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"testing.d.ts","names":[],"sources":["../../src/transports/testing.ts"],"mappings":";;;;;cAOa,YAAI;;cAGJ,kBAAmB,QAC9B,OAAO,QACP,SAAS,OAAO,oBACf;KAQS,cAAc;;EAExB,WAAW,gBAAgB,mBAAmB;;EAE9C;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;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,5 +1,7 @@
1
- import { recordMutations } from "../client/optimistic.js";
1
+ import { recordMutations } from "../util/recordMutations.js";
2
2
  import { StatewireSSE } from "./sse.js";
3
+ import { openSSEStream, sseFrame } from "./fake-sse.js";
4
+ import { commandFrame, createCounterElement, createSocketRecorder, settle } from "./fixtures.js";
3
5
  //#region src/transports/testing.ts
4
6
  /** Let queued microtasks and op batches drain through the statewire client. */
5
7
  const tick = () => new Promise((r) => setTimeout(r, 0));
@@ -19,32 +21,20 @@ const createMockTransport = (initialState) => {
19
21
  const streams = /* @__PURE__ */ new Set();
20
22
  const commands = [];
21
23
  const sendEnvelope = (envelope) => {
22
- for (const stream of streams) stream.send(`data: ${JSON.stringify(envelope)}\n\n`);
24
+ for (const stream of streams) stream.send(sseFrame(envelope));
23
25
  return tick();
24
26
  };
25
- const handleStream = (id, signal) => {
26
- clientId = id;
27
- const lastSeq = clients.get(id) ?? 0;
28
- clients.set(id, lastSeq);
29
- const encoder = new TextEncoder();
30
- let handle;
31
- const body = new ReadableStream({
32
- start: (controller) => {
33
- let open = true;
34
- handle = {
35
- clientId: id,
36
- send: (text) => {
37
- if (open) controller.enqueue(encoder.encode(text));
38
- },
39
- close: () => {
40
- if (!open) return;
41
- open = false;
42
- streams.delete(handle);
43
- controller.close();
44
- }
45
- };
46
- streams.add(handle);
47
- handle.send(`data: ${JSON.stringify({
27
+ const fakeFetch = (async (input, init) => {
28
+ if (finished) throw new TypeError("fetch failed (finished)");
29
+ const url = String(input);
30
+ const headers = new Headers(init?.headers);
31
+ const id = headers.get("Statewire-Client-Id") ?? "";
32
+ if (url.endsWith("/stream")) {
33
+ clientId = id;
34
+ const lastSeq = clients.get(id) ?? 0;
35
+ clients.set(id, lastSeq);
36
+ return openSSEStream(streams, id, (handle) => {
37
+ handle.send(sseFrame({
48
38
  ops: [{
49
39
  op: "replace",
50
40
  path: [],
@@ -54,26 +44,9 @@ const createMockTransport = (initialState) => {
54
44
  lastSeq,
55
45
  lease: "mock-lease"
56
46
  }
57
- })}\n\n`);
58
- },
59
- cancel: () => {
60
- streams.delete(handle);
61
- }
62
- });
63
- signal?.addEventListener("abort", () => {
64
- for (const stream of [...streams]) if (stream.clientId === id) stream.close();
65
- });
66
- return new Response(body, {
67
- status: 200,
68
- headers: { "content-type": "text/event-stream" }
69
- });
70
- };
71
- const fakeFetch = (async (input, init) => {
72
- if (finished) throw new TypeError("fetch failed (finished)");
73
- const url = String(input);
74
- const headers = new Headers(init?.headers);
75
- const id = headers.get("Statewire-Client-Id") ?? "";
76
- if (url.endsWith("/stream")) return handleStream(id, init?.signal ?? void 0);
47
+ }));
48
+ }, init?.signal ?? void 0);
49
+ }
77
50
  if (url.endsWith("/commands")) {
78
51
  const seq = Number(headers.get("Statewire-Command-Seq"));
79
52
  clients.set(id, Math.max(clients.get(id) ?? 0, seq));
@@ -116,6 +89,6 @@ const createMockTransport = (initialState) => {
116
89
  };
117
90
  };
118
91
  //#endregion
119
- export { applyOptimistic, createMockTransport, tick };
92
+ export { applyOptimistic, commandFrame, createCounterElement, createMockTransport, createSocketRecorder, settle, tick };
120
93
 
121
94
  //# sourceMappingURL=testing.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"testing.js","names":["ResourceElement","StatewireEnvelope","recordMutations","StatewireSSE","StatewireTransport","tick","Promise","r","setTimeout","applyOptimistic","state","TState","mutate","draft","StreamHandle","clientId","send","text","close","MockTransport","transport","commands","method","params","setState","ack","seq","sendEnvelope","envelope","finish","reason","message","attached","mockCount","createMockTransport","initialState","finished","clients","Map","streams","Set","stream","JSON","stringify","handleStream","id","signal","AbortSignal","lastSeq","get","set","encoder","TextEncoder","handle","body","ReadableStream","Uint8Array","start","controller","open","enqueue","encode","delete","add","ops","op","path","value","syn","lease","cancel","addEventListener","Response","status","headers","fakeFetch","input","RequestInfo","URL","init","RequestInit","TypeError","url","String","Headers","endsWith","undefined","Number","Math","max","push","parse","json","fetch","next","fin","size"],"sources":["../../src/transports/testing.ts"],"sourcesContent":["import type { ResourceElement } from \"@assistant-ui/tap\";\nimport type { StatewireEnvelope } from \"../client/connection\";\nimport { recordMutations } from \"../client/optimistic\";\nimport { StatewireSSE } from \"./sse\";\nimport type { StatewireTransport } from \"../types\";\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\ntype StreamHandle = {\n clientId: string;\n send: (text: string) => void;\n close: () => void;\n};\n\nexport type MockTransport<TState> = {\n /** Pass as the `transport:` option of useStatewire / Harness. */\n transport: ResourceElement<StatewireTransport<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<StreamHandle>();\n const commands: { method: string; params: unknown[] }[] = [];\n\n const sendEnvelope = (envelope: StatewireEnvelope) => {\n for (const stream of streams) {\n stream.send(`data: ${JSON.stringify(envelope)}\\n\\n`);\n }\n return tick();\n };\n\n const handleStream = (id: string, signal: AbortSignal | undefined) => {\n clientId = id;\n const lastSeq = clients.get(id) ?? 0;\n clients.set(id, lastSeq);\n const encoder = new TextEncoder();\n let handle: StreamHandle;\n const body = new ReadableStream<Uint8Array>({\n start: (controller) => {\n let open = true;\n handle = {\n clientId: id,\n send: (text) => {\n if (open) controller.enqueue(encoder.encode(text));\n },\n close: () => {\n if (!open) return;\n open = false;\n streams.delete(handle);\n controller.close();\n },\n };\n streams.add(handle);\n handle.send(\n `data: ${JSON.stringify({\n ops: [{ op: \"replace\", path: [], value: state }],\n syn: { lastSeq, lease: \"mock-lease\" },\n })}\\n\\n`,\n );\n },\n cancel: () => {\n streams.delete(handle);\n },\n });\n signal?.addEventListener(\"abort\", () => {\n for (const stream of [...streams]) {\n if (stream.clientId === id) stream.close();\n }\n });\n return new Response(body, {\n status: 200,\n headers: { \"content-type\": \"text/event-stream\" },\n });\n };\n\n const fakeFetch = (async (\n input: RequestInfo | URL,\n init?: RequestInit,\n ): Promise<Response> => {\n if (finished) throw new TypeError(\"fetch failed (finished)\");\n const url = String(input);\n const headers = new Headers(init?.headers);\n const id = headers.get(\"Statewire-Client-Id\") ?? \"\";\n if (url.endsWith(\"/stream\"))\n return handleStream(id, init?.signal ?? undefined);\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":"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"}
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,18 +1,9 @@
1
- import { StatewireSessionStore } from "../client/sessionStore.js";
2
- import { StatewireConnection } from "./sse.js";
3
- import { StatewireCommands, StatewireTransport } from "../types.js";
1
+ import { StatewireClient } from "../statewire.js";
2
+ import { Statewire } from "../types.js";
4
3
  import { ResourceElement } from "@assistant-ui/tap";
5
4
  //#region src/transports/ws.d.ts
6
- type StatewireWSOptions = {
7
- url: string;
8
- protocols?: string | string[];
9
- query?: Record<string, string> | (() => Record<string, string> | Promise<Record<string, string>>);
10
- webSocket?: new (url: string, protocols?: string | string[]) => WebSocket;
11
- onChange?: (connection: StatewireConnection) => void;
12
- sessionStore?: StatewireSessionStore;
13
- };
14
5
  declare const WS_SUBPROTOCOL = "statewire.v1";
15
- declare const StatewireWS: <TState = any, TCommands extends StatewireCommands = StatewireCommands, TEvent = any>(options: StatewireWSOptions) => ResourceElement<StatewireTransport<TState | undefined, TCommands, TEvent>>;
6
+ declare const StatewireWS: <TState = any, TCommands extends Statewire.Commands = Statewire.Commands, TEvent = any>(options: StatewireClient.WSOptions) => ResourceElement<Statewire<TState | undefined, TCommands, TEvent>>;
16
7
  //#endregion
17
- export { StatewireWS, StatewireWSOptions, WS_SUBPROTOCOL };
8
+ export { StatewireWS, WS_SUBPROTOCOL };
18
9
  //# sourceMappingURL=ws.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ws.d.ts","names":[],"sources":["../../src/transports/ws.ts"],"mappings":";;;;;KAuBY;EACV;EACA;EACA,QACI,gCACO,yBAAyB,QAAQ;EAC5C,iBAAiB,aAAa,kCAAkC;EAChE,YAAY,YAAY;EACxB,eAAe;;cAuBJ;cAmNA,cACX,cACA,kBAAkB,oBAAoB,mBACtC,cAEA,SAAS,uBACN,gBAAgB,mBAAmB,oBAAoB,WAAW"}
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,67 +1,42 @@
1
1
  import { encodeParams } from "../wire/meta.js";
2
+ import { CLIENT_QUERY_PARAM } from "../host/adapter.js";
2
3
  import { timing } from "../client/timing.js";
3
- import { createGate, handleEnvelope, overloadedError, reconnectConn } from "../client/connection.js";
4
+ import { handleEnvelope, overloadedError, useStatewireConn } from "../client/connection.js";
4
5
  import { useStatewireRemote } from "./remote.js";
6
+ import { useState } from "@assistant-ui/tap/react-shim";
5
7
  import { flushTapSync, resource } from "@assistant-ui/tap";
6
8
  //#region src/transports/ws.ts
7
9
  const OPEN = 1;
8
10
  const WS_SUBPROTOCOL = "statewire.v1";
9
- const createConn = (latest, notify) => {
10
- const c = {
11
- hasSnapshot: false,
12
- lease: null,
13
- attempt: 0,
14
- episode: null,
15
- abort: null,
16
- socket: null,
17
- pending: [],
18
- gate: createGate(),
19
- lifetime: null,
20
- latest,
21
- notify,
22
- reconnect: () => reconnectConn(c),
23
- attemptOnce: (abort) => attemptSocket(c, abort),
24
- send: (args) => sendCommand(c, args),
25
- connection: {
26
- status: "connecting",
27
- reconnect: () => c.reconnect()
28
- }
29
- };
30
- return c;
31
- };
32
- const socketUrl = async (c) => {
33
- const { url, query } = c.latest.current.options;
11
+ const socketUrl = async (options, clientId) => {
12
+ const { url, query } = options;
34
13
  const params = new URLSearchParams(typeof query === "function" ? await query() : query);
35
- params.set("client", c.latest.current.ctx.clientId);
14
+ params.set(CLIENT_QUERY_PARAM, clientId);
36
15
  return `${url.replace(/\/$/, "").replace(/^http/, "ws")}/ws?${params.toString()}`;
37
16
  };
38
- const settleOverloaded = (c, { seq, retryAfterMs }) => {
17
+ const settleOverloaded = (w, { seq, retryAfterMs }) => {
39
18
  if (seq === void 0) throw overloadedError(retryAfterMs);
40
- const entry = c.pending.find((p) => p.seq === seq);
19
+ const entry = w.pending.find((p) => p.seq === seq);
41
20
  if (!entry) return;
42
- c.pending = c.pending.filter((p) => p !== entry);
21
+ w.pending = w.pending.filter((p) => p !== entry);
43
22
  entry.reject(overloadedError(retryAfterMs));
44
23
  };
45
- const resolveReceipts = (c, ack) => {
46
- while (c.pending[0] && c.pending[0].seq <= ack) c.pending.shift().resolve({ type: "receipt" });
47
- };
48
- const resolveResponded = (c, seqs) => {
24
+ const resolvePending = (w, matches) => {
49
25
  const remaining = [];
50
- for (const entry of c.pending) if (seqs.has(entry.seq)) entry.resolve({ type: "receipt" });
26
+ for (const entry of w.pending) if (matches(entry)) entry.resolve({ type: "receipt" });
51
27
  else remaining.push(entry);
52
- c.pending = remaining;
28
+ w.pending = remaining;
53
29
  };
54
- const failPending = (c, error) => {
55
- const pending = c.pending;
56
- c.pending = [];
30
+ const failPending = (w, error) => {
31
+ const pending = w.pending;
32
+ w.pending = [];
57
33
  for (const entry of pending) entry.reject(error);
58
34
  };
59
- const runSocket = (c, ws, abort) => new Promise((resolve, reject) => {
35
+ const runSocket = (c, w, ws, abort) => new Promise((resolve, reject) => {
60
36
  let timer;
61
37
  let timedOut = false;
62
38
  let outcome = null;
63
39
  let closed = false;
64
- let attached = false;
65
40
  const arm = () => {
66
41
  clearTimeout(timer);
67
42
  timer = setTimeout(() => {
@@ -79,10 +54,15 @@ const runSocket = (c, ws, abort) => new Promise((resolve, reject) => {
79
54
  ws.close();
80
55
  };
81
56
  const processEnv = (env) => {
82
- const done = flushTapSync(() => handleEnvelope(c, env, attached));
83
- attached = true;
84
- if (env.ack !== void 0) resolveReceipts(c, env.ack);
85
- if (env.res !== void 0) resolveResponded(c, new Set(env.res.map((r) => r.seq)));
57
+ const done = flushTapSync(() => handleEnvelope(c, env));
58
+ if (env.ack !== void 0) {
59
+ const ack = env.ack;
60
+ resolvePending(w, (entry) => entry.seq <= ack);
61
+ }
62
+ if (env.res !== void 0) {
63
+ const seqs = new Set(env.res.map((r) => r.seq));
64
+ resolvePending(w, (entry) => seqs.has(entry.seq));
65
+ }
86
66
  if (done) {
87
67
  outcome = done;
88
68
  ws.close();
@@ -99,7 +79,7 @@ const runSocket = (c, ws, abort) => new Promise((resolve, reject) => {
99
79
  return;
100
80
  }
101
81
  try {
102
- if (frame.overloaded !== void 0) settleOverloaded(c, frame.overloaded);
82
+ if (frame.overloaded !== void 0) settleOverloaded(w, frame.overloaded);
103
83
  else processEnv(frame);
104
84
  } catch (error) {
105
85
  fail(error);
@@ -108,28 +88,28 @@ const runSocket = (c, ws, abort) => new Promise((resolve, reject) => {
108
88
  ws.onclose = () => {
109
89
  closed = true;
110
90
  clearTimeout(timer);
111
- if (c.socket === ws) failPending(c, /* @__PURE__ */ new Error("network"));
91
+ if (w.socket === ws) failPending(w, /* @__PURE__ */ new Error("network"));
112
92
  if (outcome) resolve(outcome);
113
93
  else reject(/* @__PURE__ */ new Error(timedOut ? "timeout" : "network"));
114
94
  };
115
95
  });
116
- const attemptSocket = async (c, abort) => {
96
+ const attemptSocket = async (c, w, options, abort) => {
117
97
  let ws = null;
118
98
  try {
119
- const url = await socketUrl(c);
99
+ const url = await socketUrl(options, c.ctx.clientId);
120
100
  abort.signal.throwIfAborted();
121
- const { protocols, webSocket } = c.latest.current.options;
101
+ const { protocols, webSocket } = options;
122
102
  const WS = webSocket ?? globalThis.WebSocket;
123
103
  if (!WS) throw new Error("no WebSocket implementation");
124
104
  ws = new WS(url, [WS_SUBPROTOCOL, ...protocols === void 0 ? [] : typeof protocols === "string" ? [protocols] : protocols]);
125
- c.socket = ws;
126
- return await runSocket(c, ws, abort);
105
+ w.socket = ws;
106
+ return await runSocket(c, w, ws, abort);
127
107
  } finally {
128
- if (c.socket === ws) c.socket = null;
108
+ if (w.socket === ws) w.socket = null;
129
109
  }
130
110
  };
131
- const sendCommand = (c, { seq, command }) => {
132
- const ws = c.socket;
111
+ const sendCommand = (w, { seq, command }) => {
112
+ const ws = w.socket;
133
113
  if (!ws || ws.readyState !== OPEN) return Promise.reject(/* @__PURE__ */ new Error("network"));
134
114
  return new Promise((resolve, reject) => {
135
115
  const entry = {
@@ -144,11 +124,11 @@ const sendCommand = (c, { seq, command }) => {
144
124
  }
145
125
  };
146
126
  const timer = setTimeout(() => {
147
- c.pending = c.pending.filter((p) => p !== entry);
127
+ w.pending = w.pending.filter((p) => p !== entry);
148
128
  reject(/* @__PURE__ */ new Error("timeout"));
149
129
  ws.close();
150
130
  }, timing.heartbeatTimeoutMs);
151
- c.pending.push(entry);
131
+ w.pending.push(entry);
152
132
  try {
153
133
  ws.send(JSON.stringify({
154
134
  method: command.method,
@@ -157,14 +137,31 @@ const sendCommand = (c, { seq, command }) => {
157
137
  }));
158
138
  } catch {
159
139
  clearTimeout(timer);
160
- c.pending = c.pending.filter((p) => p !== entry);
140
+ w.pending = w.pending.filter((p) => p !== entry);
161
141
  reject(/* @__PURE__ */ new Error("network"));
162
142
  }
163
143
  });
164
144
  };
165
- const useStatewireWS = (options) => {
166
- return useStatewireRemote(options, createConn);
145
+ const useWsLink = (options, ctx) => {
146
+ const [w] = useState(() => ({
147
+ socket: null,
148
+ pending: []
149
+ }));
150
+ const c_0 = useStatewireConn({
151
+ url: options.url,
152
+ onChange: options.onChange,
153
+ ctx,
154
+ attemptOnce: (c, abort) => attemptSocket(c, w, options, abort)
155
+ });
156
+ const [link] = useState(() => ({
157
+ get connection() {
158
+ return c_0.connection;
159
+ },
160
+ send: (args) => sendCommand(w, args)
161
+ }));
162
+ return link;
167
163
  };
164
+ const useStatewireWS = (options) => useStatewireRemote(options, useWsLink);
168
165
  const StatewireWS = resource(useStatewireWS);
169
166
  //#endregion
170
167
  export { StatewireWS, WS_SUBPROTOCOL };