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,26 +1,16 @@
1
1
  import { useEffect, useRef, useState } from "react";
2
2
  import { flushTapSync } from "@assistant-ui/tap";
3
3
  import { reviveOps, reviveResponse, reviveValue } from "../wire/meta";
4
- import { backoff } from "./timing";
5
- import type {
6
- ConnectionState,
7
- RemoteLink,
8
- StatewireConnection,
9
- StatewireTransportProps,
10
- } from "../transports/sse";
4
+ import { retryDelay } from "./timing";
5
+ import type { ConnectionState, StatewireRemoteProps } from "../transports/sse";
6
+ import type { StatewireClient } from "../statewire";
7
+ import type { StatewireHost } from "../host";
11
8
 
12
9
  export type Episode = {
13
- cause: "dropped" | "ended" | "error";
10
+ cause: "dropped" | "evicted" | "error";
14
11
  message?: string;
15
12
  };
16
13
 
17
- export type StatewireFinish = {
18
- reason: "ended" | "error" | "gone" | "superseded" | (string & {});
19
- message?: string;
20
- payload?: unknown;
21
- meta?: unknown;
22
- };
23
-
24
14
  export type StatewireResponse = {
25
15
  seq: number;
26
16
  type: "accepted" | "rejected" | "pending" | "crashed" | "result-unavailable";
@@ -40,10 +30,10 @@ export type StatewireEnvelope = {
40
30
  res?: StatewireResponse[];
41
31
  ack?: number;
42
32
  syn?: StatewireSyn;
43
- fin?: StatewireFinish;
33
+ fin?: StatewireHost.Fin;
44
34
  };
45
35
 
46
- export const createGate = () => {
36
+ const createGate = () => {
47
37
  let latched = false;
48
38
  let wake: (() => void) | undefined;
49
39
  return {
@@ -81,8 +71,8 @@ export const createGate = () => {
81
71
 
82
72
  export type Gate = ReturnType<typeof createGate>;
83
73
 
84
- export type ConnBase = {
85
- connection: StatewireConnection;
74
+ export type Conn = {
75
+ connection: StatewireClient.Connection;
86
76
  hasSnapshot: boolean;
87
77
  lease: string | null;
88
78
  attempt: number;
@@ -90,37 +80,33 @@ export type ConnBase = {
90
80
  gate: Gate;
91
81
  abort: AbortController | null;
92
82
  lifetime: AbortController | null;
93
- latest: {
94
- readonly current: {
95
- options: { onChange?: (connection: StatewireConnection) => void };
96
- ctx: StatewireTransportProps;
97
- };
98
- };
99
- notify: (connection: StatewireConnection) => void;
100
- reconnect: () => void;
83
+ ctx: StatewireRemoteProps;
84
+ onChange: (connection: StatewireClient.Connection) => void;
101
85
  attemptOnce: (
102
86
  abort: AbortController,
103
87
  ) => Promise<"stop" | "reattach" | "ended">;
88
+ notify: (connection: StatewireClient.Connection) => void;
89
+ reconnect: () => void;
104
90
  };
105
91
 
106
- export const setStatus = (c: ConnBase, next: ConnectionState) => {
92
+ const setStatus = (c: Conn, next: ConnectionState) => {
107
93
  const changed = next.status !== c.connection.status;
108
94
  c.connection = { ...next, reconnect: c.reconnect };
109
95
  c.notify(c.connection);
110
- if (changed) c.latest.current.options.onChange?.(c.connection);
96
+ if (changed) c.onChange(c.connection);
111
97
  };
112
98
 
113
- export const deliverEnvelope = (
114
- c: ConnBase,
99
+ const deliverEnvelope = (
100
+ c: Conn,
115
101
  env: Pick<StatewireEnvelope, "ops" | "res" | "ack" | "syn">,
116
- attached: boolean,
117
102
  ) => {
118
103
  if (env.syn?.lease !== undefined) c.lease = env.syn.lease;
119
- const ctx = c.latest.current.ctx;
104
+ const ctx = c.ctx;
120
105
  const ops = env.ops === undefined ? [] : reviveOps(env.ops);
121
106
  const res = env.res?.map(reviveResponse);
122
- if (!attached) {
123
- ctx.onSnapshot({ ops, lastSeq: env.syn?.lastSeq ?? -1 });
107
+ if (env.syn !== undefined) {
108
+ ctx.onSnapshot({ ops, lastSeq: env.syn.lastSeq });
109
+ c.hasSnapshot = true;
124
110
  } else if (
125
111
  env.ops !== undefined ||
126
112
  env.res !== undefined ||
@@ -132,15 +118,14 @@ export const deliverEnvelope = (
132
118
  ...(env.ack !== undefined && { ack: env.ack }),
133
119
  });
134
120
  }
135
- c.hasSnapshot = true;
136
121
  };
137
122
 
138
- export const isTransientFinish = (
139
- reason: StatewireFinish["reason"],
140
- ): reason is "ended" | "error" => reason === "ended" || reason === "error";
123
+ const isTransientFinish = (
124
+ reason: StatewireHost.Fin["reason"],
125
+ ): reason is "evicted" | "error" => reason === "evicted" || reason === "error";
141
126
 
142
- export const settleEnvelope = (
143
- c: ConnBase,
127
+ const settleEnvelope = (
128
+ c: Conn,
144
129
  env: StatewireEnvelope,
145
130
  ): "stop" | "reattach" | null => {
146
131
  let finish = env.fin;
@@ -149,7 +134,7 @@ export const settleEnvelope = (
149
134
  const { meta, ...rest } = finish;
150
135
  finish = { ...rest, payload: reviveValue(rest.payload, meta) };
151
136
  }
152
- c.latest.current.ctx.onFinish(finish);
137
+ c.ctx.onFinish(finish);
153
138
  if (isTransientFinish(finish.reason)) {
154
139
  c.episode = {
155
140
  cause: finish.reason,
@@ -173,23 +158,22 @@ export const settleEnvelope = (
173
158
  };
174
159
 
175
160
  export const handleEnvelope = (
176
- c: ConnBase,
161
+ c: Conn,
177
162
  env: StatewireEnvelope,
178
- attached: boolean,
179
163
  ): "stop" | "reattach" | null => {
180
- deliverEnvelope(c, env, attached);
164
+ deliverEnvelope(c, env);
181
165
  return settleEnvelope(c, env);
182
166
  };
183
167
 
184
- export const scheduleRetry = (
185
- c: ConnBase,
168
+ const scheduleRetry = (
169
+ c: Conn,
186
170
  failure: string | undefined,
187
171
  immediate: boolean,
188
- retryAfterMs?: number,
172
+ cause?: unknown,
189
173
  ) => {
190
174
  c.attempt += 1;
191
175
  c.episode ??= { cause: "dropped" };
192
- const delay = immediate ? 0 : (retryAfterMs ?? backoff(c.attempt));
176
+ const delay = immediate ? 0 : retryDelay(cause, c.attempt);
193
177
  if (c.hasSnapshot) {
194
178
  setStatus(c, {
195
179
  status: "reconnecting",
@@ -202,7 +186,7 @@ export const scheduleRetry = (
202
186
  return delay;
203
187
  };
204
188
 
205
- export const reconnectConn = (c: ConnBase) => {
189
+ const reconnectConn = (c: Conn) => {
206
190
  if (!c.lifetime) return;
207
191
  if (c.connection.status === "gone") {
208
192
  c.attempt = 0;
@@ -216,13 +200,13 @@ export const reconnectConn = (c: ConnBase) => {
216
200
  c.abort?.abort();
217
201
  };
218
202
 
219
- const run = async (c: ConnBase, lifetime: AbortController, gate: Gate) => {
203
+ const run = async (c: Conn, lifetime: AbortController, gate: Gate) => {
220
204
  const alive = () => c.lifetime === lifetime;
221
205
  while (alive()) {
222
206
  const abort = new AbortController();
223
207
  c.abort = abort;
224
208
  let failure: string | undefined;
225
- let retryAfterMs: number | undefined;
209
+ let cause: unknown;
226
210
  let gone = false;
227
211
  try {
228
212
  gone = (await c.attemptOnce(abort)) === "stop";
@@ -230,13 +214,7 @@ const run = async (c: ConnBase, lifetime: AbortController, gate: Gate) => {
230
214
  if (!abort.signal.aborted) {
231
215
  failure =
232
216
  error instanceof Error && error.message ? error.message : "network";
233
- if (
234
- error instanceof Error &&
235
- "retryAfterMs" in error &&
236
- typeof error.retryAfterMs === "number"
237
- ) {
238
- retryAfterMs = error.retryAfterMs;
239
- }
217
+ cause = error;
240
218
  }
241
219
  }
242
220
  abort.abort();
@@ -247,69 +225,65 @@ const run = async (c: ConnBase, lifetime: AbortController, gate: Gate) => {
247
225
  }
248
226
  const immediate = gate.take();
249
227
  const delay = flushTapSync(() =>
250
- scheduleRetry(c, failure, immediate, retryAfterMs),
228
+ scheduleRetry(c, failure, immediate, cause),
251
229
  );
252
230
  if (delay === 0) continue;
253
231
  await gate.wait(delay);
254
232
  }
255
233
  };
256
234
 
257
- const startConn = (c: ConnBase) => {
258
- c.hasSnapshot = false;
259
- c.lease = null;
260
- c.attempt = 0;
261
- c.episode = null;
262
- if (c.connection.status !== "connecting") {
263
- setStatus(c, { status: "connecting" });
264
- }
265
- c.lifetime = new AbortController();
266
- c.gate = createGate();
267
- void run(c, c.lifetime, c.gate);
268
- };
269
-
270
- const stopConn = (c: ConnBase) => {
271
- c.abort?.abort();
272
- c.lifetime?.abort();
273
- c.lifetime = null;
274
- c.gate.open();
275
- };
276
-
277
- export const useStatewireConn = <
278
- TOptions extends {
279
- url: string;
280
- onChange?: (connection: StatewireConnection) => void;
281
- },
282
- >(
283
- options: TOptions,
284
- ctx: StatewireTransportProps,
285
- create: (
286
- latest: {
287
- readonly current: {
288
- options: TOptions;
289
- ctx: StatewireTransportProps;
290
- };
291
- },
292
- notify: (connection: StatewireConnection) => void,
293
- ) => ConnBase & { send: RemoteLink["send"] },
294
- ): RemoteLink => {
295
- const latest = useRef({ options, ctx });
296
- const [connection, setConnection] = useState<StatewireConnection | null>(
297
- null,
298
- );
299
- const [c] = useState(() => create(latest, setConnection));
300
-
301
- useEffect(() => {
302
- latest.current = { options, ctx };
235
+ export const useStatewireConn = (props: {
236
+ url: string;
237
+ onChange?: ((connection: StatewireClient.Connection) => void) | undefined;
238
+ ctx: StatewireRemoteProps;
239
+ attemptOnce: (
240
+ c: Conn,
241
+ abort: AbortController,
242
+ ) => Promise<"stop" | "reattach" | "ended">;
243
+ }): Conn => {
244
+ const latest = useRef(props);
245
+ latest.current = props;
246
+ const [, setConnection] = useState<StatewireClient.Connection | null>(null);
247
+ const [c] = useState((): Conn => {
248
+ const conn: Conn = {
249
+ hasSnapshot: false,
250
+ lease: null,
251
+ attempt: 0,
252
+ episode: null,
253
+ abort: null,
254
+ gate: createGate(),
255
+ lifetime: null,
256
+ ctx: props.ctx,
257
+ onChange: (connection) => latest.current.onChange?.(connection),
258
+ attemptOnce: (abort) => latest.current.attemptOnce(conn, abort),
259
+ notify: setConnection,
260
+ reconnect: () => reconnectConn(conn),
261
+ connection: { status: "connecting", reconnect: () => conn.reconnect() },
262
+ };
263
+ return conn;
303
264
  });
265
+
304
266
  useEffect(() => {
305
- startConn(c);
306
- return () => stopConn(c);
307
- }, [c, options.url]);
267
+ c.hasSnapshot = false;
268
+ c.lease = null;
269
+ c.attempt = 0;
270
+ c.episode = null;
271
+ if (c.connection.status !== "connecting") {
272
+ setStatus(c, { status: "connecting" });
273
+ }
274
+ const lifetime = new AbortController();
275
+ c.lifetime = lifetime;
276
+ c.gate = createGate();
277
+ void run(c, lifetime, c.gate);
278
+ return () => {
279
+ c.abort?.abort();
280
+ lifetime.abort();
281
+ if (c.lifetime === lifetime) c.lifetime = null;
282
+ c.gate.open();
283
+ };
284
+ }, [c, props.url]);
308
285
 
309
- return {
310
- connection: connection ?? c.connection,
311
- send: c.send,
312
- };
286
+ return c;
313
287
  };
314
288
 
315
289
  export const classify = (timeout: AbortSignal) =>
@@ -1,27 +1,21 @@
1
+ import { useEffect, useState } from "react";
1
2
  import { createChanges } from "./changes";
2
3
  import { getCommandsHost, getEmitHost, getStateHost } from "../host/host";
3
4
  import type { CommandsInvocation, CommandsListener } from "../host/host";
4
- import { recordMutations } from "./optimistic";
5
+ import { recordMutations } from "../util/recordMutations";
6
+ import { createMacrotaskPoster } from "../util/macrotask";
7
+ import { createMethodProxy } from "../util/methodProxy";
5
8
  import type { StatewirePath } from "../wire/ops";
6
9
  import { StatewireSendError } from "../types";
7
- import type {
8
- SendFailure as StatewireSendFailure,
9
- StatewireStateSource,
10
- StatewireTransport,
11
- StatewireCommand,
12
- StatewireCommandsProxy,
13
- StatewireCommandUpdate,
14
- StatewireOptimisticContext,
15
- StatewireOptions,
16
- } from "../types";
17
- import type { StatewireConnection } from "../transports/sse";
10
+ import type { Statewire } from "../types";
11
+ import type { StatewireClient } from "../statewire";
18
12
 
19
- export const LIVE_CONNECTION: StatewireConnection = {
13
+ export const LIVE_CONNECTION: StatewireClient.Connection = {
20
14
  status: "live",
21
15
  reconnect: () => {},
22
16
  };
23
17
 
24
- const failureOf = (error: unknown): StatewireSendFailure =>
18
+ const failureOf = (error: unknown): StatewireClient.SendFailure =>
25
19
  error instanceof StatewireSendError
26
20
  ? error.failure
27
21
  : {
@@ -30,43 +24,15 @@ const failureOf = (error: unknown): StatewireSendFailure =>
30
24
  message: error instanceof Error ? error.message : String(error),
31
25
  };
32
26
 
33
- // MessageChannel schedules the flush as a macrotask (tap's pattern): it runs
34
- // after the whole microtask drain, so every producer in the current turn
35
- // coalesces. The port is ref'd only while a flush is pending — an active
36
- // MessagePort holds the Node event loop open. ref/unref are Node-only.
37
- const createMacrotaskPoster = (callback: () => void): (() => void) => {
38
- if (typeof MessageChannel !== "undefined") {
39
- let port1: (MessagePort & { ref?: () => void; unref?: () => void }) | null =
40
- null;
41
- let port2: MessagePort;
42
- return () => {
43
- if (!port1) {
44
- const channel = new MessageChannel();
45
- channel.port1.onmessage = () => {
46
- port1?.unref?.();
47
- callback();
48
- };
49
- port1 = channel.port1;
50
- port2 = channel.port2;
51
- }
52
- port1.ref?.();
53
- port2!.postMessage(null);
54
- };
55
- }
56
- return () => setTimeout(callback, 0);
57
- };
58
-
59
27
  type Overlay = { inv: CommandsInvocation; dropped: boolean };
60
28
 
61
- export type StatewireCore = ReturnType<typeof createStatewireCore>;
62
-
63
- export const createStatewireCore = (
64
- getOptions: () => StatewireOptions<any, any, any>,
29
+ const createCore = (
30
+ getOptions: () => StatewireClient.Options<any, any, any>,
65
31
  ) => {
66
- let value: StatewireTransport<unknown> | null = null;
67
- let source: StatewireStateSource | null = null;
32
+ let value: Statewire<unknown> | null = null;
33
+ let source: Statewire.StateSource | null = null;
68
34
  let overlays: Overlay[] = [];
69
- let updates: StatewireCommandUpdate[] = [];
35
+ let updates: Statewire.CommandUpdate[] = [];
70
36
  let events: unknown[] = [];
71
37
  let pendingChanged: StatewirePath[] = [];
72
38
  let canonical: unknown;
@@ -74,11 +40,10 @@ export const createStatewireCore = (
74
40
  let overlayPaths: readonly StatewirePath[] = [];
75
41
  let notifiedPublished: unknown;
76
42
  let notifiedOverlayPaths: readonly StatewirePath[] = [];
77
- let notifiedConnection: StatewireConnection | null = null;
43
+ let notifiedConnection: StatewireClient.Connection | null = null;
78
44
  let stale = false;
79
45
  let overlaysDirty = false;
80
46
  let flushing = false;
81
- let disposed = false;
82
47
  const listeners = new Set<() => void>();
83
48
 
84
49
  const post = createMacrotaskPoster(() => {
@@ -87,7 +52,7 @@ export const createStatewireCore = (
87
52
  });
88
53
  let scheduled = false;
89
54
  const schedule = () => {
90
- if (scheduled || disposed) return;
55
+ if (scheduled) return;
91
56
  scheduled = true;
92
57
  post();
93
58
  };
@@ -100,7 +65,7 @@ export const createStatewireCore = (
100
65
  const drainCanonical = () => {
101
66
  if (source) {
102
67
  pendingChanged.push(...source.takeChanged());
103
- canonical = source.root();
68
+ canonical = source.snapshot();
104
69
  } else if (value && value.state !== canonical) {
105
70
  pendingChanged.push([]);
106
71
  canonical = value.state;
@@ -121,7 +86,7 @@ export const createStatewireCore = (
121
86
  for (const overlay of overlays) {
122
87
  if (overlay.dropped) continue;
123
88
  const { inv } = overlay;
124
- const ctx: StatewireOptimisticContext = {
89
+ const ctx: StatewireClient.OptimisticContext = {
125
90
  ts: inv.ts,
126
91
  seq: inv.seq,
127
92
  ...(inv.clientId !== undefined && { clientId: inv.clientId }),
@@ -130,7 +95,7 @@ export const createStatewireCore = (
130
95
  const recorded = recordMutations(next, (draft) =>
131
96
  optimistic(
132
97
  draft as never,
133
- { method: inv.method, params: inv.params } as StatewireCommand,
98
+ { method: inv.method, params: inv.params } as Statewire.Command,
134
99
  ctx,
135
100
  ),
136
101
  );
@@ -149,11 +114,11 @@ export const createStatewireCore = (
149
114
  overlayPaths = paths;
150
115
  };
151
116
 
152
- const getConnection = (): StatewireConnection =>
117
+ const getConnection = (): StatewireClient.Connection =>
153
118
  value?.connection ?? LIVE_CONNECTION;
154
119
 
155
120
  const flush = () => {
156
- if (disposed || !value) return;
121
+ if (!value) return;
157
122
  if (flushing) {
158
123
  schedule();
159
124
  return;
@@ -184,7 +149,7 @@ export const createStatewireCore = (
184
149
  options.onCommandChange?.(update, { state: published as never });
185
150
  }
186
151
  }
187
- if (events.length > 0) {
152
+ if (events.length > 0 && published !== undefined) {
188
153
  const batch = events;
189
154
  events = [];
190
155
  for (const event of batch) {
@@ -204,8 +169,8 @@ export const createStatewireCore = (
204
169
 
205
170
  const note = (
206
171
  inv: CommandsInvocation,
207
- status: StatewireCommandUpdate["status"],
208
- extra?: { result?: unknown; failure?: StatewireSendFailure },
172
+ status: Statewire.CommandUpdate["status"],
173
+ extra?: { result?: unknown; failure?: StatewireClient.SendFailure },
209
174
  ) => {
210
175
  if (!getOptions().onCommandChange) return;
211
176
  updates.push({
@@ -251,8 +216,8 @@ export const createStatewireCore = (
251
216
  schedule();
252
217
  };
253
218
 
254
- const attach = (next: StatewireTransport<unknown>) => {
255
- if (disposed || next === value) return;
219
+ const attach = (next: Statewire<unknown>) => {
220
+ if (next === value) return;
256
221
  const prev = value;
257
222
  value = next;
258
223
  const nextSource =
@@ -294,48 +259,33 @@ export const createStatewireCore = (
294
259
  }
295
260
  };
296
261
 
297
- const memo = new Map<string, (...params: unknown[]) => Promise<unknown>>();
298
- const commands = new Proxy({} as StatewireCommandsProxy<any>, {
299
- get: (_target, method) => {
300
- if (typeof method !== "string") return undefined;
301
- let fn = memo.get(method);
302
- if (fn === undefined) {
303
- fn = (...params: unknown[]) => {
304
- if (!value) {
305
- return Promise.reject(
306
- new StatewireSendError({ fate: "aborted", cause: "unmount" }),
307
- );
308
- }
309
- const send = (
310
- value.commands as Record<
311
- string,
312
- (...args: unknown[]) => Promise<unknown>
313
- >
314
- )[method]!;
315
- const promise = send(...params);
316
- flush();
317
- return promise;
318
- };
319
- memo.set(method, fn);
262
+ const commands = createMethodProxy<Statewire.CommandsProxy<any>>(
263
+ (method, params) => {
264
+ if (!value) {
265
+ return Promise.reject(
266
+ new StatewireSendError({ fate: "aborted", cause: "unmount" }),
267
+ );
320
268
  }
321
- return fn;
269
+ const send = (
270
+ value.commands as Record<
271
+ string,
272
+ (...args: unknown[]) => Promise<unknown>
273
+ >
274
+ )[method]!;
275
+ const promise = send(...params);
276
+ flush();
277
+ return promise;
322
278
  },
323
- });
279
+ );
324
280
 
325
281
  return {
326
282
  attach,
327
283
  detach,
328
- dispose: () => {
329
- detach();
330
- disposed = true;
331
- },
332
284
  getState: () => {
333
285
  recompute();
334
286
  return published;
335
287
  },
336
- getConnection,
337
288
  commands,
338
- flush,
339
289
  subscribe: (listener: () => void) => {
340
290
  listeners.add(listener);
341
291
  return () => {
@@ -344,3 +294,19 @@ export const createStatewireCore = (
344
294
  },
345
295
  };
346
296
  };
297
+
298
+ export const useStatewireCore = (
299
+ getOptions: () => StatewireClient.Options<any, any, any>,
300
+ value: Statewire<unknown>,
301
+ ) => {
302
+ const [core] = useState(() => createCore(getOptions));
303
+ // Attach during render so the first snapshot reads the transport's state;
304
+ // re-attaching the same transport is a no-op and the single-slot host
305
+ // listeners make a swap land on the last writer.
306
+ core.attach(value);
307
+ useEffect(() => {
308
+ core.attach(value);
309
+ });
310
+ useEffect(() => () => core.detach(), [core]);
311
+ return core;
312
+ };