statewire 0.8.0 → 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 -259
  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 -276
  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,6 +1,7 @@
1
1
  export type StatewireSessionRecord = {
2
2
  clientId: string;
3
3
  nextSeq: number;
4
+ ctxNextSeq: number;
4
5
  };
5
6
 
6
7
  export type StatewireSessionStore = {
@@ -6,9 +6,3 @@ export const timing = {
6
6
 
7
7
  export const backoff = (attempt: number) =>
8
8
  Math.min(timing.backoffBaseMs * 2 ** (attempt - 1), timing.backoffMaxMs);
9
-
10
- export const retryDelay = (error: unknown, attempt: number): number => {
11
- const retryAfterMs = (error as { retryAfterMs?: unknown } | undefined)
12
- ?.retryAfterMs;
13
- return typeof retryAfterMs === "number" ? retryAfterMs : backoff(attempt);
14
- };
package/src/host/draft.ts CHANGED
@@ -4,11 +4,9 @@ import { sanitizeValue } from "../wire/ops";
4
4
  import { isUnsafeKey } from "../util/statePath";
5
5
  import { sharedDraftTraps } from "../util/draftTraps";
6
6
 
7
- export type ServerOp = Exclude<Statewire.Op, { op: "event" }>;
8
-
9
7
  export type DraftHost = {
10
8
  snapshot: () => unknown;
11
- record: (op: ServerOp, changedPath: StatewirePath) => void;
9
+ record: (op: Statewire.Op, changedPath: StatewirePath) => void;
12
10
  };
13
11
 
14
12
  export type StateDraft<T> = {
package/src/host/host.ts CHANGED
@@ -4,7 +4,7 @@ import type { StatewirePath } from "../wire/ops";
4
4
  import { StatewireReject } from "./engine";
5
5
  import { createStateDraft } from "./draft";
6
6
  import { createMethodProxy } from "../util/methodProxy";
7
- import type { ServerOp, StateDraft } from "./draft";
7
+ import type { StateDraft } from "./draft";
8
8
  import { StatewireSendError } from "../types";
9
9
  import type { Statewire } from "../types";
10
10
  import type { StatewireHost } from "../host";
@@ -25,7 +25,7 @@ type StateCell = {
25
25
  current: unknown;
26
26
  draft: StateDraft<object> | null;
27
27
  changed: StatewirePath[];
28
- onOp: ((op: ServerOp) => void) | null;
28
+ onOp: ((op: Statewire.Op) => void) | null;
29
29
  onChange: (() => void) | null;
30
30
  ctl: StatewireHost.StateCtl;
31
31
  host: StatewireHost.StateHost;
@@ -51,15 +51,10 @@ export const useStatewireState = <TState>(
51
51
  ctl: {
52
52
  apply: (ops) => {
53
53
  const result = applyOps(commit(), ops);
54
- if (result.events.length > 0) {
55
- throw new Error(
56
- "statewire: event ops do not apply to state — route them to emit",
57
- );
58
- }
59
54
  c.current = result.state;
60
55
  c.draft?.invalidate();
61
56
  c.changed.push(...result.changedPaths);
62
- if (c.onOp) for (const op of ops) c.onOp(op as ServerOp);
57
+ if (c.onOp) for (const op of ops) c.onOp(op);
63
58
  c.bump();
64
59
  c.onChange?.();
65
60
  },
@@ -121,7 +116,7 @@ export type CommandsOutcome =
121
116
  export type CommandsListener = {
122
117
  onStart: (inv: CommandsInvocation) => void;
123
118
  onPosted?: (inv: CommandsInvocation) => void;
124
- onAck: (inv: CommandsInvocation) => void;
119
+ onApplied: (inv: CommandsInvocation) => void;
125
120
  /** Canonical state now covers the invocation's effect — drop any overlay. */
126
121
  onCovered?: (inv: CommandsInvocation) => void;
127
122
  onSettle: (inv: CommandsInvocation, outcome: CommandsOutcome) => void;
@@ -132,13 +127,15 @@ export type CommandsSubmission = {
132
127
  params: readonly unknown[];
133
128
  clientId?: string;
134
129
  caller?: StatewireHost.Caller;
135
- onAck?: () => void;
130
+ onApplied?: () => void;
136
131
  onSettle?: (outcome: CommandsOutcome) => void;
137
132
  };
138
133
 
139
134
  export type CommandsHost = {
140
135
  setListener: (listener: CommandsListener | null) => void;
141
136
  submit?: (call: CommandsSubmission) => Promise<unknown>;
137
+ /** Registered command names, read live. */
138
+ methods?: () => string[];
142
139
  };
143
140
 
144
141
  const commandsHosts = new WeakMap<object, CommandsHost>();
@@ -160,7 +157,7 @@ type QueuedCall = {
160
157
  };
161
158
 
162
159
  type ActiveInvocation = CommandsInvocation & {
163
- acked: boolean;
160
+ applied: boolean;
164
161
  origin?: CommandsSubmission;
165
162
  };
166
163
 
@@ -188,19 +185,24 @@ const LOCAL_CALLER: StatewireHost.Caller = Object.freeze({
188
185
  const toSendError = (error: unknown): StatewireSendError =>
189
186
  error instanceof StatewireSendError
190
187
  ? error
191
- : new StatewireSendError({
192
- fate: "rejected",
193
- cause: "server",
194
- message: error instanceof Error ? error.message : String(error),
195
- ...(error instanceof StatewireReject &&
196
- error.payload !== undefined && { payload: error.payload }),
197
- });
198
-
199
- const ackInvocation = (cell: CommandsCell, inv: ActiveInvocation) => {
200
- if (inv.acked) return;
201
- inv.acked = true;
202
- cell.listener?.onAck(inv);
203
- inv.origin?.onAck?.();
188
+ : error instanceof StatewireReject
189
+ ? new StatewireSendError({
190
+ fate: "rejected",
191
+ cause: "server",
192
+ message: error.message,
193
+ ...(error.payload !== undefined && { payload: error.payload }),
194
+ })
195
+ : new StatewireSendError({
196
+ fate: "failed",
197
+ cause: "server",
198
+ message: error instanceof Error ? error.message : String(error),
199
+ });
200
+
201
+ const applyInvocation = (cell: CommandsCell, inv: ActiveInvocation) => {
202
+ if (inv.applied) return;
203
+ inv.applied = true;
204
+ cell.listener?.onApplied(inv);
205
+ inv.origin?.onApplied?.();
204
206
  };
205
207
 
206
208
  const settleInvocation = (
@@ -209,7 +211,7 @@ const settleInvocation = (
209
211
  call: QueuedCall,
210
212
  settled: Settled,
211
213
  ) => {
212
- ackInvocation(cell, inv);
214
+ applyInvocation(cell, inv);
213
215
  if (settled.type === "resolved") {
214
216
  const outcome: CommandsOutcome =
215
217
  settled.value === undefined
@@ -236,8 +238,9 @@ const dispatch = (cell: CommandsCell, call: QueuedCall) => {
236
238
  const handler = cell.handlers![call.method];
237
239
  if (handler === undefined) {
238
240
  const error = new StatewireSendError({
239
- fate: "unknown-command",
241
+ fate: "rejected",
240
242
  cause: "server",
243
+ code: "unknown-command",
241
244
  });
242
245
  call.origin?.onSettle?.({ type: "rejected", error });
243
246
  call.reject(error);
@@ -248,7 +251,7 @@ const dispatch = (cell: CommandsCell, call: QueuedCall) => {
248
251
  params: call.params,
249
252
  ts: call.ts,
250
253
  seq: ++cell.seq,
251
- acked: false,
254
+ applied: false,
252
255
  ...(call.origin !== undefined && { origin: call.origin }),
253
256
  ...(call.origin?.clientId !== undefined && {
254
257
  clientId: call.origin.clientId,
@@ -259,7 +262,7 @@ const dispatch = (cell: CommandsCell, call: QueuedCall) => {
259
262
  cell.listener?.onCovered?.(inv);
260
263
  const ctx: StatewireHost.Ctx = {
261
264
  caller: call.origin?.caller ?? LOCAL_CALLER,
262
- ack: () => ackInvocation(cell, inv),
265
+ applied: () => applyInvocation(cell, inv),
263
266
  };
264
267
  try {
265
268
  const value = (
@@ -358,6 +361,7 @@ export const useStatewireCommands = <THandlers extends Statewire.Commands>(
358
361
  setListener: (listener) => {
359
362
  cell.listener = listener;
360
363
  },
364
+ methods: () => (cell.handlers ? Object.keys(cell.handlers) : []),
361
365
  submit: (origin) =>
362
366
  new Promise((resolve, reject) => {
363
367
  if (cell.unmounted) {
@@ -400,39 +404,18 @@ export const useStatewireCommands = <THandlers extends Statewire.Commands>(
400
404
  }, [built]);
401
405
 
402
406
  useEffect(() => {
407
+ if (commands) {
408
+ for (const method of Object.keys(commands)) {
409
+ if (method.startsWith("statewire/")) {
410
+ throw new Error(
411
+ `statewire: cannot register "${method}": the "statewire/" method namespace is reserved`,
412
+ );
413
+ }
414
+ }
415
+ }
403
416
  built.cell.handlers = commands || null;
404
417
  pump(built.cell);
405
418
  });
406
419
 
407
420
  return built.proxy;
408
421
  };
409
-
410
- // ─── emit ───────────────────────────────────────────────
411
-
412
- export type EmitHost = {
413
- setSink: (sink: ((event: unknown) => void) | null) => void;
414
- };
415
-
416
- const emitHosts = new WeakMap<object, EmitHost>();
417
-
418
- export const getEmitHost = (emit: object | undefined): EmitHost | undefined =>
419
- emit && emitHosts.get(emit);
420
-
421
- export const useStatewireEmit = <TEvent>(): ((event: TEvent) => void) => {
422
- const [built] = useState(() => {
423
- const cell = { sink: null as ((event: unknown) => void) | null };
424
- const emit = (event: TEvent) => {
425
- if (!cell.sink) {
426
- throw new Error("statewire: emit before a host bound the event sink");
427
- }
428
- cell.sink(sanitizeValue(event));
429
- };
430
- emitHosts.set(emit, {
431
- setSink: (sink) => {
432
- cell.sink = sink;
433
- },
434
- });
435
- return emit;
436
- });
437
- return built;
438
- };