statewire 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/README.md +14 -13
  2. package/dist/client/changes.d.ts +2 -2
  3. package/dist/client/changes.d.ts.map +1 -1
  4. package/dist/client/changes.js +3 -10
  5. package/dist/client/changes.js.map +1 -1
  6. package/dist/client/connection.d.ts +18 -39
  7. package/dist/client/connection.d.ts.map +1 -1
  8. package/dist/client/connection.js +61 -54
  9. package/dist/client/connection.js.map +1 -1
  10. package/dist/client/core.d.ts +7 -11
  11. package/dist/client/core.d.ts.map +1 -1
  12. package/dist/client/core.js +58 -55
  13. package/dist/client/core.js.map +1 -1
  14. package/dist/client/session.d.ts +12 -26
  15. package/dist/client/session.d.ts.map +1 -1
  16. package/dist/client/session.js +70 -43
  17. package/dist/client/session.js.map +1 -1
  18. package/dist/client/sessionStore.d.ts +1 -1
  19. package/dist/client/sessionStore.js.map +1 -1
  20. package/dist/client/timing.d.ts +2 -1
  21. package/dist/client/timing.d.ts.map +1 -1
  22. package/dist/client/timing.js +5 -1
  23. package/dist/client/timing.js.map +1 -1
  24. package/dist/host/adapter.d.ts +8 -0
  25. package/dist/host/adapter.d.ts.map +1 -0
  26. package/dist/host/adapter.js +23 -0
  27. package/dist/host/adapter.js.map +1 -0
  28. package/dist/host/draft.d.ts +21 -7
  29. package/dist/host/draft.d.ts.map +1 -1
  30. package/dist/host/draft.js +190 -103
  31. package/dist/host/draft.js.map +1 -1
  32. package/dist/host/host.d.ts +19 -35
  33. package/dist/host/host.d.ts.map +1 -1
  34. package/dist/host/host.js +86 -54
  35. package/dist/host/host.js.map +1 -1
  36. package/dist/host/socket.d.ts +14 -0
  37. package/dist/host/socket.d.ts.map +1 -0
  38. package/dist/host/socket.js +511 -0
  39. package/dist/host/socket.js.map +1 -0
  40. package/dist/host-internal.d.ts +27 -0
  41. package/dist/host-internal.d.ts.map +1 -0
  42. package/dist/host-internal.js +3 -0
  43. package/dist/host.d.ts +54 -0
  44. package/dist/host.d.ts.map +1 -0
  45. package/dist/host.js +4 -0
  46. package/dist/index.d.ts +5 -28
  47. package/dist/index.js +2 -3
  48. package/dist/statewire.d.ts +97 -10
  49. package/dist/statewire.d.ts.map +1 -1
  50. package/dist/statewire.js +32 -36
  51. package/dist/statewire.js.map +1 -1
  52. package/dist/transports/fake-sse.d.ts +12 -0
  53. package/dist/transports/fake-sse.d.ts.map +1 -0
  54. package/dist/transports/fake-sse.js +39 -0
  55. package/dist/transports/fake-sse.js.map +1 -0
  56. package/dist/transports/fixtures.d.ts +33 -0
  57. package/dist/transports/fixtures.d.ts.map +1 -0
  58. package/dist/transports/fixtures.js +50 -0
  59. package/dist/transports/fixtures.js.map +1 -0
  60. package/dist/transports/remote.d.ts +6 -13
  61. package/dist/transports/remote.d.ts.map +1 -1
  62. package/dist/transports/remote.js +7 -25
  63. package/dist/transports/remote.js.map +1 -1
  64. package/dist/transports/sse.d.ts +32 -20
  65. package/dist/transports/sse.d.ts.map +1 -1
  66. package/dist/transports/sse.js +57 -46
  67. package/dist/transports/sse.js.map +1 -1
  68. package/dist/transports/testing.d.ts +8 -6
  69. package/dist/transports/testing.d.ts.map +1 -1
  70. package/dist/transports/testing.js +19 -46
  71. package/dist/transports/testing.js.map +1 -1
  72. package/dist/transports/ws.d.ts +4 -13
  73. package/dist/transports/ws.d.ts.map +1 -1
  74. package/dist/transports/ws.js +59 -63
  75. package/dist/transports/ws.js.map +1 -1
  76. package/dist/types.d.ts +55 -100
  77. package/dist/types.d.ts.map +1 -1
  78. package/dist/types.js.map +1 -1
  79. package/dist/util/draftTraps.d.ts +6 -0
  80. package/dist/util/draftTraps.d.ts.map +1 -0
  81. package/dist/util/draftTraps.js +25 -0
  82. package/dist/util/draftTraps.js.map +1 -0
  83. package/dist/util/macrotask.d.ts +5 -0
  84. package/dist/util/macrotask.d.ts.map +1 -0
  85. package/dist/util/macrotask.js +25 -0
  86. package/dist/util/macrotask.js.map +1 -0
  87. package/dist/util/methodProxy.d.ts +5 -0
  88. package/dist/util/methodProxy.d.ts.map +1 -0
  89. package/dist/util/methodProxy.js +17 -0
  90. package/dist/util/methodProxy.js.map +1 -0
  91. package/dist/util/randomId.d.ts +5 -0
  92. package/dist/util/randomId.d.ts.map +1 -0
  93. package/dist/util/randomId.js +6 -0
  94. package/dist/util/randomId.js.map +1 -0
  95. package/dist/{client/optimistic.d.ts → util/recordMutations.d.ts} +2 -2
  96. package/dist/util/recordMutations.d.ts.map +1 -0
  97. package/dist/{client/optimistic.js → util/recordMutations.js} +5 -24
  98. package/dist/util/recordMutations.js.map +1 -0
  99. package/dist/util/statePath.d.ts +14 -0
  100. package/dist/util/statePath.d.ts.map +1 -0
  101. package/dist/util/statePath.js +31 -0
  102. package/dist/util/statePath.js.map +1 -0
  103. package/dist/wire/meta.d.ts +6 -1
  104. package/dist/wire/meta.d.ts.map +1 -1
  105. package/dist/wire/meta.js +30 -28
  106. package/dist/wire/meta.js.map +1 -1
  107. package/dist/wire/ops.d.ts +5 -26
  108. package/dist/wire/ops.d.ts.map +1 -1
  109. package/dist/wire/ops.js +67 -80
  110. package/dist/wire/ops.js.map +1 -1
  111. package/package.json +15 -11
  112. package/src/client/changes.ts +5 -15
  113. package/src/client/connection.ts +87 -113
  114. package/src/client/core.ts +57 -91
  115. package/src/client/session.ts +129 -90
  116. package/src/client/sessionStore.ts +1 -1
  117. package/src/client/timing.ts +6 -0
  118. package/src/host/adapter.ts +33 -0
  119. package/src/host/draft.ts +293 -125
  120. package/src/host/host.ts +122 -110
  121. package/src/host/socket.ts +618 -0
  122. package/src/host-internal.ts +28 -0
  123. package/src/host.ts +72 -0
  124. package/src/index.ts +2 -62
  125. package/src/statewire.ts +168 -61
  126. package/src/transports/fake-sse.ts +51 -0
  127. package/src/transports/fixtures.ts +60 -0
  128. package/src/transports/remote.ts +14 -51
  129. package/src/transports/sse.ts +87 -89
  130. package/src/transports/testing.ts +33 -63
  131. package/src/transports/ws.ts +78 -100
  132. package/src/types.ts +52 -131
  133. package/src/util/draftTraps.ts +32 -0
  134. package/src/util/macrotask.ts +25 -0
  135. package/src/util/methodProxy.ts +16 -0
  136. package/src/util/randomId.ts +4 -0
  137. package/src/{client/optimistic.ts → util/recordMutations.ts} +0 -0
  138. package/src/util/statePath.ts +61 -0
  139. package/src/wire/meta.ts +42 -47
  140. package/src/wire/ops.ts +107 -106
  141. package/LICENSE +0 -21
  142. package/dist/client/optimistic.d.ts.map +0 -1
  143. package/dist/client/optimistic.js.map +0 -1
  144. package/dist/index.d.ts.map +0 -1
  145. package/dist/server.d.ts +0 -15
  146. package/dist/server.d.ts.map +0 -1
  147. package/dist/server.js +0 -3
  148. package/src/server.ts +0 -32
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,18 @@ 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
+ caller?: StatewireHost.Caller;
207
+ onAck?: () => void;
208
+ onSettle?: (outcome: CommandsOutcome) => void;
209
+ };
210
+
223
211
  export type CommandsHost = {
224
212
  setListener: (listener: CommandsListener | null) => void;
213
+ submit?: (call: CommandsSubmission) => Promise<unknown>;
225
214
  };
226
215
 
227
216
  const commandsHosts = new WeakMap<object, CommandsHost>();
@@ -239,9 +228,13 @@ type QueuedCall = {
239
228
  ts: number;
240
229
  resolve: (value: unknown) => void;
241
230
  reject: (error: unknown) => void;
231
+ origin?: CommandsSubmission;
242
232
  };
243
233
 
244
- type ActiveInvocation = CommandsInvocation & { acked: boolean };
234
+ type ActiveInvocation = CommandsInvocation & {
235
+ acked: boolean;
236
+ origin?: CommandsSubmission;
237
+ };
245
238
 
246
239
  type CommandsCell = {
247
240
  handlers: Record<string, (...args: never[]) => unknown> | null;
@@ -255,6 +248,12 @@ type CommandsCell = {
255
248
 
256
249
  const STALL_WARN_MS = 5000;
257
250
 
251
+ // In-process dispatch has no wire client: never any ambient context, trivially attached.
252
+ const LOCAL_CALLER: StatewireHost.Caller = Object.freeze({
253
+ context: null,
254
+ isConnected: true,
255
+ });
256
+
258
257
  const toSendError = (error: unknown): StatewireSendError =>
259
258
  error instanceof StatewireSendError
260
259
  ? error
@@ -270,6 +269,7 @@ const ackInvocation = (cell: CommandsCell, inv: ActiveInvocation) => {
270
269
  if (inv.acked) return;
271
270
  inv.acked = true;
272
271
  cell.listener?.onAck(inv);
272
+ inv.origin?.onAck?.();
273
273
  };
274
274
 
275
275
  const settleInvocation = (
@@ -283,12 +283,15 @@ const settleInvocation = (
283
283
  const outcome: CommandsOutcome =
284
284
  settled.value === undefined
285
285
  ? { type: "accepted" }
286
- : { type: "accepted", payload: frozen(settled.value) };
286
+ : { type: "accepted", payload: sanitizeValue(settled.value) };
287
287
  cell.listener?.onSettle(inv, outcome);
288
+ inv.origin?.onSettle?.(outcome);
288
289
  call.resolve(outcome.payload);
289
290
  } else {
290
291
  const error = toSendError(settled.error);
291
- cell.listener?.onSettle(inv, { type: "rejected", error });
292
+ const outcome: CommandsOutcome = { type: "rejected", error };
293
+ cell.listener?.onSettle(inv, outcome);
294
+ inv.origin?.onSettle?.(outcome);
292
295
  call.reject(error);
293
296
  }
294
297
  };
@@ -302,9 +305,12 @@ const pump = (cell: CommandsCell) => {
302
305
  if (!call) return;
303
306
  const handler = cell.handlers[call.method];
304
307
  if (handler === undefined) {
305
- call.reject(
306
- new StatewireSendError({ fate: "unknown-command", cause: "server" }),
307
- );
308
+ const error = new StatewireSendError({
309
+ fate: "unknown-command",
310
+ cause: "server",
311
+ });
312
+ call.origin?.onSettle?.({ type: "rejected", error });
313
+ call.reject(error);
308
314
  pump(cell);
309
315
  return;
310
316
  }
@@ -314,6 +320,10 @@ const pump = (cell: CommandsCell) => {
314
320
  ts: call.ts,
315
321
  seq: ++cell.seq,
316
322
  acked: false,
323
+ ...(call.origin !== undefined && { origin: call.origin }),
324
+ ...(call.origin?.clientId !== undefined && {
325
+ clientId: call.origin.clientId,
326
+ }),
317
327
  };
318
328
  cell.running = true;
319
329
  cell.listener?.onStart(inv);
@@ -321,9 +331,9 @@ const pump = (cell: CommandsCell) => {
321
331
  cell.listener?.onCovered?.(inv);
322
332
  void (async () => {
323
333
  try {
324
- const value = await (handler as (...args: readonly unknown[]) => unknown)(
325
- ...call.params,
326
- );
334
+ const value = await (
335
+ handler as (...args: readonly unknown[]) => unknown
336
+ ).call(call.origin?.caller ?? LOCAL_CALLER, ...call.params);
327
337
  const execution =
328
338
  typeof value === "object" && value !== null
329
339
  ? executionStates.get(value)
@@ -363,17 +373,13 @@ const watchStall = (cell: CommandsCell) => {
363
373
  }, STALL_WARN_MS);
364
374
  };
365
375
 
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>(
375
- commands: THandlers | undefined | null | false,
376
- ): StatewireCommandsProxy<StatewireHandledCommands<THandlers>> => {
376
+ export const useStatewireCommands = <THandlers extends Statewire.Commands>(
377
+ commands:
378
+ | (THandlers & ThisType<StatewireHost.Caller>)
379
+ | undefined
380
+ | null
381
+ | false,
382
+ ): Statewire.CommandsProxy<StatewireHost.HandledCommands<THandlers>> => {
377
383
  const [built] = useState(() => {
378
384
  const cell: CommandsCell = {
379
385
  handlers: null,
@@ -384,39 +390,45 @@ export const useStatewireCommands = <THandlers extends StatewireCommands>(
384
390
  unmounted: false,
385
391
  stallTimer: null,
386
392
  };
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);
393
+ const proxy = createMethodProxy<
394
+ Statewire.CommandsProxy<StatewireHost.HandledCommands<THandlers>>
395
+ >(
396
+ (method, params) =>
397
+ new Promise((resolve, reject) => {
398
+ if (cell.unmounted) {
399
+ reject(new Error("statewire: commands called after unmount"));
400
+ return;
411
401
  }
412
- return fn;
413
- },
414
- },
402
+ cell.queue.push({
403
+ method,
404
+ params,
405
+ ts: Date.now(),
406
+ resolve,
407
+ reject,
408
+ });
409
+ pump(cell);
410
+ }),
415
411
  );
416
412
  commandsHosts.set(proxy, {
417
413
  setListener: (listener) => {
418
414
  cell.listener = listener;
419
415
  },
416
+ submit: (origin) =>
417
+ new Promise((resolve, reject) => {
418
+ if (cell.unmounted) {
419
+ reject(new Error("statewire: commands called after unmount"));
420
+ return;
421
+ }
422
+ cell.queue.push({
423
+ method: origin.method,
424
+ params: origin.params,
425
+ ts: Date.now(),
426
+ resolve,
427
+ reject,
428
+ origin,
429
+ });
430
+ pump(cell);
431
+ }),
420
432
  });
421
433
  return { cell, proxy };
422
434
  });
@@ -458,8 +470,8 @@ export type EmitHost = {
458
470
 
459
471
  const emitHosts = new WeakMap<object, EmitHost>();
460
472
 
461
- export const getEmitHost = (emit: object): EmitHost | undefined =>
462
- emitHosts.get(emit);
473
+ export const getEmitHost = (emit: object | undefined): EmitHost | undefined =>
474
+ emit && emitHosts.get(emit);
463
475
 
464
476
  export const useStatewireEmit = <TEvent>(): ((event: TEvent) => void) => {
465
477
  const [built] = useState(() => {
@@ -468,7 +480,7 @@ export const useStatewireEmit = <TEvent>(): ((event: TEvent) => void) => {
468
480
  if (!cell.sink) {
469
481
  throw new Error("statewire: emit before a host bound the event sink");
470
482
  }
471
- cell.sink(frozen(event));
483
+ cell.sink(sanitizeValue(event));
472
484
  };
473
485
  emitHosts.set(emit, {
474
486
  setSink: (sink) => {