statewire 0.18.2 → 0.19.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 (86) hide show
  1. package/dist/client/connection.d.ts +8 -5
  2. package/dist/client/connection.d.ts.map +1 -1
  3. package/dist/client/connection.js +24 -13
  4. package/dist/client/connection.js.map +1 -1
  5. package/dist/client/session.d.ts +13 -15
  6. package/dist/client/session.d.ts.map +1 -1
  7. package/dist/client/session.js +291 -256
  8. package/dist/client/session.js.map +1 -1
  9. package/dist/client/timing.d.ts +2 -1
  10. package/dist/client/timing.d.ts.map +1 -1
  11. package/dist/client/timing.js +2 -1
  12. package/dist/client/timing.js.map +1 -1
  13. package/dist/host/engine.d.ts +2 -0
  14. package/dist/host/engine.d.ts.map +1 -1
  15. package/dist/host/engine.js +2 -0
  16. package/dist/host/engine.js.map +1 -1
  17. package/dist/host/host.d.ts +3 -1
  18. package/dist/host/host.d.ts.map +1 -1
  19. package/dist/host/host.js +38 -7
  20. package/dist/host/host.js.map +1 -1
  21. package/dist/host/http.d.ts.map +1 -1
  22. package/dist/host/http.js +8 -17
  23. package/dist/host/http.js.map +1 -1
  24. package/dist/host/socket.d.ts +47 -23
  25. package/dist/host/socket.d.ts.map +1 -1
  26. package/dist/host/socket.js +389 -245
  27. package/dist/host/socket.js.map +1 -1
  28. package/dist/host/synced.d.ts +9 -0
  29. package/dist/host/synced.d.ts.map +1 -0
  30. package/dist/host/synced.js +45 -0
  31. package/dist/host/synced.js.map +1 -0
  32. package/dist/host-internal.d.ts +20 -8
  33. package/dist/host-internal.d.ts.map +1 -1
  34. package/dist/host.d.ts +33 -9
  35. package/dist/host.d.ts.map +1 -1
  36. package/dist/host.js +2 -1
  37. package/dist/index.d.ts +2 -2
  38. package/dist/index.js +2 -1
  39. package/dist/statewire.d.ts +8 -10
  40. package/dist/statewire.d.ts.map +1 -1
  41. package/dist/statewire.js +0 -14
  42. package/dist/statewire.js.map +1 -1
  43. package/dist/storage.d.ts +2 -5
  44. package/dist/storage.d.ts.map +1 -1
  45. package/dist/storage.js +55 -41
  46. package/dist/storage.js.map +1 -1
  47. package/dist/transports/remote.d.ts.map +1 -1
  48. package/dist/transports/remote.js +1 -6
  49. package/dist/transports/remote.js.map +1 -1
  50. package/dist/transports/sse.d.ts +2 -5
  51. package/dist/transports/sse.d.ts.map +1 -1
  52. package/dist/transports/sse.js.map +1 -1
  53. package/dist/transports/testing.d.ts +7 -5
  54. package/dist/transports/testing.d.ts.map +1 -1
  55. package/dist/transports/testing.js +19 -19
  56. package/dist/transports/testing.js.map +1 -1
  57. package/dist/types.d.ts +3 -5
  58. package/dist/types.d.ts.map +1 -1
  59. package/dist/types.js.map +1 -1
  60. package/dist/wire/limits.d.ts +2 -2
  61. package/dist/wire/limits.js +2 -2
  62. package/dist/wire/limits.js.map +1 -1
  63. package/dist/wire/protocol.d.ts +6 -1
  64. package/dist/wire/protocol.d.ts.map +1 -1
  65. package/dist/wire/protocol.js +33 -12
  66. package/dist/wire/protocol.js.map +1 -1
  67. package/package.json +1 -1
  68. package/src/client/connection.ts +46 -33
  69. package/src/client/session.ts +434 -365
  70. package/src/client/timing.ts +2 -0
  71. package/src/host/engine.ts +3 -1
  72. package/src/host/host.ts +50 -3
  73. package/src/host/http.ts +22 -23
  74. package/src/host/socket.ts +550 -321
  75. package/src/host/synced.ts +60 -0
  76. package/src/host-internal.ts +21 -3
  77. package/src/host.ts +40 -8
  78. package/src/index.ts +1 -0
  79. package/src/statewire.ts +8 -30
  80. package/src/storage.ts +96 -79
  81. package/src/transports/remote.ts +0 -7
  82. package/src/transports/sse.ts +8 -3
  83. package/src/transports/testing.ts +34 -19
  84. package/src/types.ts +3 -5
  85. package/src/wire/limits.ts +1 -1
  86. package/src/wire/protocol.ts +40 -11
@@ -6,6 +6,8 @@ export const timing = {
6
6
  random: Math.random,
7
7
  };
8
8
 
9
+ export const retryHint = (ms: number) => Math.min(ms, timing.backoffMaxMs);
10
+
9
11
  export const backoff = (attempt: number) => {
10
12
  const limit = Math.min(
11
13
  timing.backoffBaseMs * 2 ** (attempt - 1),
@@ -3,11 +3,13 @@
3
3
  * message/payload travel to the client's rejected promise.
4
4
  */
5
5
  export class StatewireReject extends Error {
6
+ readonly code?: string;
6
7
  readonly payload?: unknown;
7
8
 
8
- constructor(message: string, options?: { payload?: unknown }) {
9
+ constructor(message: string, options?: { code?: string; payload?: unknown }) {
9
10
  super(message);
10
11
  this.name = "StatewireReject";
12
+ if (options?.code !== undefined) this.code = options.code;
11
13
  if (options?.payload !== undefined) this.payload = options.payload;
12
14
  }
13
15
  }
package/src/host/host.ts CHANGED
@@ -16,6 +16,9 @@ const isDev = () =>
16
16
 
17
17
  const stateHosts = new WeakMap<object, StatewireHost.StateHost>();
18
18
 
19
+ export const documentIdentity = (protocol: string, key?: readonly string[]) =>
20
+ JSON.stringify([protocol, key ?? null]);
21
+
19
22
  /** Host-side accessor for a `useStatewireState` state object's source (`snapshot()` reads the committed state). */
20
23
  export const getStateHost = (
21
24
  state: object,
@@ -25,8 +28,10 @@ type StateCell = {
25
28
  current: unknown;
26
29
  draft: StateDraft<object> | null;
27
30
  changed: StatewirePath[];
31
+ documents: Map<string, StatewireHost.Document>;
28
32
  onOp: ((op: Statewire.Op) => void) | null;
29
33
  onChange: (() => void) | null;
34
+ onDocuments: (() => void) | null;
30
35
  ctl: StatewireHost.StateCtl;
31
36
  host: StatewireHost.StateHost;
32
37
  bump: () => void;
@@ -45,8 +50,10 @@ export const useStatewireState = <TState>(
45
50
  current: sanitizeValue(init()),
46
51
  draft: null,
47
52
  changed: [[]],
53
+ documents: new Map(),
48
54
  onOp: null,
49
55
  onChange: null,
56
+ onDocuments: null,
50
57
  bump: () => setVersion((v) => v + 1),
51
58
  ctl: {
52
59
  apply: (ops) => {
@@ -73,6 +80,38 @@ export const useStatewireState = <TState>(
73
80
  c.changed = [];
74
81
  return changed;
75
82
  },
83
+ mount: (protocol, value, options) => {
84
+ const key = options?.key;
85
+ const main = options?.main ?? key === undefined;
86
+ if (main) {
87
+ for (const [id, doc] of c.documents) {
88
+ if (
89
+ doc.protocol === protocol &&
90
+ doc.main &&
91
+ id !== documentIdentity(protocol, key)
92
+ )
93
+ throw new Error(
94
+ `statewire: ${protocol} already has a main document`,
95
+ );
96
+ }
97
+ }
98
+ c.documents.set(documentIdentity(protocol, key), {
99
+ protocol,
100
+ key: key === undefined ? null : [...key],
101
+ main,
102
+ value: sanitizeValue(value),
103
+ });
104
+ c.onDocuments?.();
105
+ },
106
+ unmount: (protocol, key) => {
107
+ if (!c.documents.delete(documentIdentity(protocol, key)))
108
+ throw new Error("statewire: document is not mounted");
109
+ c.onDocuments?.();
110
+ },
111
+ documents: () => c.documents,
112
+ setOnDocuments: (listener) => {
113
+ c.onDocuments = listener;
114
+ },
76
115
  },
77
116
  };
78
117
  return c;
@@ -128,6 +167,7 @@ export type CommandsSubmission = {
128
167
  params: readonly unknown[];
129
168
  clientId?: string;
130
169
  caller?: StatewireHost.Caller;
170
+ attach?: StatewireHost.Attach;
131
171
  onApplied?: () => void;
132
172
  onSettle?: (outcome: CommandsOutcome) => void;
133
173
  };
@@ -178,13 +218,12 @@ type CommandsCell = {
178
218
 
179
219
  const STALL_WARN_MS = 5000;
180
220
 
181
- // In-process dispatch has no wire client: never any ambient context, trivially attached.
221
+ // In-process dispatch has no wire client: trivially attached.
182
222
  const LOCAL_CALLER: StatewireHost.Caller = Object.freeze({
183
223
  get clientId(): string {
184
224
  throw new Error("statewire: in-process dispatch has no client id");
185
225
  },
186
226
  auth: undefined,
187
- context: null,
188
227
  isConnected: true,
189
228
  });
190
229
 
@@ -196,6 +235,7 @@ const toSendError = (error: unknown): StatewireSendError =>
196
235
  fate: "rejected",
197
236
  cause: "server",
198
237
  message: error.message,
238
+ ...(error.code !== undefined && { code: error.code }),
199
239
  ...(error.payload !== undefined && { payload: error.payload }),
200
240
  })
201
241
  : new StatewireSendError({
@@ -221,8 +261,8 @@ const settleInvocation = (
221
261
  if (inv.settled) return;
222
262
  inv.settled = true;
223
263
  cell.active.delete(inv);
224
- applyInvocation(cell, inv);
225
264
  if (settled.type === "resolved") {
265
+ applyInvocation(cell, inv);
226
266
  const outcome: CommandsOutcome =
227
267
  settled.value === undefined
228
268
  ? { type: "accepted" }
@@ -231,6 +271,7 @@ const settleInvocation = (
231
271
  inv.origin?.onSettle?.(outcome);
232
272
  call.resolve(outcome.payload);
233
273
  } else {
274
+ cell.listener?.onCovered?.(inv);
234
275
  const error = toSendError(settled.error);
235
276
  const outcome: CommandsOutcome = { type: "rejected", error };
236
277
  cell.listener?.onSettle(inv, outcome);
@@ -299,8 +340,14 @@ const dispatch = (cell: CommandsCell, call: QueuedCall) => {
299
340
  cell.active.set(inv, call);
300
341
  cell.listener?.onStart(inv);
301
342
  cell.listener?.onPosted?.(inv);
343
+ const attach = call.origin?.attach;
302
344
  const ctx: StatewireHost.Ctx = {
303
345
  caller: call.origin?.caller ?? LOCAL_CALLER,
346
+ get attach() {
347
+ if (attach === undefined)
348
+ throw new Error("statewire: in-process dispatch has no attach");
349
+ return attach;
350
+ },
304
351
  signal: inv.controller.signal,
305
352
  applied: () => {
306
353
  if (!inv.settled) applyInvocation(cell, inv);
package/src/host/http.ts CHANGED
@@ -141,6 +141,7 @@ const attachSse = (
141
141
  clientId: string,
142
142
  auth: unknown,
143
143
  selection: StatewireProtocol.Selection,
144
+ frame?: ParsedFrame,
144
145
  ): SseAttachment => {
145
146
  let controller: ReadableStreamDefaultController<Uint8Array>;
146
147
  let connection: Registration["connection"] | null = null;
@@ -194,7 +195,15 @@ const attachSse = (
194
195
  }
195
196
  },
196
197
  };
197
- const registration = register(h, clientId, socket, false, auth, selection);
198
+ const registration = register(
199
+ h,
200
+ clientId,
201
+ socket,
202
+ false,
203
+ auth,
204
+ selection,
205
+ frame,
206
+ );
198
207
  connection = registration?.connection ?? null;
199
208
  if (!open) connection?.close();
200
209
  return {
@@ -220,18 +229,6 @@ const streamResponse = (
220
229
  });
221
230
  };
222
231
 
223
- const admitAttachFrame = (
224
- h: SocketHostState,
225
- registration: Registration,
226
- frame: ParsedFrame,
227
- ) => {
228
- if (frame.ctx.length === 0 && frame.cmd.length === 0) return;
229
- const admission = admitFrame(h, registration.conn, frame);
230
- if (admission.type === "refused") {
231
- sendTo(h, registration.conn, JSON.stringify(admission.syn));
232
- }
233
- };
234
-
235
232
  const getStream = async (
236
233
  h: SocketHostState,
237
234
  request: Request,
@@ -277,10 +274,7 @@ const postStream = async (
277
274
  }
278
275
  let frame: ParsedFrame;
279
276
  try {
280
- frame =
281
- body === null && !raw.trim()
282
- ? { ctx: [], cmd: [] }
283
- : parseFrameValue(body);
277
+ frame = body === null && !raw.trim() ? { cmd: [] } : parseFrameValue(body);
284
278
  } catch (error) {
285
279
  if (!(error instanceof Error) || error.name !== "StatewireViolation") {
286
280
  throw error;
@@ -301,14 +295,16 @@ const postStream = async (
301
295
  ]);
302
296
  if ("fin" in selection)
303
297
  return Response.json(selection, { status: selection.fin.status });
304
- const attachment = attachSse(h, client.clientId, authorized.auth, selection);
305
- if (attachment.registration === null) return streamResponse(attachment, null);
306
- const lease = mintLease();
307
- attachment.registration.conn.lease = lease;
298
+ let attachment: SseAttachment;
308
299
  try {
309
- admitAttachFrame(h, attachment.registration, frame);
300
+ attachment = attachSse(
301
+ h,
302
+ client.clientId,
303
+ authorized.auth,
304
+ selection,
305
+ frame,
306
+ );
310
307
  } catch (error) {
311
- closeConn(h, attachment.registration.conn, 1008, "invalid-context");
312
308
  if (!(error instanceof Error) || error.name !== "StatewireViolation")
313
309
  throw error;
314
310
  return Response.json(
@@ -316,6 +312,9 @@ const postStream = async (
316
312
  { status: 400 },
317
313
  );
318
314
  }
315
+ if (attachment.registration === null) return streamResponse(attachment, null);
316
+ const lease = mintLease();
317
+ attachment.registration.conn.lease = lease;
319
318
  return streamResponse(attachment, lease);
320
319
  };
321
320