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
@@ -1,27 +1,28 @@
1
+ import { useEffect, useState } from "react";
1
2
  import { applyOps } from "../wire/ops";
2
3
  import type { StatewirePath } from "../wire/ops";
3
4
  import type { StatewireResponse } from "./connection";
4
5
  import { bindCommandsHost } from "../host/host";
5
6
  import type { CommandsInvocation, CommandsListener } from "../host/host";
6
- import type { StatewireStateSource } from "../types";
7
+ import type { Statewire } from "../types";
8
+ import type { StatewireClient } from "../statewire";
7
9
  import { createMemorySessionStore } from "./sessionStore";
8
10
  import type {
9
11
  StatewireSessionRecord,
10
12
  StatewireSessionStore,
13
+ StatewireSessionStoreOptions,
11
14
  } from "./sessionStore";
12
- import { backoff } from "./timing";
15
+ import { retryDelay } from "./timing";
16
+ import { createMethodProxy } from "../util/methodProxy";
17
+ import { randomId } from "../util/randomId";
13
18
  import { StatewireSendError } from "../types";
14
- import type { SendFailure as StatewireSendFailure } from "../types";
15
19
  import type {
16
20
  RemoteLink,
21
+ StatewireRemoteProps,
17
22
  StatewireSubmitResult,
18
23
  StatewireWireCommand,
19
24
  } from "../transports/sse";
20
25
 
21
- export type SessionOptions = {
22
- sessionStore?: StatewireSessionStore | undefined;
23
- };
24
-
25
26
  /**
26
27
  * queued/trying/posted: overlay live, result pending. awaiting-result: acked
27
28
  * (overlay gone) but the result rides a late res; `posted: false` re-sends.
@@ -39,7 +40,8 @@ type PendingState =
39
40
  type Pending = {
40
41
  readonly seq: number;
41
42
  readonly command: StatewireWireCommand;
42
- readonly inv: CommandsInvocation;
43
+ /** null for context submissions — they carry no command lifecycle. */
44
+ readonly inv: CommandsInvocation | null;
43
45
  state: PendingState;
44
46
  resolve: (payload: unknown) => void;
45
47
  reject: (error: StatewireSendError) => void;
@@ -47,7 +49,7 @@ type Pending = {
47
49
 
48
50
  type Outcome =
49
51
  | { ok: true; payload: unknown }
50
- | { ok: false; failure: StatewireSendFailure };
52
+ | { ok: false; failure: StatewireClient.SendFailure };
51
53
 
52
54
  const isResolved = (state: PendingState) =>
53
55
  state.at === "settled-live" || state.at === "done";
@@ -55,6 +57,15 @@ const isResolved = (state: PendingState) =>
55
57
  const isStateSettled = (state: PendingState) =>
56
58
  state.at === "awaiting-result" || state.at === "done";
57
59
 
60
+ // The pump admits an entry as trying before sending; only the pump itself
61
+ // requeues, and a done entry is never still pending.
62
+ const inFlight = (state: PendingState) => {
63
+ if (state.at === "queued" || state.at === "done") {
64
+ throw new Error(`statewire: ${state.at} entry observed after a send`);
65
+ }
66
+ return state;
67
+ };
68
+
58
69
  const legalTransitions: Record<PendingState["at"], PendingState["at"][]> = {
59
70
  queued: ["queued", "trying", "awaiting-result", "settled-live", "done"],
60
71
  // trying -> queued: an answered unknown-client proves the seq was never admitted.
@@ -92,20 +103,22 @@ const transition = (
92
103
  }
93
104
  const covers = !isStateSettled(prev) && isStateSettled(next);
94
105
  entry.state = next;
95
- if (covers) s.lifecycle?.onCovered?.(entry.inv);
106
+ if (covers && entry.inv) s.lifecycle?.onCovered?.(entry.inv);
96
107
  if (settles && outcome !== undefined) {
97
108
  if (outcome.ok) {
98
109
  entry.resolve(outcome.payload);
99
- s.lifecycle?.onSettle(entry.inv, {
100
- type: "accepted",
101
- payload: outcome.payload,
102
- });
110
+ if (entry.inv)
111
+ s.lifecycle?.onSettle(entry.inv, {
112
+ type: "accepted",
113
+ payload: outcome.payload,
114
+ });
103
115
  } else {
104
116
  entry.reject(new StatewireSendError(outcome.failure));
105
- s.lifecycle?.onSettle(entry.inv, {
106
- type: "rejected",
107
- error: new StatewireSendError(outcome.failure),
108
- });
117
+ if (entry.inv)
118
+ s.lifecycle?.onSettle(entry.inv, {
119
+ type: "rejected",
120
+ error: new StatewireSendError(outcome.failure),
121
+ });
109
122
  }
110
123
  }
111
124
  if (next.at === "done") {
@@ -122,10 +135,11 @@ const carried = (
122
135
  ? { posted: state.at === "posted", failedAttempts: state.failedAttempts }
123
136
  : { posted: state.posted, failedAttempts: state.failedAttempts };
124
137
 
125
- const newClientId = () =>
126
- `c-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
138
+ const newClientId = () => `c-${randomId()}`;
127
139
 
128
- const rejectedSend = (failure: StatewireSendFailure): Promise<unknown> => {
140
+ const rejectedSend = (
141
+ failure: StatewireClient.SendFailure,
142
+ ): Promise<unknown> => {
129
143
  const result = Promise.reject<unknown>(new StatewireSendError(failure));
130
144
  result.catch(() => {});
131
145
  return result;
@@ -135,14 +149,16 @@ type AttachState = "detached" | "attached" | "finished";
135
149
 
136
150
  type Session = {
137
151
  record: StatewireSessionRecord;
152
+ ctx: StatewireRemoteProps;
138
153
  recordPersisted: boolean;
139
154
  store: StatewireSessionStore;
140
155
  lastAck: number;
156
+ context: Record<string, unknown> | undefined;
141
157
  pending: Pending[];
142
158
  canonical: unknown;
143
159
  lastPublished: unknown;
144
160
  changed: StatewirePath[];
145
- source: StatewireStateSource;
161
+ source: Statewire.StateSource;
146
162
  attach: AttachState;
147
163
  attachEpoch: number;
148
164
  running: boolean;
@@ -156,38 +172,30 @@ type Session = {
156
172
  commands: Record<string, (...params: unknown[]) => Promise<unknown>>;
157
173
  };
158
174
 
159
- const createCommandsProxy = (
160
- send: (command: StatewireWireCommand) => Promise<unknown>,
161
- ): Session["commands"] => {
162
- const memo = new Map<string, (...params: unknown[]) => Promise<unknown>>();
163
- return new Proxy({} as Session["commands"], {
164
- get: (_target, method) => {
165
- if (typeof method !== "string") return undefined;
166
- let fn = memo.get(method);
167
- if (fn === undefined) {
168
- fn = (...params: unknown[]) => send({ method, params });
169
- memo.set(method, fn);
170
- }
171
- return fn;
172
- },
173
- });
174
- };
175
-
176
- const createSession = (options: SessionOptions): Session => {
175
+ const createSession = (options: StatewireSessionStoreOptions): Session => {
177
176
  const store = options.sessionStore ?? createMemorySessionStore();
178
177
  const stored = store.load();
179
178
  const record = stored ?? { clientId: newClientId(), nextSeq: 1 };
180
179
  const s: Session = {
181
180
  record,
181
+ ctx: {
182
+ get clientId() {
183
+ return s.record.clientId;
184
+ },
185
+ onSnapshot: (args) => handleSnapshot(s, args),
186
+ onOps: (args) => handleOps(s, args),
187
+ onFinish: (args) => handleFinish(s, args),
188
+ },
182
189
  recordPersisted: stored !== null,
183
190
  store,
184
191
  lastAck: -1,
192
+ context: undefined,
185
193
  pending: [],
186
194
  canonical: undefined,
187
195
  lastPublished: undefined,
188
196
  changed: [],
189
197
  source: {
190
- root: () => s.canonical,
198
+ snapshot: () => s.canonical,
191
199
  takeChanged: () => {
192
200
  const changed = s.changed;
193
201
  s.changed = [];
@@ -207,7 +215,7 @@ const createSession = (options: SessionOptions): Session => {
207
215
  notify: () => {},
208
216
  emitEvent: () => {},
209
217
  send: (command) => sendCommand(s, command),
210
- commands: createCommandsProxy((command) => s.send(command)),
218
+ commands: createMethodProxy((method, params) => s.send({ method, params })),
211
219
  };
212
220
  bindCommandsHost(s.commands, {
213
221
  setListener: (listener) => {
@@ -231,12 +239,25 @@ const wake = (s: Session) => {
231
239
 
232
240
  const waitForWake = (s: Session) => new Promise<void>((r) => s.waiters.push(r));
233
241
 
242
+ const sleepUntilWake = (s: Session, delayMs: number) =>
243
+ new Promise<void>((resolve) => {
244
+ const waiter = () => {
245
+ clearTimeout(timer);
246
+ resolve();
247
+ };
248
+ const timer = setTimeout(() => {
249
+ s.waiters = s.waiters.filter((w) => w !== waiter);
250
+ resolve();
251
+ }, delayMs);
252
+ s.waiters.push(waiter);
253
+ });
254
+
234
255
  const settleAll = (
235
256
  s: Session,
236
257
  detach: "evicted" | "unmount" | "gone" | "superseded",
237
258
  verdict?: {
238
259
  entry: Pending;
239
- failure: StatewireSendFailure;
260
+ failure: StatewireClient.SendFailure;
240
261
  },
241
262
  ) => {
242
263
  const cause = (
@@ -248,7 +269,7 @@ const settleAll = (
248
269
  } as const
249
270
  )[detach];
250
271
  const batch = s.pending;
251
- const detachFailure = (entry: Pending): StatewireSendFailure => {
272
+ const detachFailure = (entry: Pending): StatewireClient.SendFailure => {
252
273
  const state = entry.state;
253
274
  if (state.at === "awaiting-result")
254
275
  return { fate: "result-unavailable", cause };
@@ -271,13 +292,16 @@ const evict = (
271
292
  s: Session,
272
293
  verdict?: {
273
294
  entry: Pending;
274
- failure: StatewireSendFailure;
295
+ failure: StatewireClient.SendFailure;
275
296
  },
276
297
  ) => {
277
298
  settleAll(s, "evicted", verdict);
278
299
  s.record = { clientId: newClientId(), nextSeq: 1 };
279
300
  s.store.save(s.record);
280
301
  s.attach = "detached";
302
+ // The server dropped this client's record and its stored context with it:
303
+ // the new session opens with a fresh full root replace.
304
+ if (s.context !== undefined) void s.send({ context: s.context });
281
305
  publish(s);
282
306
  wake(s);
283
307
  };
@@ -285,7 +309,7 @@ const evict = (
285
309
  const noteAcked = (s: Session, seq: number) => {
286
310
  for (const entry of s.pending) {
287
311
  if (entry.seq > seq || isStateSettled(entry.state)) continue;
288
- s.lifecycle?.onAck(entry.inv);
312
+ if (entry.inv) s.lifecycle?.onAck(entry.inv);
289
313
  }
290
314
  };
291
315
 
@@ -347,12 +371,16 @@ const applyResponse = (s: Session, response: StatewireResponse) => {
347
371
  ? {
348
372
  fate: "invalid-params",
349
373
  cause: "server",
350
- message: response.message ?? "",
374
+ message:
375
+ response.message ??
376
+ protocolError("rejected response missing message"),
351
377
  }
352
378
  : {
353
379
  fate: "rejected",
354
380
  cause: "server",
355
- message: response.message ?? "",
381
+ message:
382
+ response.message ??
383
+ protocolError("rejected response missing message"),
356
384
  ...(response.payload !== undefined && {
357
385
  payload: response.payload,
358
386
  }),
@@ -382,12 +410,9 @@ const ingestOps = (s: Session, ops: unknown[]) => {
382
410
  return applied.events;
383
411
  };
384
412
 
385
- const publishIngested = (
386
- s: Session,
387
- events: { event: unknown; state: unknown }[],
388
- ) => {
413
+ const publishIngested = (s: Session, events: unknown[]) => {
389
414
  publish(s);
390
- for (const { event } of events) s.emitEvent(event);
415
+ for (const event of events) s.emitEvent(event);
391
416
  wake(s);
392
417
  };
393
418
 
@@ -459,7 +484,7 @@ const handleOps = (
459
484
  };
460
485
 
461
486
  const handleFinish = (s: Session, { reason }: { reason: string }) => {
462
- if (reason === "ended" || reason === "error") return;
487
+ if (reason === "evicted" || reason === "error") return;
463
488
  s.attach = "finished";
464
489
  settleAll(s, reason === "superseded" ? "superseded" : "gone");
465
490
  publish(s);
@@ -504,18 +529,12 @@ const pump = async (s: Session) => {
504
529
  } catch (error) {
505
530
  if (!s.running || isFinished(s)) return;
506
531
  if (!s.pending.includes(entry)) continue;
507
- const state = entry.state;
508
- if (state.at === "queued" || state.at === "done") continue;
532
+ const state = inFlight(entry.state);
509
533
  transition(s, entry, {
510
534
  ...state,
511
535
  failedAttempts: state.failedAttempts + 1,
512
536
  });
513
- const { retryAfterMs } = error as { retryAfterMs?: unknown };
514
- const delay =
515
- typeof retryAfterMs === "number"
516
- ? retryAfterMs
517
- : backoff(state.failedAttempts + 1);
518
- await new Promise((r) => setTimeout(r, delay));
537
+ await sleepUntilWake(s, retryDelay(error, state.failedAttempts + 1));
519
538
  continue;
520
539
  }
521
540
  if (!s.running) return;
@@ -535,13 +554,12 @@ const pump = async (s: Session) => {
535
554
  ) {
536
555
  transition(s, entry, { ...state, posted: true });
537
556
  }
538
- s.lifecycle?.onPosted?.(entry.inv);
557
+ if (entry.inv) s.lifecycle?.onPosted?.(entry.inv);
539
558
  if (response !== undefined) {
540
559
  applyResponse(s, response);
541
560
  publish(s);
542
561
  }
543
562
  } else if (result.type === "rejected") {
544
- const { message, payload } = result;
545
563
  transition(
546
564
  s,
547
565
  entry,
@@ -551,8 +569,7 @@ const pump = async (s: Session) => {
551
569
  failure: {
552
570
  fate: "rejected",
553
571
  cause: "server",
554
- message,
555
- ...(payload !== undefined && { payload }),
572
+ message: result.message,
556
573
  },
557
574
  },
558
575
  );
@@ -589,16 +606,14 @@ const pump = async (s: Session) => {
589
606
  );
590
607
  // Every attempt answered means the server never saw it; a thrown
591
608
  // attempt may have left the machine unanswered, so the verdict is "lost".
592
- const state = entry.state;
593
- const failure: StatewireSendFailure =
609
+ const state = inFlight(entry.state);
610
+ const failure: StatewireClient.SendFailure =
594
611
  state.at === "awaiting-result"
595
612
  ? { fate: "result-unavailable", cause: "session-lost" }
596
- : state.at === "queued" || state.at === "done"
597
- ? { fate: "aborted", cause: "session-lost" }
598
- : {
599
- fate: state.failedAttempts > 0 ? "lost" : "aborted",
600
- cause: "session-lost",
601
- };
613
+ : {
614
+ fate: state.failedAttempts > 0 ? "lost" : "aborted",
615
+ cause: "session-lost",
616
+ };
602
617
  evict(s, { entry, failure });
603
618
  s.transport?.connection.reconnect();
604
619
  }
@@ -646,13 +661,16 @@ const sendCommand = (
646
661
  const entry: Pending = {
647
662
  seq,
648
663
  command,
649
- inv: {
650
- method: command.method,
651
- params: command.params,
652
- ts: Date.now(),
653
- seq,
654
- clientId: s.record.clientId,
655
- },
664
+ inv:
665
+ command.method === undefined
666
+ ? null
667
+ : {
668
+ method: command.method,
669
+ params: command.params,
670
+ ts: Date.now(),
671
+ seq,
672
+ clientId: s.record.clientId,
673
+ },
656
674
  state: { at: "queued", pendingMarked: false },
657
675
  resolve: () => {},
658
676
  reject: () => {},
@@ -663,17 +681,38 @@ const sendCommand = (
663
681
  });
664
682
  result.catch(() => {});
665
683
  s.pending.push(entry);
666
- s.lifecycle?.onStart(entry.inv);
684
+ if (entry.inv) s.lifecycle?.onStart(entry.inv);
667
685
  void pump(s);
668
686
  return result;
669
687
  };
670
688
 
671
- export {
672
- createSession,
673
- handleSnapshot,
674
- handleFinish,
675
- handleOps,
676
- setTransport,
677
- start,
678
- stop,
689
+ const setContext = (
690
+ s: Session,
691
+ value: Record<string, unknown>,
692
+ ): Promise<unknown> => {
693
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
694
+ throw new Error("statewire: context must be a plain object");
695
+ }
696
+ s.context = value;
697
+ return s.send({ context: value });
698
+ };
699
+
700
+ export const useStatewireSession = (
701
+ options: StatewireSessionStoreOptions,
702
+ emit: (event: unknown) => void,
703
+ ): Session => {
704
+ const [s] = useState(() => createSession(options));
705
+ useEffect(() => {
706
+ s.emitEvent = emit;
707
+ return () => {
708
+ s.emitEvent = () => {};
709
+ };
710
+ }, [s, emit]);
711
+ useEffect(() => {
712
+ start(s);
713
+ return () => stop(s);
714
+ }, [s]);
715
+ return s;
679
716
  };
717
+
718
+ export { setContext, setTransport };
@@ -9,7 +9,7 @@ export type StatewireSessionStore = {
9
9
  };
10
10
 
11
11
  export type StatewireSessionStoreOptions = {
12
- sessionStore?: StatewireSessionStore;
12
+ sessionStore?: StatewireSessionStore | undefined;
13
13
  };
14
14
 
15
15
  export const createMemorySessionStore = (): StatewireSessionStore => {
@@ -6,3 +6,9 @@ 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
+ };
@@ -0,0 +1,33 @@
1
+ import type { StatewireHostInternal } from "../host-internal";
2
+
3
+ export const CLIENT_QUERY_PARAM = "client";
4
+
5
+ export const clientIdFromUrl = (url: string | null | undefined): string => {
6
+ if (!url) {
7
+ throw new Error("statewire: cannot resolve client id: missing url");
8
+ }
9
+ let parsed: URL;
10
+ try {
11
+ parsed = new URL(url, "http://statewire");
12
+ } catch {
13
+ throw new Error(
14
+ `statewire: cannot resolve client id: unparseable url "${url}"`,
15
+ );
16
+ }
17
+ const clientId = parsed.searchParams.get(CLIENT_QUERY_PARAM);
18
+ if (!clientId) {
19
+ throw new Error(
20
+ `statewire: url is missing the "${CLIENT_QUERY_PARAM}" query parameter`,
21
+ );
22
+ }
23
+ return clientId;
24
+ };
25
+
26
+ export const textFrameOrClose = (
27
+ socket: Pick<StatewireHostInternal.Socket, "close">,
28
+ data: unknown,
29
+ ): string | null => {
30
+ if (typeof data === "string") return data;
31
+ socket.close(1008, "text frames only");
32
+ return null;
33
+ };