statewire 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +12 -26
  15. package/dist/client/session.d.ts.map +1 -1
  16. package/dist/client/session.js +70 -43
  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 +19 -35
  33. package/dist/host/host.d.ts.map +1 -1
  34. package/dist/host/host.js +86 -54
  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 +511 -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 +54 -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 +97 -10
  49. package/dist/statewire.d.ts.map +1 -1
  50. package/dist/statewire.js +32 -36
  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 +7 -25
  63. package/dist/transports/remote.js.map +1 -1
  64. package/dist/transports/sse.d.ts +32 -20
  65. package/dist/transports/sse.d.ts.map +1 -1
  66. package/dist/transports/sse.js +57 -46
  67. package/dist/transports/sse.js.map +1 -1
  68. package/dist/transports/testing.d.ts +8 -6
  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 +59 -63
  75. package/dist/transports/ws.js.map +1 -1
  76. package/dist/types.d.ts +55 -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 +129 -90
  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 +122 -110
  121. package/src/host/socket.ts +618 -0
  122. package/src/host-internal.ts +28 -0
  123. package/src/host.ts +72 -0
  124. package/src/index.ts +2 -62
  125. package/src/statewire.ts +168 -61
  126. package/src/transports/fake-sse.ts +51 -0
  127. package/src/transports/fixtures.ts +60 -0
  128. package/src/transports/remote.ts +14 -51
  129. package/src/transports/sse.ts +87 -89
  130. package/src/transports/testing.ts +33 -63
  131. package/src/transports/ws.ts +78 -100
  132. package/src/types.ts +52 -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,3 +1,4 @@
1
+ import { useRef, useState } from "react";
1
2
  import { flushTapSync, resource } from "@assistant-ui/tap";
2
3
  import type { ResourceElement } from "@assistant-ui/tap";
3
4
  import { encodeParams, reviveResponse } from "../wire/meta";
@@ -5,24 +6,22 @@ import { createSseParser } from "./sse-parser";
5
6
  import { timing } from "../client/timing";
6
7
  import {
7
8
  classify,
8
- createGate,
9
9
  handleEnvelope,
10
10
  overloadedError,
11
11
  parseRetryAfter,
12
- reconnectConn,
12
+ useStatewireConn,
13
13
  } from "../client/connection";
14
14
  import type {
15
- ConnBase,
15
+ Conn,
16
16
  StatewireEnvelope,
17
- StatewireFinish,
18
17
  StatewireResponse,
19
18
  } from "../client/connection";
20
- import type { StatewireTransport } from "../types";
19
+ import type { Statewire } from "../types";
20
+ import type { StatewireClient } from "../statewire";
21
+ import type { StatewireHost } from "../host";
21
22
  import { useStatewireRemote } from "./remote";
22
- import type { StatewireSessionStore } from "../client/sessionStore";
23
- import type { StatewireCommands } from "../types";
24
23
 
25
- export type StatewireTransportProps = {
24
+ export type StatewireRemoteProps = {
26
25
  clientId: string;
27
26
  onSnapshot: (args: { ops: unknown[]; lastSeq: number }) => void;
28
27
  onOps: (args: {
@@ -30,17 +29,31 @@ export type StatewireTransportProps = {
30
29
  res?: StatewireResponse[];
31
30
  ack?: number;
32
31
  }) => void;
33
- onFinish: (args: StatewireFinish) => void;
32
+ onFinish: (args: StatewireHost.Fin) => void;
34
33
  };
35
34
 
36
- export type StatewireWireCommand = {
37
- method: string;
38
- params: readonly unknown[];
39
- };
35
+ export type StatewireWireCommand =
36
+ | { method: string; params: readonly unknown[]; context?: never }
37
+ | { method?: never; params?: never; context: Record<string, unknown> };
38
+
39
+ /** The wire shape of a client context submission: one root replace op. */
40
+ export const contextOps = (value: Record<string, unknown>) => [
41
+ { op: "replace", path: [], value },
42
+ ];
43
+
44
+ export const encodeWireCommand = (command: StatewireWireCommand) => ({
45
+ ...(command.method !== undefined && {
46
+ method: command.method,
47
+ ...encodeParams(command.params),
48
+ }),
49
+ ...(command.context !== undefined && {
50
+ context: contextOps(command.context),
51
+ }),
52
+ });
40
53
 
41
54
  export type StatewireSubmitResult =
42
55
  | { type: "receipt"; response?: StatewireResponse }
43
- | { type: "rejected"; message: string; payload?: unknown }
56
+ | { type: "rejected"; message: string }
44
57
  | { type: "seq-gap" }
45
58
  | { type: "unknown-client" }
46
59
  | { type: "stale-lease" };
@@ -50,7 +63,7 @@ export type ConnectionState =
50
63
  | { status: "live" }
51
64
  | {
52
65
  status: "reconnecting";
53
- cause: "dropped" | "ended" | "error";
66
+ cause: "dropped" | "evicted" | "error";
54
67
  attempt: number;
55
68
  nextRetryAt: number;
56
69
  lastError?: string;
@@ -58,61 +71,23 @@ export type ConnectionState =
58
71
  }
59
72
  | { status: "gone"; message?: string; payload?: unknown };
60
73
 
61
- export type StatewireConnection = ConnectionState & { reconnect: () => void };
62
-
63
74
  /** The live wire a remote session drives: connection status plus command POST. */
64
75
  export type RemoteLink = {
65
- connection: StatewireConnection;
76
+ connection: StatewireClient.Connection;
66
77
  send: (args: {
67
78
  seq: number;
68
79
  command: StatewireWireCommand;
69
80
  }) => Promise<StatewireSubmitResult>;
70
81
  };
71
82
 
72
- export type StatewireSSEOptions = {
73
- url: string;
74
- headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
75
- fetch?: typeof globalThis.fetch;
76
- onChange?: (connection: StatewireConnection) => void;
77
- sessionStore?: StatewireSessionStore;
78
- };
79
-
80
- type Latest = {
81
- options: StatewireSSEOptions;
82
- ctx: StatewireTransportProps;
83
- };
83
+ const baseUrl = (options: StatewireClient.SSEOptions) =>
84
+ options.url.replace(/\/$/, "");
84
85
 
85
- type Conn = ConnBase & {
86
- latest: { readonly current: Latest };
87
- send: RemoteLink["send"];
88
- };
89
-
90
- const createConn = (
91
- latest: { readonly current: Latest },
92
- notify: (connection: StatewireConnection) => void,
93
- ): Conn => {
94
- const c: Conn = {
95
- hasSnapshot: false,
96
- lease: null,
97
- attempt: 0,
98
- episode: null,
99
- abort: null,
100
- gate: createGate(),
101
- lifetime: null,
102
- latest,
103
- notify,
104
- reconnect: () => reconnectConn(c),
105
- attemptOnce: (abort) => attemptStream(c, abort),
106
- send: (args) => sendCommand(c, args),
107
- connection: { status: "connecting", reconnect: () => c.reconnect() },
108
- };
109
- return c;
110
- };
111
-
112
- const baseUrl = (c: Conn) => c.latest.current.options.url.replace(/\/$/, "");
113
-
114
- const requestHeaders = async (c: Conn, extra: Record<string, string>) => {
115
- const { headers } = c.latest.current.options;
86
+ const requestHeaders = async (
87
+ options: StatewireClient.SSEOptions,
88
+ extra: Record<string, string>,
89
+ ) => {
90
+ const { headers } = options;
116
91
  const merged = new Headers(
117
92
  typeof headers === "function" ? await headers() : headers,
118
93
  );
@@ -121,16 +96,13 @@ const requestHeaders = async (c: Conn, extra: Record<string, string>) => {
121
96
  };
122
97
 
123
98
  const timedFetch = async (
124
- c: Conn,
99
+ options: StatewireClient.SSEOptions,
125
100
  input: string,
126
101
  init: RequestInit & { signal: AbortSignal },
127
102
  timeout: AbortSignal,
128
103
  ): Promise<Response> => {
129
104
  try {
130
- return await (c.latest.current.options.fetch ?? globalThis.fetch)(
131
- input,
132
- init,
133
- );
105
+ return await (options.fetch ?? globalThis.fetch)(input, init);
134
106
  } catch {
135
107
  throw classify(timeout);
136
108
  }
@@ -144,7 +116,6 @@ const consume = async (
144
116
  ): Promise<"stop" | "reattach" | "ended"> => {
145
117
  const decoder = new TextDecoder();
146
118
  const parse = createSseParser();
147
- let attached = false;
148
119
  for (;;) {
149
120
  const timer = setTimeout(
150
121
  () => heartbeat.abort(),
@@ -162,8 +133,7 @@ const consume = async (
162
133
  for (const data of parse(decoder.decode(result.value, { stream: true }))) {
163
134
  if (dead()) return "ended";
164
135
  const env = JSON.parse(data) as StatewireEnvelope;
165
- const done = flushTapSync(() => handleEnvelope(c, env, attached));
166
- attached = true;
136
+ const done = flushTapSync(() => handleEnvelope(c, env));
167
137
  if (done) return done;
168
138
  }
169
139
  }
@@ -171,10 +141,11 @@ const consume = async (
171
141
 
172
142
  const attemptStream = async (
173
143
  c: Conn,
144
+ options: StatewireClient.SSEOptions,
174
145
  abort: AbortController,
175
146
  ): Promise<"stop" | "reattach" | "ended"> => {
176
- const headers = await requestHeaders(c, {
177
- "Statewire-Client-Id": c.latest.current.ctx.clientId,
147
+ const headers = await requestHeaders(options, {
148
+ "Statewire-Client-Id": c.ctx.clientId,
178
149
  accept: "text/event-stream",
179
150
  });
180
151
  const heartbeat = new AbortController();
@@ -185,8 +156,8 @@ const attemptStream = async (
185
156
  let res: Response;
186
157
  try {
187
158
  res = await timedFetch(
188
- c,
189
- `${baseUrl(c)}/stream`,
159
+ options,
160
+ `${baseUrl(options)}/stream`,
190
161
  {
191
162
  headers,
192
163
  cache: "no-store",
@@ -212,24 +183,22 @@ const overloaded = (res: Response) =>
212
183
 
213
184
  const sendCommand = async (
214
185
  c: Conn,
186
+ options: StatewireClient.SSEOptions,
215
187
  { seq, command }: { seq: number; command: StatewireWireCommand },
216
188
  ): Promise<StatewireSubmitResult> => {
217
189
  const timeout = AbortSignal.timeout(timing.heartbeatTimeoutMs);
218
190
  const res = await timedFetch(
219
- c,
220
- `${baseUrl(c)}/commands`,
191
+ options,
192
+ `${baseUrl(options)}/commands`,
221
193
  {
222
194
  method: "POST",
223
- headers: await requestHeaders(c, {
195
+ headers: await requestHeaders(options, {
224
196
  "content-type": "application/json",
225
- "Statewire-Client-Id": c.latest.current.ctx.clientId,
197
+ "Statewire-Client-Id": c.ctx.clientId,
226
198
  "Statewire-Command-Seq": String(seq),
227
199
  ...(c.lease !== null && { "Statewire-Lease": c.lease }),
228
200
  }),
229
- body: JSON.stringify({
230
- method: command.method,
231
- ...encodeParams(command.params),
232
- }),
201
+ body: JSON.stringify(encodeWireCommand(command)),
233
202
  signal: AbortSignal.any([
234
203
  c.lifetime?.signal ?? AbortSignal.abort(),
235
204
  timeout,
@@ -238,9 +207,15 @@ const sendCommand = async (
238
207
  timeout,
239
208
  );
240
209
  if (res.ok) {
241
- const body = (await res.json().catch(() => null)) as {
242
- res?: StatewireResponse;
243
- } | null;
210
+ let body: { res?: StatewireResponse } | null;
211
+ try {
212
+ body = (await res.json()) as { res?: StatewireResponse } | null;
213
+ } catch {
214
+ return {
215
+ type: "rejected",
216
+ message: "statewire: protocol error: malformed /commands receipt body",
217
+ };
218
+ }
244
219
  return body?.res !== undefined
245
220
  ? { type: "receipt", response: reviveResponse(body.res) }
246
221
  : { type: "receipt" };
@@ -267,14 +242,37 @@ const sendCommand = async (
267
242
  throw new Error(`HTTP ${res.status}`);
268
243
  };
269
244
 
245
+ const useSseLink = (
246
+ options: StatewireClient.SSEOptions,
247
+ ctx: StatewireRemoteProps,
248
+ ): RemoteLink => {
249
+ const latest = useRef(options);
250
+ latest.current = options;
251
+ const c = useStatewireConn({
252
+ url: options.url,
253
+ onChange: options.onChange,
254
+ ctx,
255
+ attemptOnce: (c, abort) => attemptStream(c, options, abort),
256
+ });
257
+ const [link] = useState(
258
+ (): RemoteLink => ({
259
+ get connection() {
260
+ return c.connection;
261
+ },
262
+ send: (args) => sendCommand(c, latest.current, args),
263
+ }),
264
+ );
265
+ return link;
266
+ };
267
+
270
268
  const useStatewireSSE = (
271
- options: StatewireSSEOptions,
272
- ): StatewireTransport<any, any, any> => useStatewireRemote(options, createConn);
269
+ options: StatewireClient.SSEOptions,
270
+ ): Statewire<any, any, any> => useStatewireRemote(options, useSseLink);
273
271
 
274
272
  export const StatewireSSE = resource(useStatewireSSE) as <
275
273
  TState = any,
276
- TCommands extends StatewireCommands = StatewireCommands,
274
+ TCommands extends Statewire.Commands = Statewire.Commands,
277
275
  TEvent = any,
278
276
  >(
279
- options: StatewireSSEOptions,
280
- ) => ResourceElement<StatewireTransport<TState | undefined, TCommands, TEvent>>;
277
+ options: StatewireClient.SSEOptions,
278
+ ) => ResourceElement<Statewire<TState | undefined, TCommands, TEvent>>;
@@ -1,8 +1,12 @@
1
1
  import type { ResourceElement } from "@assistant-ui/tap";
2
2
  import type { StatewireEnvelope } from "../client/connection";
3
- import { recordMutations } from "../client/optimistic";
3
+ import { recordMutations } from "../util/recordMutations";
4
4
  import { StatewireSSE } from "./sse";
5
- import type { StatewireTransport } from "../types";
5
+ import type { Statewire } from "../types";
6
+ import { openSSEStream, sseFrame } from "./fake-sse";
7
+ import type { FakeStreamHandle } from "./fake-sse";
8
+
9
+ export * from "./fixtures";
6
10
 
7
11
  /** Let queued microtasks and op batches drain through the statewire client. */
8
12
  export const tick = () => new Promise<void>((r) => setTimeout(r, 0));
@@ -13,17 +17,11 @@ export const applyOptimistic = <TState>(
13
17
  mutate: (draft: TState) => void,
14
18
  ): TState => recordMutations(state, mutate).state;
15
19
 
16
- type StreamHandle = {
17
- clientId: string;
18
- send: (text: string) => void;
19
- close: () => void;
20
- };
21
-
22
20
  export type MockTransport<TState> = {
23
21
  /** Pass as the `transport:` option of useStatewire / Harness. */
24
- transport: ResourceElement<StatewireTransport<TState | undefined, any, any>>;
25
- /** Captured `{ method, params }` command bodies, in POST order. */
26
- commands: { method: string; params: unknown[] }[];
22
+ transport: ResourceElement<Statewire<TState | undefined, any, any>>;
23
+ /** Captured `/commands` POST bodies, in POST order. */
24
+ commands: { method?: string; params?: unknown[]; context?: unknown }[];
27
25
  /** Replace the replicated state and broadcast it as a root snapshot op. */
28
26
  setState: (state: TState) => Promise<void>;
29
27
  /** Deliver an ack watermark on the stream. */
@@ -50,60 +48,14 @@ export const createMockTransport = <TState>(
50
48
  let clientId = "";
51
49
  let finished = false;
52
50
  const clients = new Map<string, number>();
53
- const streams = new Set<StreamHandle>();
54
- const commands: { method: string; params: unknown[] }[] = [];
51
+ const streams = new Set<FakeStreamHandle>();
52
+ const commands: MockTransport<unknown>["commands"] = [];
55
53
 
56
54
  const sendEnvelope = (envelope: StatewireEnvelope) => {
57
- for (const stream of streams) {
58
- stream.send(`data: ${JSON.stringify(envelope)}\n\n`);
59
- }
55
+ for (const stream of streams) stream.send(sseFrame(envelope));
60
56
  return tick();
61
57
  };
62
58
 
63
- const handleStream = (id: string, signal: AbortSignal | undefined) => {
64
- clientId = id;
65
- const lastSeq = clients.get(id) ?? 0;
66
- clients.set(id, lastSeq);
67
- const encoder = new TextEncoder();
68
- let handle: StreamHandle;
69
- const body = new ReadableStream<Uint8Array>({
70
- start: (controller) => {
71
- let open = true;
72
- handle = {
73
- clientId: id,
74
- send: (text) => {
75
- if (open) controller.enqueue(encoder.encode(text));
76
- },
77
- close: () => {
78
- if (!open) return;
79
- open = false;
80
- streams.delete(handle);
81
- controller.close();
82
- },
83
- };
84
- streams.add(handle);
85
- handle.send(
86
- `data: ${JSON.stringify({
87
- ops: [{ op: "replace", path: [], value: state }],
88
- syn: { lastSeq, lease: "mock-lease" },
89
- })}\n\n`,
90
- );
91
- },
92
- cancel: () => {
93
- streams.delete(handle);
94
- },
95
- });
96
- signal?.addEventListener("abort", () => {
97
- for (const stream of [...streams]) {
98
- if (stream.clientId === id) stream.close();
99
- }
100
- });
101
- return new Response(body, {
102
- status: 200,
103
- headers: { "content-type": "text/event-stream" },
104
- });
105
- };
106
-
107
59
  const fakeFetch = (async (
108
60
  input: RequestInfo | URL,
109
61
  init?: RequestInit,
@@ -112,13 +64,31 @@ export const createMockTransport = <TState>(
112
64
  const url = String(input);
113
65
  const headers = new Headers(init?.headers);
114
66
  const id = headers.get("Statewire-Client-Id") ?? "";
115
- if (url.endsWith("/stream"))
116
- return handleStream(id, init?.signal ?? undefined);
67
+ if (url.endsWith("/stream")) {
68
+ clientId = id;
69
+ const lastSeq = clients.get(id) ?? 0;
70
+ clients.set(id, lastSeq);
71
+ return openSSEStream(
72
+ streams,
73
+ id,
74
+ (handle) => {
75
+ handle.send(
76
+ sseFrame({
77
+ ops: [{ op: "replace", path: [], value: state }],
78
+ syn: { lastSeq, lease: "mock-lease" },
79
+ }),
80
+ );
81
+ },
82
+ init?.signal ?? undefined,
83
+ );
84
+ }
117
85
  if (url.endsWith("/commands")) {
118
86
  const seq = Number(headers.get("Statewire-Command-Seq"));
119
87
  clients.set(id, Math.max(clients.get(id) ?? 0, seq));
120
88
  commands.push(
121
- JSON.parse(String(init?.body)) as { method: string; params: unknown[] },
89
+ JSON.parse(
90
+ String(init?.body),
91
+ ) as MockTransport<unknown>["commands"][number],
122
92
  );
123
93
  return Response.json({});
124
94
  }