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
package/src/host/host.ts CHANGED
@@ -1,54 +1,48 @@
1
1
  import { useEffect, useState } from "react";
2
- import { applyOp, applyOps } from "../wire/ops";
3
- import type { StatewireOp, StatewirePath } from "../wire/ops";
2
+ import { applyOps, sanitizeValue } from "../wire/ops";
3
+ import type { StatewirePath } from "../wire/ops";
4
4
  import { StatewireReject } from "./engine";
5
- import { createStateDraft, frozen } from "./draft";
6
- import type { ServerOp } from "./draft";
5
+ import { createStateDraft } from "./draft";
6
+ import { createMethodProxy } from "../util/methodProxy";
7
+ import type { ServerOp, StateDraft } from "./draft";
7
8
  import { StatewireSendError } from "../types";
8
- import type {
9
- StatewireCommands,
10
- StatewireCommandsProxy,
11
- StatewireStateSource,
12
- } from "../types";
9
+ import type { Statewire } from "../types";
10
+ import type { StatewireHost } from "../host";
13
11
 
14
12
  const isDev = () =>
15
13
  typeof process === "undefined" || process.env["NODE_ENV"] !== "production";
16
14
 
17
15
  // ─── state ──────────────────────────────────────────────
18
16
 
19
- export type StatewireStateCtl = {
20
- /** Apply server-vocabulary ops (root replace = whole-state swap). */
21
- apply: (ops: readonly StatewireOp[]) => void;
22
- };
23
-
24
- export type StateHost = StatewireStateSource & {
25
- /** Synchronous, per locally-recorded op; the host owns batching. */
26
- setOnOp: (listener: ((op: ServerOp) => void) | null) => void;
27
- };
28
-
29
- const stateHosts = new WeakMap<object, StateHost>();
17
+ const stateHosts = new WeakMap<object, StatewireHost.StateHost>();
30
18
 
31
- export const getStateHost = (state: object): StateHost | undefined =>
32
- stateHosts.get(state);
19
+ /** Host-side accessor for a `useStatewireState` state object's source (`snapshot()` reads the committed state). */
20
+ export const getStateHost = (
21
+ state: object,
22
+ ): StatewireHost.StateHost | undefined => stateHosts.get(state);
33
23
 
34
24
  type StateCell = {
35
25
  current: unknown;
36
- draft: object | null;
26
+ draft: StateDraft<object> | null;
37
27
  changed: StatewirePath[];
38
28
  onOp: ((op: ServerOp) => void) | null;
39
29
  onChange: (() => void) | null;
40
- ctl: StatewireStateCtl;
41
- host: StateHost;
30
+ ctl: StatewireHost.StateCtl;
31
+ host: StatewireHost.StateHost;
42
32
  bump: () => void;
43
33
  };
44
34
 
45
35
  export const useStatewireState = <TState>(
46
36
  init: () => TState,
47
- ): readonly [TState, StatewireStateCtl] => {
37
+ ): readonly [TState, StatewireHost.StateCtl<TState>] => {
48
38
  const [, setVersion] = useState(0);
49
39
  const [cell] = useState((): StateCell => {
40
+ const commit = () => {
41
+ if (c.draft) c.current = c.draft.flush();
42
+ return c.current;
43
+ };
50
44
  const c: StateCell = {
51
- current: frozen(init()),
45
+ current: sanitizeValue(init()),
52
46
  draft: null,
53
47
  changed: [[]],
54
48
  onOp: null,
@@ -56,20 +50,23 @@ export const useStatewireState = <TState>(
56
50
  bump: () => setVersion((v) => v + 1),
57
51
  ctl: {
58
52
  apply: (ops) => {
59
- const result = applyOps(c.current, ops);
53
+ const result = applyOps(commit(), ops);
60
54
  if (result.events.length > 0) {
61
55
  throw new Error(
62
56
  "statewire: event ops do not apply to state — route them to emit",
63
57
  );
64
58
  }
65
59
  c.current = result.state;
60
+ c.draft?.invalidate();
66
61
  c.changed.push(...result.changedPaths);
62
+ if (c.onOp) for (const op of ops) c.onOp(op as ServerOp);
67
63
  c.bump();
68
64
  c.onChange?.();
69
65
  },
66
+ snapshot: commit,
70
67
  },
71
68
  host: {
72
- root: () => c.current,
69
+ snapshot: commit,
73
70
  setOnOp: (listener) => {
74
71
  c.onOp = listener;
75
72
  },
@@ -86,23 +83,21 @@ export const useStatewireState = <TState>(
86
83
  return c;
87
84
  });
88
85
 
89
- let value = cell.current;
86
+ let value = cell.ctl.snapshot();
90
87
  if (typeof value === "object" && value !== null) {
91
88
  cell.draft ??= createStateDraft<object>({
92
- root: () => cell.current,
93
- record: (op) => {
94
- const applied = applyOp(cell.current, op);
95
- cell.current = applied.state;
96
- cell.changed.push(...applied.changedPaths);
89
+ snapshot: () => cell.current,
90
+ record: (op, changedPath) => {
91
+ cell.changed.push(changedPath);
97
92
  cell.onOp?.(op);
98
93
  cell.bump();
99
94
  cell.onChange?.();
100
95
  },
101
96
  });
102
- stateHosts.set(cell.draft, cell.host);
103
- value = cell.draft;
97
+ stateHosts.set(cell.draft.proxy, cell.host);
98
+ value = cell.draft.proxy;
104
99
  }
105
- return [value as TState, cell.ctl] as const;
100
+ return [value as TState, cell.ctl as StatewireHost.StateCtl<TState>] as const;
106
101
  };
107
102
 
108
103
  // ─── commands ───────────────────────────────────────────
@@ -124,24 +119,6 @@ type ExecutionState = {
124
119
 
125
120
  const executionStates = new WeakMap<object, ExecutionState>();
126
121
 
127
- declare const executionResult: unique symbol;
128
-
129
- /**
130
- * Handle a command handler returns to keep the command open past its own
131
- * return: `ack()` releases the client's optimistic overlay / ack watermark,
132
- * `resolve`/`reject` settle the command (and imply ack). `ack` is idempotent
133
- * and a no-op after settle.
134
- */
135
- export type CommandExecution<T = void> = {
136
- readonly [executionResult]: (value: T) => void;
137
- };
138
-
139
- export type CommandExecutionResolvers<T = void> = {
140
- ack: () => void;
141
- resolve: (value: T) => void;
142
- reject: (error: unknown) => void;
143
- };
144
-
145
122
  /**
146
123
  * `CommandExecution(executor)` runs the (possibly async) `executor({ ack })`
147
124
  * immediately: its return value resolves the execution, a throw/rejection
@@ -154,9 +131,11 @@ export const CommandExecution = Object.assign(
154
131
  executor: (resolvers: {
155
132
  ack: () => void;
156
133
  }) => T | void | PromiseLike<T | void>,
157
- ): CommandExecution<T> => {
134
+ ): StatewireHost.Execution<T> => {
158
135
  const state: ExecutionState = { acked: false, settled: null, bind: null };
159
- const execution = Object.freeze({}) as unknown as CommandExecution<T>;
136
+ const execution = Object.freeze(
137
+ {},
138
+ ) as unknown as StatewireHost.Execution<T>;
160
139
  executionStates.set(execution, state);
161
140
  const settle = (settled: ExecutionSettled) => {
162
141
  if (state.settled) return;
@@ -180,8 +159,8 @@ export const CommandExecution = Object.assign(
180
159
  },
181
160
  {
182
161
  withResolvers: <T = void>(): [
183
- CommandExecution<T>,
184
- CommandExecutionResolvers<T>,
162
+ StatewireHost.Execution<T>,
163
+ StatewireHost.ExecutionResolvers<T>,
185
164
  ] => {
186
165
  let resolve!: (value: T) => void;
187
166
  let reject!: (error: unknown) => void;
@@ -220,8 +199,17 @@ export type CommandsListener = {
220
199
  onSettle: (inv: CommandsInvocation, outcome: CommandsOutcome) => void;
221
200
  };
222
201
 
202
+ export type CommandsSubmission = {
203
+ method: string;
204
+ params: readonly unknown[];
205
+ clientId?: string;
206
+ onAck?: () => void;
207
+ onSettle?: (outcome: CommandsOutcome) => void;
208
+ };
209
+
223
210
  export type CommandsHost = {
224
211
  setListener: (listener: CommandsListener | null) => void;
212
+ submit?: (call: CommandsSubmission) => Promise<unknown>;
225
213
  };
226
214
 
227
215
  const commandsHosts = new WeakMap<object, CommandsHost>();
@@ -239,9 +227,13 @@ type QueuedCall = {
239
227
  ts: number;
240
228
  resolve: (value: unknown) => void;
241
229
  reject: (error: unknown) => void;
230
+ origin?: CommandsSubmission;
242
231
  };
243
232
 
244
- type ActiveInvocation = CommandsInvocation & { acked: boolean };
233
+ type ActiveInvocation = CommandsInvocation & {
234
+ acked: boolean;
235
+ origin?: CommandsSubmission;
236
+ };
245
237
 
246
238
  type CommandsCell = {
247
239
  handlers: Record<string, (...args: never[]) => unknown> | null;
@@ -270,6 +262,7 @@ const ackInvocation = (cell: CommandsCell, inv: ActiveInvocation) => {
270
262
  if (inv.acked) return;
271
263
  inv.acked = true;
272
264
  cell.listener?.onAck(inv);
265
+ inv.origin?.onAck?.();
273
266
  };
274
267
 
275
268
  const settleInvocation = (
@@ -283,12 +276,15 @@ const settleInvocation = (
283
276
  const outcome: CommandsOutcome =
284
277
  settled.value === undefined
285
278
  ? { type: "accepted" }
286
- : { type: "accepted", payload: frozen(settled.value) };
279
+ : { type: "accepted", payload: sanitizeValue(settled.value) };
287
280
  cell.listener?.onSettle(inv, outcome);
281
+ inv.origin?.onSettle?.(outcome);
288
282
  call.resolve(outcome.payload);
289
283
  } else {
290
284
  const error = toSendError(settled.error);
291
- cell.listener?.onSettle(inv, { type: "rejected", error });
285
+ const outcome: CommandsOutcome = { type: "rejected", error };
286
+ cell.listener?.onSettle(inv, outcome);
287
+ inv.origin?.onSettle?.(outcome);
292
288
  call.reject(error);
293
289
  }
294
290
  };
@@ -302,9 +298,12 @@ const pump = (cell: CommandsCell) => {
302
298
  if (!call) return;
303
299
  const handler = cell.handlers[call.method];
304
300
  if (handler === undefined) {
305
- call.reject(
306
- new StatewireSendError({ fate: "unknown-command", cause: "server" }),
307
- );
301
+ const error = new StatewireSendError({
302
+ fate: "unknown-command",
303
+ cause: "server",
304
+ });
305
+ call.origin?.onSettle?.({ type: "rejected", error });
306
+ call.reject(error);
308
307
  pump(cell);
309
308
  return;
310
309
  }
@@ -314,6 +313,10 @@ const pump = (cell: CommandsCell) => {
314
313
  ts: call.ts,
315
314
  seq: ++cell.seq,
316
315
  acked: false,
316
+ ...(call.origin !== undefined && { origin: call.origin }),
317
+ ...(call.origin?.clientId !== undefined && {
318
+ clientId: call.origin.clientId,
319
+ }),
317
320
  };
318
321
  cell.running = true;
319
322
  cell.listener?.onStart(inv);
@@ -363,17 +366,9 @@ const watchStall = (cell: CommandsCell) => {
363
366
  }, STALL_WARN_MS);
364
367
  };
365
368
 
366
- type UnwrapExecution<V> = V extends CommandExecution<infer T> ? T : V;
367
-
368
- export type StatewireHandledCommands<THandlers extends StatewireCommands> = {
369
- [K in keyof THandlers]: (
370
- ...args: Parameters<THandlers[K]>
371
- ) => UnwrapExecution<Awaited<ReturnType<THandlers[K]>>>;
372
- };
373
-
374
- export const useStatewireCommands = <THandlers extends StatewireCommands>(
369
+ export const useStatewireCommands = <THandlers extends Statewire.Commands>(
375
370
  commands: THandlers | undefined | null | false,
376
- ): StatewireCommandsProxy<StatewireHandledCommands<THandlers>> => {
371
+ ): Statewire.CommandsProxy<StatewireHost.HandledCommands<THandlers>> => {
377
372
  const [built] = useState(() => {
378
373
  const cell: CommandsCell = {
379
374
  handlers: null,
@@ -384,39 +379,45 @@ export const useStatewireCommands = <THandlers extends StatewireCommands>(
384
379
  unmounted: false,
385
380
  stallTimer: null,
386
381
  };
387
- const memo = new Map<string, (...params: unknown[]) => Promise<unknown>>();
388
- const proxy = new Proxy(
389
- {} as StatewireCommandsProxy<StatewireHandledCommands<THandlers>>,
390
- {
391
- get: (_target, method) => {
392
- if (typeof method !== "string") return undefined;
393
- let fn = memo.get(method);
394
- if (fn === undefined) {
395
- fn = (...params: unknown[]) =>
396
- new Promise((resolve, reject) => {
397
- if (cell.unmounted) {
398
- reject(new Error("statewire: commands called after unmount"));
399
- return;
400
- }
401
- cell.queue.push({
402
- method,
403
- params,
404
- ts: Date.now(),
405
- resolve,
406
- reject,
407
- });
408
- pump(cell);
409
- });
410
- memo.set(method, fn);
382
+ const proxy = createMethodProxy<
383
+ Statewire.CommandsProxy<StatewireHost.HandledCommands<THandlers>>
384
+ >(
385
+ (method, params) =>
386
+ new Promise((resolve, reject) => {
387
+ if (cell.unmounted) {
388
+ reject(new Error("statewire: commands called after unmount"));
389
+ return;
411
390
  }
412
- return fn;
413
- },
414
- },
391
+ cell.queue.push({
392
+ method,
393
+ params,
394
+ ts: Date.now(),
395
+ resolve,
396
+ reject,
397
+ });
398
+ pump(cell);
399
+ }),
415
400
  );
416
401
  commandsHosts.set(proxy, {
417
402
  setListener: (listener) => {
418
403
  cell.listener = listener;
419
404
  },
405
+ submit: (origin) =>
406
+ new Promise((resolve, reject) => {
407
+ if (cell.unmounted) {
408
+ reject(new Error("statewire: commands called after unmount"));
409
+ return;
410
+ }
411
+ cell.queue.push({
412
+ method: origin.method,
413
+ params: origin.params,
414
+ ts: Date.now(),
415
+ resolve,
416
+ reject,
417
+ origin,
418
+ });
419
+ pump(cell);
420
+ }),
420
421
  });
421
422
  return { cell, proxy };
422
423
  });
@@ -458,8 +459,8 @@ export type EmitHost = {
458
459
 
459
460
  const emitHosts = new WeakMap<object, EmitHost>();
460
461
 
461
- export const getEmitHost = (emit: object): EmitHost | undefined =>
462
- emitHosts.get(emit);
462
+ export const getEmitHost = (emit: object | undefined): EmitHost | undefined =>
463
+ emit && emitHosts.get(emit);
463
464
 
464
465
  export const useStatewireEmit = <TEvent>(): ((event: TEvent) => void) => {
465
466
  const [built] = useState(() => {
@@ -468,7 +469,7 @@ export const useStatewireEmit = <TEvent>(): ((event: TEvent) => void) => {
468
469
  if (!cell.sink) {
469
470
  throw new Error("statewire: emit before a host bound the event sink");
470
471
  }
471
- cell.sink(frozen(event));
472
+ cell.sink(sanitizeValue(event));
472
473
  };
473
474
  emitHosts.set(emit, {
474
475
  setSink: (sink) => {