statewire 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/README.md +2 -2
  2. package/dist/client/connection.d.ts +23 -19
  3. package/dist/client/connection.d.ts.map +1 -1
  4. package/dist/client/connection.js +143 -103
  5. package/dist/client/connection.js.map +1 -1
  6. package/dist/client/core.d.ts +30 -3
  7. package/dist/client/core.d.ts.map +1 -1
  8. package/dist/client/core.js +222 -260
  9. package/dist/client/core.js.map +1 -1
  10. package/dist/client/session.d.ts +34 -39
  11. package/dist/client/session.d.ts.map +1 -1
  12. package/dist/client/session.js +380 -373
  13. package/dist/client/session.js.map +1 -1
  14. package/dist/client/sessionStore.d.ts +1 -0
  15. package/dist/client/sessionStore.d.ts.map +1 -1
  16. package/dist/client/sessionStore.js.map +1 -1
  17. package/dist/client/timing.d.ts +1 -2
  18. package/dist/client/timing.d.ts.map +1 -1
  19. package/dist/client/timing.js +1 -5
  20. package/dist/client/timing.js.map +1 -1
  21. package/dist/host/draft.d.ts +2 -5
  22. package/dist/host/draft.d.ts.map +1 -1
  23. package/dist/host/draft.js.map +1 -1
  24. package/dist/host/host.d.ts +5 -8
  25. package/dist/host/host.d.ts.map +1 -1
  26. package/dist/host/host.js +23 -32
  27. package/dist/host/host.js.map +1 -1
  28. package/dist/host/socket.d.ts +3 -2
  29. package/dist/host/socket.d.ts.map +1 -1
  30. package/dist/host/socket.js +484 -424
  31. package/dist/host/socket.js.map +1 -1
  32. package/dist/host-internal.d.ts +4 -0
  33. package/dist/host-internal.d.ts.map +1 -1
  34. package/dist/host.d.ts +10 -10
  35. package/dist/host.d.ts.map +1 -1
  36. package/dist/host.js +2 -2
  37. package/dist/index.d.ts +1 -2
  38. package/dist/index.js +1 -2
  39. package/dist/statewire.d.ts +29 -24
  40. package/dist/statewire.d.ts.map +1 -1
  41. package/dist/statewire.js +8 -2
  42. package/dist/statewire.js.map +1 -1
  43. package/dist/transports/fake-sse.d.ts +1 -1
  44. package/dist/transports/fake-sse.d.ts.map +1 -1
  45. package/dist/transports/fake-sse.js +5 -2
  46. package/dist/transports/fake-sse.js.map +1 -1
  47. package/dist/transports/fixtures.d.ts +1 -2
  48. package/dist/transports/fixtures.d.ts.map +1 -1
  49. package/dist/transports/fixtures.js +5 -7
  50. package/dist/transports/fixtures.js.map +1 -1
  51. package/dist/transports/remote.d.ts +2 -1
  52. package/dist/transports/remote.d.ts.map +1 -1
  53. package/dist/transports/remote.js +10 -4
  54. package/dist/transports/remote.js.map +1 -1
  55. package/dist/transports/sse.d.ts +32 -46
  56. package/dist/transports/sse.d.ts.map +1 -1
  57. package/dist/transports/sse.js +56 -59
  58. package/dist/transports/sse.js.map +1 -1
  59. package/dist/transports/testing.d.ts +7 -5
  60. package/dist/transports/testing.d.ts.map +1 -1
  61. package/dist/transports/testing.js +41 -14
  62. package/dist/transports/testing.js.map +1 -1
  63. package/dist/transports/ws.d.ts +2 -3
  64. package/dist/transports/ws.d.ts.map +1 -1
  65. package/dist/transports/ws.js +25 -80
  66. package/dist/transports/ws.js.map +1 -1
  67. package/dist/types.d.ts +8 -8
  68. package/dist/types.d.ts.map +1 -1
  69. package/dist/types.js +3 -4
  70. package/dist/types.js.map +1 -1
  71. package/dist/wire/ops.d.ts +1 -5
  72. package/dist/wire/ops.d.ts.map +1 -1
  73. package/dist/wire/ops.js +1 -4
  74. package/dist/wire/ops.js.map +1 -1
  75. package/package.json +1 -1
  76. package/src/client/connection.ts +175 -128
  77. package/src/client/core.ts +287 -278
  78. package/src/client/session.ts +474 -480
  79. package/src/client/sessionStore.ts +1 -0
  80. package/src/client/timing.ts +0 -6
  81. package/src/host/draft.ts +1 -3
  82. package/src/host/host.ts +41 -58
  83. package/src/host/socket.ts +653 -448
  84. package/src/host-internal.ts +4 -0
  85. package/src/host.ts +13 -10
  86. package/src/index.ts +0 -1
  87. package/src/statewire.ts +38 -32
  88. package/src/transports/fake-sse.ts +2 -1
  89. package/src/transports/fixtures.ts +4 -9
  90. package/src/transports/remote.ts +12 -5
  91. package/src/transports/sse.ts +99 -115
  92. package/src/transports/testing.ts +47 -19
  93. package/src/transports/ws.ts +44 -115
  94. package/src/types.ts +15 -14
  95. package/src/wire/ops.ts +9 -16
@@ -1,88 +1,77 @@
1
1
  import { useRef, useState } from "react";
2
2
  import { flushTapSync, resource } from "@assistant-ui/tap";
3
3
  import type { ResourceElement } from "@assistant-ui/tap";
4
- import { encodeParams, reviveResponse } from "../wire/meta";
5
4
  import { createSseParser } from "./sse-parser";
6
5
  import { timing } from "../client/timing";
7
6
  import {
8
7
  classify,
9
8
  handleEnvelope,
10
- overloadedError,
11
- parseRetryAfter,
12
9
  useStatewireConn,
13
10
  } from "../client/connection";
14
11
  import type {
12
+ AttemptOutcome,
15
13
  Conn,
14
+ StatewireCmd,
16
15
  StatewireEnvelope,
17
- StatewireResponse,
16
+ StatewireSyn,
18
17
  } from "../client/connection";
19
18
  import type { Statewire } from "../types";
20
19
  import type { StatewireClient } from "../statewire";
21
20
  import type { StatewireHost } from "../host";
22
21
  import { useStatewireRemote } from "./remote";
23
22
 
23
+ /** A client frame's lanes: stamped statements, strictly consecutive per lane. */
24
+ export type StatewireFrame = {
25
+ ctx?: { ops: unknown[]; seq: number }[];
26
+ cmd?: { method: string; params: unknown[]; meta?: unknown; seq: number }[];
27
+ };
28
+
24
29
  export type StatewireRemoteProps = {
25
30
  clientId: string;
26
- onSnapshot: (args: { ops: unknown[]; lastSeq: number }) => void;
27
- onOps: (args: {
28
- ops: unknown[];
29
- res?: StatewireResponse[];
30
- ack?: number;
31
- }) => void;
31
+ /** Lanes to ride the attach frame (no-lease supersession only); records the included seqs. */
32
+ takeAttachLanes: () => StatewireFrame | null;
33
+ onHello: (args: { ops: unknown[]; syn: StatewireSyn }) => void;
34
+ onOps: (args: { ops: unknown[]; cmds: StatewireCmd[] }) => void;
35
+ onCmds: (cmds: StatewireCmd[]) => void;
36
+ onRefusal: (syn: StatewireSyn) => void;
37
+ onIdle: (level: boolean) => void;
38
+ onDetach: () => void;
32
39
  onFinish: (args: StatewireHost.Fin) => void;
33
40
  };
34
41
 
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
- });
53
-
54
42
  export type StatewireSubmitResult =
55
- | { type: "receipt"; response?: StatewireResponse }
56
- | { type: "rejected"; message: string }
57
- | { type: "seq-gap" }
58
- | { type: "unknown-client" }
59
- | { type: "stale-lease" };
43
+ | { type: "ok" }
44
+ | { type: "refusal"; syn: StatewireSyn }
45
+ | { type: "stale-lease" }
46
+ | { type: "no-lease" };
60
47
 
61
48
  export type ConnectionState = StatewireClient.ConnectionState;
62
49
 
63
- /** The live wire a remote session drives: connection status plus command POST. */
50
+ /** The live wire a remote session drives: connection status plus frame submission. */
64
51
  export type RemoteLink = {
65
52
  connection: StatewireClient.Connection;
66
- send: (args: {
67
- seq: number;
68
- command: StatewireWireCommand;
69
- }) => Promise<StatewireSubmitResult>;
53
+ send: (frame: StatewireFrame) => Promise<StatewireSubmitResult>;
70
54
  };
71
55
 
56
+ const LEASE_HEADER = "Statewire-Lease";
57
+
72
58
  const baseUrl = (options: StatewireClient.SSEOptions) =>
73
59
  options.url.replace(/\/$/, "");
74
60
 
75
61
  export type AuthState = { stale: boolean };
76
62
 
77
- const requestHeaders = async (
63
+ const resolveHeaders = async (
78
64
  options: StatewireClient.SSEOptions,
79
- extra: Record<string, string>,
80
65
  ctx: { staleAuth: boolean },
81
- ) => {
66
+ ): Promise<Headers> => {
82
67
  const { headers } = options;
83
- const merged = new Headers(
68
+ return new Headers(
84
69
  typeof headers === "function" ? await headers(ctx) : headers,
85
70
  );
71
+ };
72
+
73
+ const withExtra = (headers: Headers, extra: Record<string, string>) => {
74
+ const merged = new Headers(headers);
86
75
  for (const [name, value] of Object.entries(extra)) merged.set(name, value);
87
76
  return merged;
88
77
  };
@@ -105,7 +94,7 @@ const consume = async (
105
94
  reader: ReadableStreamDefaultReader<Uint8Array>,
106
95
  heartbeat: AbortController,
107
96
  dead: () => boolean,
108
- ): Promise<"stop" | "reattach" | "ended"> => {
97
+ ): Promise<AttemptOutcome> => {
109
98
  const decoder = new TextDecoder();
110
99
  const parse = createSseParser();
111
100
  for (;;) {
@@ -136,17 +125,16 @@ const attemptStream = async (
136
125
  options: StatewireClient.SSEOptions,
137
126
  auth: AuthState,
138
127
  abort: AbortController,
139
- ): Promise<"stop" | "reattach" | "ended"> => {
128
+ ): Promise<AttemptOutcome> => {
140
129
  const stale = auth.stale;
141
130
  auth.stale = false;
142
- const headers = await requestHeaders(
143
- options,
144
- {
145
- "Statewire-Client-Id": c.ctx.clientId,
146
- accept: "text/event-stream",
147
- },
148
- { staleAuth: stale },
149
- );
131
+ c.lease = null;
132
+ const userHeaders = await resolveHeaders(options, { staleAuth: stale });
133
+ const lanes = flushTapSync(() => c.ctx.takeAttachLanes());
134
+ const frame = {
135
+ headers: Object.fromEntries(userHeaders),
136
+ ...(lanes ?? {}),
137
+ };
150
138
  const heartbeat = new AbortController();
151
139
  const headerTimer = setTimeout(
152
140
  () => heartbeat.abort(),
@@ -158,7 +146,13 @@ const attemptStream = async (
158
146
  options,
159
147
  `${baseUrl(options)}/stream`,
160
148
  {
161
- headers,
149
+ method: "POST",
150
+ headers: withExtra(userHeaders, {
151
+ "content-type": "application/json",
152
+ "Statewire-Client-Id": c.ctx.clientId,
153
+ accept: "text/event-stream",
154
+ }),
155
+ body: JSON.stringify(frame),
162
156
  cache: "no-store",
163
157
  signal: AbortSignal.any([abort.signal, heartbeat.signal]),
164
158
  },
@@ -167,13 +161,13 @@ const attemptStream = async (
167
161
  } finally {
168
162
  clearTimeout(headerTimer);
169
163
  }
170
- if (res.status === 429 || res.status === 503) throw overloaded(res);
171
164
  if (res.status === 401) {
172
165
  if (stale) c.episode = { cause: "unauthorized" };
173
166
  auth.stale = true;
174
167
  throw new Error("unauthorized");
175
168
  }
176
169
  if (!res.ok || !res.body) throw new Error(`HTTP ${res.status}`);
170
+ c.lease = res.headers.get(LEASE_HEADER);
177
171
  const reader = res.body.getReader();
178
172
  try {
179
173
  return await consume(c, reader, heartbeat, () => abort.signal.aborted);
@@ -182,32 +176,25 @@ const attemptStream = async (
182
176
  }
183
177
  };
184
178
 
185
- const overloaded = (res: Response) =>
186
- overloadedError(parseRetryAfter(res.headers.get("Retry-After")));
187
-
188
- const postCommand = async (
179
+ const postFrame = async (
189
180
  c: Conn,
190
181
  options: StatewireClient.SSEOptions,
191
- { seq, command }: { seq: number; command: StatewireWireCommand },
182
+ lease: string,
183
+ frame: StatewireFrame,
192
184
  timeout: AbortSignal,
193
185
  stale: boolean,
194
186
  ): Promise<Response> =>
195
187
  timedFetch(
196
188
  options,
197
- `${baseUrl(options)}/commands`,
189
+ `${baseUrl(options)}/frames`,
198
190
  {
199
191
  method: "POST",
200
- headers: await requestHeaders(
201
- options,
202
- {
203
- "content-type": "application/json",
204
- "Statewire-Client-Id": c.ctx.clientId,
205
- "Statewire-Command-Seq": String(seq),
206
- ...(c.lease !== null && { "Statewire-Lease": c.lease }),
207
- },
208
- { staleAuth: stale },
209
- ),
210
- body: JSON.stringify(encodeWireCommand(command)),
192
+ headers: withExtra(await resolveHeaders(options, { staleAuth: stale }), {
193
+ "content-type": "application/json",
194
+ "Statewire-Client-Id": c.ctx.clientId,
195
+ [LEASE_HEADER]: lease,
196
+ }),
197
+ body: JSON.stringify(frame),
211
198
  signal: AbortSignal.any([
212
199
  c.lifetime?.signal ?? AbortSignal.abort(),
213
200
  timeout,
@@ -216,32 +203,51 @@ const postCommand = async (
216
203
  timeout,
217
204
  );
218
205
 
219
- const sendCommand = async (
206
+ // Delta-seconds only; anything else reads as absent.
207
+ const retryAfterMs = (res: Response): number | null => {
208
+ const header = res.headers.get("retry-after")?.trim();
209
+ return header !== undefined && /^\d+$/.test(header)
210
+ ? Number(header) * 1000
211
+ : null;
212
+ };
213
+
214
+ const refusalSyn = async (res: Response): Promise<StatewireSyn> => {
215
+ const body = (await res.json().catch(() => null)) as {
216
+ syn?: StatewireSyn;
217
+ } | null;
218
+ const syn = body?.syn;
219
+ if (typeof syn !== "object" || syn === null) {
220
+ throw new Error(`HTTP ${res.status}`);
221
+ }
222
+ return syn;
223
+ };
224
+
225
+ const sendFrame = async (
220
226
  c: Conn,
221
227
  options: StatewireClient.SSEOptions,
222
228
  auth: AuthState,
223
- args: { seq: number; command: StatewireWireCommand },
229
+ frame: StatewireFrame,
224
230
  ): Promise<StatewireSubmitResult> => {
231
+ const lease = c.lease;
232
+ if (lease === null) return { type: "no-lease" };
225
233
  const timeout = AbortSignal.timeout(timing.heartbeatTimeoutMs);
226
- let res = await postCommand(c, options, args, timeout, false);
234
+ let res = await postFrame(c, options, lease, frame, timeout, false);
227
235
  if (res.status === 401 && typeof options.headers === "function") {
228
- res = await postCommand(c, options, args, timeout, true);
236
+ res = await postFrame(c, options, lease, frame, timeout, true);
229
237
  }
230
- if (res.ok) {
231
- let body: { res?: StatewireResponse } | null;
232
- try {
233
- body = (await res.json()) as { res?: StatewireResponse } | null;
234
- } catch {
235
- return {
236
- type: "rejected",
237
- message: "statewire: protocol error: malformed /commands receipt body",
238
- };
239
- }
240
- return body?.res !== undefined
241
- ? { type: "receipt", response: reviveResponse(body.res) }
242
- : { type: "receipt" };
238
+ if (res.ok) return { type: "ok" };
239
+ if (res.status === 429 || res.status === 409) {
240
+ // The syn body retry wins; Retry-After covers 429s minted below the host.
241
+ const headerRetry = res.status === 429 ? retryAfterMs(res) : null;
242
+ const syn: StatewireSyn = await refusalSyn(res).catch((error: unknown) => {
243
+ if (headerRetry === null) throw error;
244
+ return { retry: headerRetry };
245
+ });
246
+ if (syn.retry === undefined && headerRetry !== null)
247
+ syn.retry = headerRetry;
248
+ return { type: "refusal", syn };
243
249
  }
244
- if (res.status === 429 || res.status === 503) throw overloaded(res);
250
+ if (res.status === 423) return { type: "stale-lease" };
245
251
  if (res.status === 401) {
246
252
  // Fresh credentials did not clear the 401: signal auth-failed on the
247
253
  // connection and let the send retry as a transport fault.
@@ -250,27 +256,6 @@ const sendCommand = async (
250
256
  c.connection.reconnect();
251
257
  throw new Error("unauthorized");
252
258
  }
253
- // Bodies settle, statuses pace: only a statewire-shaped body carries a
254
- // verdict; any bare status is middleware noise — a transport fault, retried.
255
- const body = (await res.json().catch(() => null)) as {
256
- error?: string;
257
- rejections?: { message?: string }[];
258
- } | null;
259
- if (body?.error === "rejected") {
260
- return {
261
- type: "rejected",
262
- message: body.rejections?.[0]?.message ?? `HTTP ${res.status}`,
263
- };
264
- }
265
- if (res.status === 409 || res.status === 412 || res.status === 423) {
266
- const verdict =
267
- res.status === 409
268
- ? ("seq-gap" as const)
269
- : res.status === 412
270
- ? ("unknown-client" as const)
271
- : ("stale-lease" as const);
272
- if (body?.error === verdict) return { type: verdict };
273
- }
274
259
  throw new Error(`HTTP ${res.status}`);
275
260
  };
276
261
 
@@ -292,7 +277,7 @@ const useSseLink = (
292
277
  get connection() {
293
278
  return c.connection;
294
279
  },
295
- send: (args) => sendCommand(c, latest.current, auth, args),
280
+ send: (frame) => sendFrame(c, latest.current, auth, frame),
296
281
  }),
297
282
  );
298
283
  return link;
@@ -300,12 +285,11 @@ const useSseLink = (
300
285
 
301
286
  const useStatewireSSE = (
302
287
  options: StatewireClient.SSEOptions,
303
- ): Statewire<any, any, any> => useStatewireRemote(options, useSseLink);
288
+ ): Statewire<any, any> => useStatewireRemote(options, useSseLink);
304
289
 
305
290
  export const StatewireSSE = resource(useStatewireSSE) as <
306
291
  TState = any,
307
292
  TCommands extends Statewire.Commands = Statewire.Commands,
308
- TEvent = any,
309
293
  >(
310
294
  options: StatewireClient.SSEOptions,
311
- ) => ResourceElement<Statewire<TState | undefined, TCommands, TEvent>>;
295
+ ) => ResourceElement<Statewire<TState | undefined, TCommands>>;
@@ -1,6 +1,7 @@
1
1
  import type { ResourceElement } from "@assistant-ui/tap";
2
2
  import type { StatewireEnvelope } from "../client/connection";
3
3
  import { StatewireSSE } from "./sse";
4
+ import type { StatewireFrame } from "./sse";
4
5
  import type { Statewire } from "../types";
5
6
  import { openSSEStream, sseFrame } from "./fake-sse";
6
7
  import type { FakeStreamHandle } from "./fake-sse";
@@ -14,13 +15,15 @@ export { applyOptimistic } from "../util/recordMutations";
14
15
 
15
16
  export type MockTransport<TState> = {
16
17
  /** Pass as the `transport:` option of useStatewire / Harness. */
17
- transport: ResourceElement<Statewire<TState | undefined, any, any>>;
18
- /** Captured `/commands` POST bodies, in POST order. */
18
+ transport: ResourceElement<Statewire<TState | undefined, any>>;
19
+ /** Captured statements in arrival order: cmd as `{method, params}`, ctx as `{context}`. */
19
20
  commands: { method?: string; params?: unknown[]; context?: unknown }[];
20
21
  /** Replace the replicated state and broadcast it as a root snapshot op. */
21
22
  setState: (state: TState) => Promise<void>;
22
- /** Deliver an ack watermark on the stream. */
23
- ack: (seq: number) => Promise<void>;
23
+ /** Deliver a bare ack cmd packet on the stream. */
24
+ applied: (seq: number) => Promise<void>;
25
+ /** Deliver a terminal result cmd packet on the stream. */
26
+ settle: (seq: number, payload?: unknown) => Promise<void>;
24
27
  /** Deliver an arbitrary envelope on the stream. */
25
28
  sendEnvelope: (envelope: StatewireEnvelope) => Promise<void>;
26
29
  /** End the stream with a fin; further attaches fail like a dead server. */
@@ -31,8 +34,8 @@ export type MockTransport<TState> = {
31
34
 
32
35
  /**
33
36
  * A mock statewire backend: serves `initialState` as the attach snapshot,
34
- * records command POSTs (receipt only — acks and results are delivered from
35
- * test code), and exposes the wire as a transport ResourceElement.
37
+ * records submitted frames (receipt only — acks and results are delivered
38
+ * from test code), and exposes the wire as a transport ResourceElement.
36
39
  */
37
40
  let mockCount = 0;
38
41
 
@@ -42,7 +45,7 @@ export const createMockTransport = <TState>(
42
45
  let state: unknown = initialState;
43
46
  let clientId = "";
44
47
  let finished = false;
45
- const clients = new Map<string, number>();
48
+ const clients = new Map<string, { seq: number; ctxSeq: number }>();
46
49
  const streams = new Set<FakeStreamHandle>();
47
50
  const commands: MockTransport<unknown>["commands"] = [];
48
51
 
@@ -51,6 +54,19 @@ export const createMockTransport = <TState>(
51
54
  return tick();
52
55
  };
53
56
 
57
+ const admit = (id: string, frame: StatewireFrame) => {
58
+ const rec = clients.get(id)!;
59
+ for (const statement of frame.ctx ?? []) {
60
+ rec.ctxSeq = Math.max(rec.ctxSeq, statement.seq);
61
+ const root = statement.ops[0] as { value?: unknown } | undefined;
62
+ commands.push({ context: root?.value });
63
+ }
64
+ for (const statement of frame.cmd ?? []) {
65
+ rec.seq = Math.max(rec.seq, statement.seq);
66
+ commands.push({ method: statement.method, params: statement.params });
67
+ }
68
+ };
69
+
54
70
  const fakeFetch = (async (
55
71
  input: RequestInfo | URL,
56
72
  init?: RequestInit,
@@ -61,8 +77,11 @@ export const createMockTransport = <TState>(
61
77
  const id = headers.get("Statewire-Client-Id") ?? "";
62
78
  if (url.endsWith("/stream")) {
63
79
  clientId = id;
64
- const lastSeq = clients.get(id) ?? 0;
65
- clients.set(id, lastSeq);
80
+ const known = clients.has(id);
81
+ if (!known) clients.set(id, { seq: 0, ctxSeq: 0 });
82
+ const rec = clients.get(id)!;
83
+ const frame = JSON.parse(String(init?.body)) as StatewireFrame;
84
+ admit(id, frame);
66
85
  return openSSEStream(
67
86
  streams,
68
87
  id,
@@ -70,21 +89,20 @@ export const createMockTransport = <TState>(
70
89
  handle.send(
71
90
  sseFrame({
72
91
  ops: [{ op: "replace", path: [], value: state }],
73
- syn: { lastSeq, lease: "mock-lease" },
92
+ syn: {
93
+ seq: rec.seq,
94
+ ctxSeq: rec.ctxSeq,
95
+ record: known ? "resumed" : "fresh",
96
+ },
74
97
  }),
75
98
  );
76
99
  },
77
100
  init?.signal ?? undefined,
101
+ { "Statewire-Lease": "mock-lease" },
78
102
  );
79
103
  }
80
- if (url.endsWith("/commands")) {
81
- const seq = Number(headers.get("Statewire-Command-Seq"));
82
- clients.set(id, Math.max(clients.get(id) ?? 0, seq));
83
- commands.push(
84
- JSON.parse(
85
- String(init?.body),
86
- ) as MockTransport<unknown>["commands"][number],
87
- );
104
+ if (url.endsWith("/frames")) {
105
+ admit(id, JSON.parse(String(init?.body)) as StatewireFrame);
88
106
  return Response.json({});
89
107
  }
90
108
  return new Response(null, { status: 404 });
@@ -100,7 +118,17 @@ export const createMockTransport = <TState>(
100
118
  state = next;
101
119
  return sendEnvelope({ ops: [{ op: "replace", path: [], value: next }] });
102
120
  },
103
- ack: (seq) => sendEnvelope({ ack: seq }),
121
+ applied: (seq) => sendEnvelope({ cmd: [{ seq, type: "applied" }] }),
122
+ settle: (seq, payload) =>
123
+ sendEnvelope({
124
+ cmd: [
125
+ {
126
+ seq,
127
+ type: "result",
128
+ ...(payload !== undefined && { payload }),
129
+ },
130
+ ],
131
+ }),
104
132
  sendEnvelope,
105
133
  finish: async (reason, message) => {
106
134
  await sendEnvelope({