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,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,7 +29,7 @@ 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
35
  export type StatewireWireCommand = {
@@ -40,7 +39,7 @@ export type StatewireWireCommand = {
40
39
 
41
40
  export type StatewireSubmitResult =
42
41
  | { type: "receipt"; response?: StatewireResponse }
43
- | { type: "rejected"; message: string; payload?: unknown }
42
+ | { type: "rejected"; message: string }
44
43
  | { type: "seq-gap" }
45
44
  | { type: "unknown-client" }
46
45
  | { type: "stale-lease" };
@@ -50,7 +49,7 @@ export type ConnectionState =
50
49
  | { status: "live" }
51
50
  | {
52
51
  status: "reconnecting";
53
- cause: "dropped" | "ended" | "error";
52
+ cause: "dropped" | "evicted" | "error";
54
53
  attempt: number;
55
54
  nextRetryAt: number;
56
55
  lastError?: string;
@@ -58,61 +57,23 @@ export type ConnectionState =
58
57
  }
59
58
  | { status: "gone"; message?: string; payload?: unknown };
60
59
 
61
- export type StatewireConnection = ConnectionState & { reconnect: () => void };
62
-
63
60
  /** The live wire a remote session drives: connection status plus command POST. */
64
61
  export type RemoteLink = {
65
- connection: StatewireConnection;
62
+ connection: StatewireClient.Connection;
66
63
  send: (args: {
67
64
  seq: number;
68
65
  command: StatewireWireCommand;
69
66
  }) => Promise<StatewireSubmitResult>;
70
67
  };
71
68
 
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
- };
84
-
85
- type Conn = ConnBase & {
86
- latest: { readonly current: Latest };
87
- send: RemoteLink["send"];
88
- };
69
+ const baseUrl = (options: StatewireClient.SSEOptions) =>
70
+ options.url.replace(/\/$/, "");
89
71
 
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;
72
+ const requestHeaders = async (
73
+ options: StatewireClient.SSEOptions,
74
+ extra: Record<string, string>,
75
+ ) => {
76
+ const { headers } = options;
116
77
  const merged = new Headers(
117
78
  typeof headers === "function" ? await headers() : headers,
118
79
  );
@@ -121,16 +82,13 @@ const requestHeaders = async (c: Conn, extra: Record<string, string>) => {
121
82
  };
122
83
 
123
84
  const timedFetch = async (
124
- c: Conn,
85
+ options: StatewireClient.SSEOptions,
125
86
  input: string,
126
87
  init: RequestInit & { signal: AbortSignal },
127
88
  timeout: AbortSignal,
128
89
  ): Promise<Response> => {
129
90
  try {
130
- return await (c.latest.current.options.fetch ?? globalThis.fetch)(
131
- input,
132
- init,
133
- );
91
+ return await (options.fetch ?? globalThis.fetch)(input, init);
134
92
  } catch {
135
93
  throw classify(timeout);
136
94
  }
@@ -144,7 +102,6 @@ const consume = async (
144
102
  ): Promise<"stop" | "reattach" | "ended"> => {
145
103
  const decoder = new TextDecoder();
146
104
  const parse = createSseParser();
147
- let attached = false;
148
105
  for (;;) {
149
106
  const timer = setTimeout(
150
107
  () => heartbeat.abort(),
@@ -162,8 +119,7 @@ const consume = async (
162
119
  for (const data of parse(decoder.decode(result.value, { stream: true }))) {
163
120
  if (dead()) return "ended";
164
121
  const env = JSON.parse(data) as StatewireEnvelope;
165
- const done = flushTapSync(() => handleEnvelope(c, env, attached));
166
- attached = true;
122
+ const done = flushTapSync(() => handleEnvelope(c, env));
167
123
  if (done) return done;
168
124
  }
169
125
  }
@@ -171,10 +127,11 @@ const consume = async (
171
127
 
172
128
  const attemptStream = async (
173
129
  c: Conn,
130
+ options: StatewireClient.SSEOptions,
174
131
  abort: AbortController,
175
132
  ): Promise<"stop" | "reattach" | "ended"> => {
176
- const headers = await requestHeaders(c, {
177
- "Statewire-Client-Id": c.latest.current.ctx.clientId,
133
+ const headers = await requestHeaders(options, {
134
+ "Statewire-Client-Id": c.ctx.clientId,
178
135
  accept: "text/event-stream",
179
136
  });
180
137
  const heartbeat = new AbortController();
@@ -185,8 +142,8 @@ const attemptStream = async (
185
142
  let res: Response;
186
143
  try {
187
144
  res = await timedFetch(
188
- c,
189
- `${baseUrl(c)}/stream`,
145
+ options,
146
+ `${baseUrl(options)}/stream`,
190
147
  {
191
148
  headers,
192
149
  cache: "no-store",
@@ -212,17 +169,18 @@ const overloaded = (res: Response) =>
212
169
 
213
170
  const sendCommand = async (
214
171
  c: Conn,
172
+ options: StatewireClient.SSEOptions,
215
173
  { seq, command }: { seq: number; command: StatewireWireCommand },
216
174
  ): Promise<StatewireSubmitResult> => {
217
175
  const timeout = AbortSignal.timeout(timing.heartbeatTimeoutMs);
218
176
  const res = await timedFetch(
219
- c,
220
- `${baseUrl(c)}/commands`,
177
+ options,
178
+ `${baseUrl(options)}/commands`,
221
179
  {
222
180
  method: "POST",
223
- headers: await requestHeaders(c, {
181
+ headers: await requestHeaders(options, {
224
182
  "content-type": "application/json",
225
- "Statewire-Client-Id": c.latest.current.ctx.clientId,
183
+ "Statewire-Client-Id": c.ctx.clientId,
226
184
  "Statewire-Command-Seq": String(seq),
227
185
  ...(c.lease !== null && { "Statewire-Lease": c.lease }),
228
186
  }),
@@ -238,9 +196,15 @@ const sendCommand = async (
238
196
  timeout,
239
197
  );
240
198
  if (res.ok) {
241
- const body = (await res.json().catch(() => null)) as {
242
- res?: StatewireResponse;
243
- } | null;
199
+ let body: { res?: StatewireResponse } | null;
200
+ try {
201
+ body = (await res.json()) as { res?: StatewireResponse } | null;
202
+ } catch {
203
+ return {
204
+ type: "rejected",
205
+ message: "statewire: protocol error: malformed /commands receipt body",
206
+ };
207
+ }
244
208
  return body?.res !== undefined
245
209
  ? { type: "receipt", response: reviveResponse(body.res) }
246
210
  : { type: "receipt" };
@@ -267,14 +231,37 @@ const sendCommand = async (
267
231
  throw new Error(`HTTP ${res.status}`);
268
232
  };
269
233
 
234
+ const useSseLink = (
235
+ options: StatewireClient.SSEOptions,
236
+ ctx: StatewireRemoteProps,
237
+ ): RemoteLink => {
238
+ const latest = useRef(options);
239
+ latest.current = options;
240
+ const c = useStatewireConn({
241
+ url: options.url,
242
+ onChange: options.onChange,
243
+ ctx,
244
+ attemptOnce: (c, abort) => attemptStream(c, options, abort),
245
+ });
246
+ const [link] = useState(
247
+ (): RemoteLink => ({
248
+ get connection() {
249
+ return c.connection;
250
+ },
251
+ send: (args) => sendCommand(c, latest.current, args),
252
+ }),
253
+ );
254
+ return link;
255
+ };
256
+
270
257
  const useStatewireSSE = (
271
- options: StatewireSSEOptions,
272
- ): StatewireTransport<any, any, any> => useStatewireRemote(options, createConn);
258
+ options: StatewireClient.SSEOptions,
259
+ ): Statewire<any, any, any> => useStatewireRemote(options, useSseLink);
273
260
 
274
261
  export const StatewireSSE = resource(useStatewireSSE) as <
275
262
  TState = any,
276
- TCommands extends StatewireCommands = StatewireCommands,
263
+ TCommands extends Statewire.Commands = Statewire.Commands,
277
264
  TEvent = any,
278
265
  >(
279
- options: StatewireSSEOptions,
280
- ) => ResourceElement<StatewireTransport<TState | undefined, TCommands, TEvent>>;
266
+ options: StatewireClient.SSEOptions,
267
+ ) => 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,15 +17,9 @@ 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>>;
22
+ transport: ResourceElement<Statewire<TState | undefined, any, any>>;
25
23
  /** Captured `{ method, params }` command bodies, in POST order. */
26
24
  commands: { method: string; params: unknown[] }[];
27
25
  /** Replace the replicated state and broadcast it as a root snapshot op. */
@@ -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>();
51
+ const streams = new Set<FakeStreamHandle>();
54
52
  const commands: { method: string; params: unknown[] }[] = [];
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,8 +64,24 @@ 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));