statewire 0.18.1 → 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 +296 -401
  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 +9 -19
  23. package/dist/host/http.js.map +1 -1
  24. package/dist/host/socket.d.ts +48 -25
  25. package/dist/host/socket.d.ts.map +1 -1
  26. package/dist/host/socket.js +399 -266
  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 -12
  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 -20
  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 +54 -89
  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 -18
  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 +442 -517
  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 +23 -31
  74. package/src/host/socket.ts +560 -339
  75. package/src/host/synced.ts +60 -0
  76. package/src/host-internal.ts +21 -7
  77. package/src/host.ts +40 -8
  78. package/src/index.ts +1 -0
  79. package/src/statewire.ts +8 -37
  80. package/src/storage.ts +90 -157
  81. package/src/transports/remote.ts +0 -7
  82. package/src/transports/sse.ts +8 -3
  83. package/src/transports/testing.ts +34 -18
  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,24 +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(
230
- h,
231
- registration.rec,
232
- frame,
233
- registration.conn.auth,
234
- registration.conn.selection,
235
- );
236
- if (admission.type === "refused") {
237
- sendTo(h, registration.conn, JSON.stringify(admission.syn));
238
- }
239
- };
240
-
241
232
  const getStream = async (
242
233
  h: SocketHostState,
243
234
  request: Request,
@@ -283,10 +274,7 @@ const postStream = async (
283
274
  }
284
275
  let frame: ParsedFrame;
285
276
  try {
286
- frame =
287
- body === null && !raw.trim()
288
- ? { ctx: [], cmd: [] }
289
- : parseFrameValue(body);
277
+ frame = body === null && !raw.trim() ? { cmd: [] } : parseFrameValue(body);
290
278
  } catch (error) {
291
279
  if (!(error instanceof Error) || error.name !== "StatewireViolation") {
292
280
  throw error;
@@ -307,14 +295,16 @@ const postStream = async (
307
295
  ]);
308
296
  if ("fin" in selection)
309
297
  return Response.json(selection, { status: selection.fin.status });
310
- const attachment = attachSse(h, client.clientId, authorized.auth, selection);
311
- if (attachment.registration === null) return streamResponse(attachment, null);
312
- const lease = mintLease();
313
- attachment.registration.conn.lease = lease;
298
+ let attachment: SseAttachment;
314
299
  try {
315
- admitAttachFrame(h, attachment.registration, frame);
300
+ attachment = attachSse(
301
+ h,
302
+ client.clientId,
303
+ authorized.auth,
304
+ selection,
305
+ frame,
306
+ );
316
307
  } catch (error) {
317
- closeConn(h, attachment.registration.conn, 1008, "invalid-context");
318
308
  if (!(error instanceof Error) || error.name !== "StatewireViolation")
319
309
  throw error;
320
310
  return Response.json(
@@ -322,6 +312,9 @@ const postStream = async (
322
312
  { status: 400 },
323
313
  );
324
314
  }
315
+ if (attachment.registration === null) return streamResponse(attachment, null);
316
+ const lease = mintLease();
317
+ attachment.registration.conn.lease = lease;
325
318
  return streamResponse(attachment, lease);
326
319
  };
327
320
 
@@ -376,7 +369,6 @@ export const frames = async (
376
369
  return Response.json({ error: "stale-lease" }, { status: 423 });
377
370
  }
378
371
  const clientId = leased.clientId;
379
- const rec = h.clients.get(clientId)!;
380
372
  const raw = await readFrameBody(request);
381
373
  if (raw === null) {
382
374
  return Response.json({ error: "frame body too large" }, { status: 413 });
@@ -395,7 +387,7 @@ export const frames = async (
395
387
  }
396
388
  let admission: ReturnType<typeof admitFrame>;
397
389
  try {
398
- admission = admitFrame(h, rec, frame, leased.auth, leased.selection);
390
+ admission = admitFrame(h, leased, frame);
399
391
  } catch (error) {
400
392
  if (!(error instanceof Error) || error.name !== "StatewireViolation")
401
393
  throw error;