statewire 0.19.4 → 0.21.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 (73) hide show
  1. package/README.md +15 -1
  2. package/dist/client/connection.d.ts.map +1 -1
  3. package/dist/client/connection.js +18 -4
  4. package/dist/client/connection.js.map +1 -1
  5. package/dist/client/session.d.ts +10 -1
  6. package/dist/client/session.d.ts.map +1 -1
  7. package/dist/client/session.js +92 -27
  8. package/dist/client/session.js.map +1 -1
  9. package/dist/client/sessionStore.d.ts +2 -0
  10. package/dist/client/sessionStore.d.ts.map +1 -1
  11. package/dist/client/sessionStore.js.map +1 -1
  12. package/dist/host/host.d.ts +2 -1
  13. package/dist/host/host.d.ts.map +1 -1
  14. package/dist/host/host.js +20 -12
  15. package/dist/host/host.js.map +1 -1
  16. package/dist/host/http.d.ts.map +1 -1
  17. package/dist/host/http.js +14 -9
  18. package/dist/host/http.js.map +1 -1
  19. package/dist/host/socket.d.ts +14 -3
  20. package/dist/host/socket.d.ts.map +1 -1
  21. package/dist/host/socket.js +162 -20
  22. package/dist/host/socket.js.map +1 -1
  23. package/dist/host-internal.d.ts +5 -2
  24. package/dist/host-internal.d.ts.map +1 -1
  25. package/dist/host.d.ts +6 -0
  26. package/dist/host.d.ts.map +1 -1
  27. package/dist/index.d.ts +1 -1
  28. package/dist/statewire.d.ts +3 -0
  29. package/dist/statewire.d.ts.map +1 -1
  30. package/dist/statewire.js +23 -5
  31. package/dist/statewire.js.map +1 -1
  32. package/dist/transports/remote.d.ts.map +1 -1
  33. package/dist/transports/remote.js +3 -2
  34. package/dist/transports/remote.js.map +1 -1
  35. package/dist/transports/sse.d.ts +5 -0
  36. package/dist/transports/sse.d.ts.map +1 -1
  37. package/dist/transports/sse.js +12 -0
  38. package/dist/transports/sse.js.map +1 -1
  39. package/dist/transports/testing.d.ts +2 -0
  40. package/dist/transports/testing.d.ts.map +1 -1
  41. package/dist/transports/testing.js +11 -4
  42. package/dist/transports/testing.js.map +1 -1
  43. package/dist/types.d.ts +4 -0
  44. package/dist/types.d.ts.map +1 -1
  45. package/dist/types.js.map +1 -1
  46. package/dist/wire/context.d.ts +14 -0
  47. package/dist/wire/context.d.ts.map +1 -0
  48. package/dist/wire/context.js +31 -0
  49. package/dist/wire/context.js.map +1 -0
  50. package/dist/wire/limits.d.ts +2 -1
  51. package/dist/wire/limits.d.ts.map +1 -1
  52. package/dist/wire/limits.js +2 -1
  53. package/dist/wire/limits.js.map +1 -1
  54. package/dist/wire/protocol.d.ts +1 -1
  55. package/dist/wire/protocol.js +4 -4
  56. package/dist/wire/protocol.js.map +1 -1
  57. package/package.json +1 -1
  58. package/src/client/connection.ts +16 -4
  59. package/src/client/session.ts +128 -19
  60. package/src/client/sessionStore.ts +5 -1
  61. package/src/host/host.ts +18 -3
  62. package/src/host/http.ts +37 -5
  63. package/src/host/socket.ts +232 -13
  64. package/src/host-internal.ts +4 -0
  65. package/src/host.ts +5 -0
  66. package/src/statewire.ts +34 -3
  67. package/src/transports/remote.ts +3 -0
  68. package/src/transports/sse.ts +19 -0
  69. package/src/transports/testing.ts +14 -3
  70. package/src/types.ts +12 -0
  71. package/src/wire/context.ts +40 -0
  72. package/src/wire/limits.ts +2 -0
  73. package/src/wire/protocol.ts +1 -1
@@ -1,10 +1,11 @@
1
1
  import { reviveDocumentValue } from "../document.js";
2
+ import "../wire/limits.js";
2
3
  import { inRange, protocolOffers } from "../wire/protocol.js";
4
+ import { encodeContext } from "../wire/context.js";
3
5
  import { createMethodProxy } from "../util/methodProxy.js";
4
6
  import { StatewireSendError } from "../types.js";
5
7
  import { bindCommandsHost } from "../host/host.js";
6
8
  import { encodeParams, reviveResponse } from "../wire/meta.js";
7
- import "../wire/limits.js";
8
9
  import { applyDocumentOps, findDocument, mainDocument, resolveDocumentKey } from "../wire/documents.js";
9
10
  import { DocumentStorageContext, SessionStorageContext, createMemorySessionStore } from "./sessionStore.js";
10
11
  import { backoff, retryHint } from "./timing.js";
@@ -65,13 +66,13 @@ const storageFailed = (s, cause) => {
65
66
  s.transport?.storageFailed(error);
66
67
  };
67
68
  const setRecord = (s, record) => {
68
- s.record = record;
69
69
  if (!s.deactivated) try {
70
70
  s.store.save(record);
71
71
  } catch (cause) {
72
72
  storageFailed(s, cause);
73
73
  throw s.storageError;
74
74
  }
75
+ s.record = record;
75
76
  };
76
77
  const toStored = (p) => {
77
78
  const { protocol: _protocol, ...command } = p.command;
@@ -113,8 +114,16 @@ const syncBacklogInner = (s) => {
113
114
  });
114
115
  };
115
116
  const syncBacklog = (s) => {
116
- if (!s.shared) return syncBacklogInner(s);
117
- transactBackground(s, () => syncBacklogInner(s));
117
+ const shared = s.shared;
118
+ if (!shared) return syncBacklogInner(s);
119
+ if (shared.syncQueued) return;
120
+ shared.syncQueued = true;
121
+ transact(s, () => {
122
+ shared.syncQueued = false;
123
+ syncBacklogInner(s);
124
+ }).catch(() => {
125
+ shared.syncQueued = false;
126
+ });
118
127
  };
119
128
  const transact = (s, fn) => {
120
129
  if (!s.shared || s.deactivated) {
@@ -125,7 +134,7 @@ const transact = (s, fn) => {
125
134
  const result = shared.txq.then(() => s.store.lock(async () => {
126
135
  const stored = s.store.load();
127
136
  if (stored !== null && stored.clientId !== s.record.clientId) adoptReset(s, stored);
128
- else if (stored !== null) s.record = stored;
137
+ else if (stored !== null) s.record = boundRecord(stored);
129
138
  fn();
130
139
  const persistence = s.store.flush?.();
131
140
  if (persistence) await persistence;
@@ -159,11 +168,27 @@ const failedCommands = (s) => {
159
168
  }
160
169
  return s.failedList;
161
170
  };
171
+ const MAX_FAILED_RETAINED = 256;
172
+ const boundFailed = (failed) => failed.length > 256 ? [...failed].sort((a, b) => a.ts - b.ts).slice(-256) : failed;
173
+ const boundRecord = (record) => {
174
+ let lanes = record.lanes;
175
+ for (const [protocol, lane] of Object.entries(lanes)) if (lane.failed !== void 0 && lane.failed.length > 256) lanes = {
176
+ ...lanes,
177
+ [protocol]: {
178
+ ...lane,
179
+ failed: boundFailed(lane.failed)
180
+ }
181
+ };
182
+ return lanes === record.lanes ? record : {
183
+ ...record,
184
+ lanes
185
+ };
186
+ };
162
187
  const retainFailedInner = (s, entry, failure) => {
163
188
  const lane = laneOf(s.record, entry.command.protocol);
164
189
  setRecord(s, withLane(s.record, entry.command.protocol, {
165
190
  ...lane,
166
- failed: [...lane.failed ?? [], {
191
+ failed: boundFailed([...lane.failed ?? [], {
167
192
  protocol: entry.command.protocol,
168
193
  ...entry.command.produced !== void 0 && { produced: entry.command.produced },
169
194
  command: entry.command.method,
@@ -172,7 +197,7 @@ const retainFailedInner = (s, entry, failure) => {
172
197
  failure,
173
198
  ts: entry.inv.ts,
174
199
  seq: entry.seq
175
- }]
200
+ }])
176
201
  }));
177
202
  s.notify();
178
203
  };
@@ -263,7 +288,7 @@ const rebaseRecord = (s, adopt) => {
263
288
  ...lanes,
264
289
  [protocol]: {
265
290
  ...current,
266
- failed: [...current.failed ?? [], ...failed]
291
+ failed: boundFailed([...current.failed ?? [], ...failed])
267
292
  }
268
293
  };
269
294
  }
@@ -282,7 +307,7 @@ const resetRecord = (s, clientId) => rebaseRecord(s, () => setRecord(s, {
282
307
  }));
283
308
  const adoptReset = (s, stored) => {
284
309
  rebaseRecord(s, () => {
285
- s.record = stored;
310
+ s.record = boundRecord(stored);
286
311
  });
287
312
  if (s.attach === "attached") s.transport?.connection.reconnect();
288
313
  };
@@ -475,6 +500,13 @@ const selectionOf = (syn) => {
475
500
  };
476
501
  };
477
502
  const handleHello = (s, { ops, syn }) => {
503
+ if (s.attach === "attached") {
504
+ s.ephemeralSeqs = {};
505
+ detachEphemeralLanes(s);
506
+ }
507
+ for (const [protocol, value] of s.contexts) if (s.attachContexts.get(protocol) === value) s.dirtyContexts.delete(protocol);
508
+ else s.dirtyContexts.add(protocol);
509
+ s.attachContexts.clear();
478
510
  s.selection = selectionOf(syn);
479
511
  s.documents = {
480
512
  mode: null,
@@ -512,7 +544,7 @@ const handleHello = (s, { ops, syn }) => {
512
544
  const handleOps = (s, { ops, cmds }) => {
513
545
  ingestOps(s, ops);
514
546
  for (const cmd of cmds) handleCmd(s, cmd);
515
- syncBacklog(s);
547
+ if (cmds.length > 0) syncBacklog(s);
516
548
  publishIngested(s);
517
549
  };
518
550
  const handleRefusal = (s, syn) => {
@@ -574,15 +606,19 @@ const buildLanes = (s, cmdGate = "all") => {
574
606
  last[protocol] = entry.seq;
575
607
  entries.push(entry);
576
608
  }
577
- if (entries.length === 0) return null;
609
+ const ctx = [...s.dirtyContexts].map((protocol) => s.contexts.get(protocol));
610
+ if (entries.length === 0 && ctx.length === 0) return null;
578
611
  return {
579
- frame: { cmd: entries.map((p) => {
580
- const { produced: _produced, ...command } = p.command;
581
- return {
582
- ...command,
583
- seq: p.seq
584
- };
585
- }) },
612
+ frame: {
613
+ ...ctx.length > 0 && { ctx },
614
+ ...entries.length > 0 && { cmd: entries.map((p) => {
615
+ const { produced: _produced, ...command } = p.command;
616
+ return {
617
+ ...command,
618
+ seq: p.seq
619
+ };
620
+ }) }
621
+ },
586
622
  entries
587
623
  };
588
624
  };
@@ -590,6 +626,8 @@ const takeAttachLanes = (s) => {
590
626
  s.attachEntries = /* @__PURE__ */ new Set();
591
627
  const wanted = s.lanesOnAttach;
592
628
  s.lanesOnAttach = false;
629
+ for (const protocol of s.contexts.keys()) s.dirtyContexts.add(protocol);
630
+ s.attachContexts = new Map(s.contexts);
593
631
  const lanes = buildLanes(s, wanted && s.sessionKnown ? "persisted" : "none");
594
632
  if (!lanes) return null;
595
633
  s.attachEntries = new Set(lanes.entries);
@@ -620,6 +658,7 @@ const pump = async (s) => {
620
658
  if (!s.hydrated) await s.ready;
621
659
  const transport = s.transport;
622
660
  if (buildLanes(s) === null) return;
661
+ if (transport?.parked && s.pending.length === 0) return;
623
662
  if (s.attach !== "attached" || !transport) {
624
663
  if (transport?.connection.status === "standby" && transport.connection.reason === "idle" || transport?.parked && s.pending.length > 0) transport.connection.reconnect();
625
664
  await waitForWake(s);
@@ -641,15 +680,24 @@ const pump = async (s) => {
641
680
  let result;
642
681
  try {
643
682
  const send = () => transport.send(lanes.frame);
644
- result = await (s.shared && !s.deactivated ? s.store.lock(send) : send());
683
+ result = await (s.shared && !s.deactivated && lanes.entries.length > 0 ? s.store.lock(send) : send());
645
684
  } catch {
646
685
  if (!s.running || isFinished(s)) return;
686
+ if (lanes.frame.ctx?.length) {
687
+ s.lanesOnAttach = true;
688
+ transport.connection.reconnect();
689
+ await waitForWake(s);
690
+ continue;
691
+ }
647
692
  s.frameFailures += 1;
648
693
  await sleepUntilWake(s, backoff(s.frameFailures));
649
694
  continue;
650
695
  }
651
696
  if (!s.running) return;
652
697
  s.frameFailures = 0;
698
+ if (epoch === s.attachEpoch && (result.type === "ok" || result.type === "refusal")) {
699
+ for (const entry of lanes.frame.ctx ?? []) if (s.contexts.get(entry.protocol) === entry) s.dirtyContexts.delete(entry.protocol);
700
+ }
653
701
  if (result.type === "ok") {
654
702
  if (epoch === s.attachEpoch) {
655
703
  for (const entry of lanes.entries) if (s.pending.includes(entry)) {
@@ -783,7 +831,7 @@ const adoptStored = (s) => {
783
831
  const known = new Set(laneEntries(s, protocol).map((p) => p.seq));
784
832
  for (const { seq, ...command } of lane.commands) {
785
833
  if (known.has(seq)) continue;
786
- const params = command.meta === void 0 ? command.params : reviveDocumentValue([...command.params], command.meta);
834
+ const params = command.meta === void 0 ? command.params : reviveDocumentValue(structuredClone(command.params), command.meta);
787
835
  const entry = {
788
836
  seq,
789
837
  command: {
@@ -836,7 +884,7 @@ const sendCommand = (s, method, params, protocol = s.options.protocol ?? "defaul
836
884
  enqueue(s, {
837
885
  protocol,
838
886
  produced: {
839
- wire: s.options.wireVersion ?? { version: "2026-09-13" },
887
+ wire: s.options.wireVersion ?? { version: "2026-09-30" },
840
888
  protocols: protocolOffers(s.options) === "*" && s.selection ? s.selection.protocols : protocolOffers(s.options)
841
889
  },
842
890
  method,
@@ -881,7 +929,7 @@ const retryFailed = (s, entry) => {
881
929
  });
882
930
  const found = findFailed(s, entry);
883
931
  if (found === void 0) throw unknownFailed(entry);
884
- const params = found.meta === void 0 ? found.params : reviveDocumentValue([...found.params], found.meta);
932
+ const params = found.meta === void 0 ? found.params : reviveDocumentValue(structuredClone(found.params), found.meta);
885
933
  const result = new Promise((resolve, reject) => {
886
934
  const retry = () => {
887
935
  if (!s.running) {
@@ -968,7 +1016,7 @@ const loadSessionRecord = (store, options) => {
968
1016
  let stored = store.load();
969
1017
  if (stored !== null) validateLanes(stored);
970
1018
  const current = {
971
- wire: options.wireVersion ?? { version: "2026-09-13" },
1019
+ wire: options.wireVersion ?? { version: "2026-09-30" },
972
1020
  protocols: protocolOffers(options)
973
1021
  };
974
1022
  const compatible = (record) => Object.entries(record.lanes).every(([protocol, lane]) => lane.commands.every((command) => storedCommandCompatible(protocol, command, current)));
@@ -981,13 +1029,20 @@ const loadSessionRecord = (store, options) => {
981
1029
  store.save(stored);
982
1030
  }
983
1031
  }
984
- const record = stored ?? {
1032
+ const record = stored === null ? {
985
1033
  clientId: randomId(),
986
1034
  lanes: {}
987
- };
988
- if (stored === null) store.save(record);
1035
+ } : boundRecord(stored);
1036
+ if (record !== stored) store.save(record);
989
1037
  return record;
990
1038
  };
1039
+ const setContext = (s, protocol, value) => {
1040
+ if (s.selection ? !s.selection.protocols.some((p) => p.name === protocol) : !offered(s, protocol)) throw new Error(`statewire: protocol "${protocol}" is not negotiated`);
1041
+ s.contexts.set(protocol, encodeContext(protocol, value));
1042
+ s.dirtyContexts.add(protocol);
1043
+ wake(s);
1044
+ pump(s);
1045
+ };
991
1046
  const useStatewireSession = (options) => {
992
1047
  const [defaultStore] = useState(createMemorySessionStore);
993
1048
  const mounted = useContext(SessionStorageContext);
@@ -1002,6 +1057,10 @@ const useStatewireSession = (options) => {
1002
1057
  };
1003
1058
  const session = {
1004
1059
  options,
1060
+ configuredContext: options.context,
1061
+ contexts: new Map(Object.entries(options.context ?? {}).map(([protocol, value]) => [protocol, encodeContext(protocol, value)])),
1062
+ dirtyContexts: new Set(Object.keys(options.context ?? {})),
1063
+ attachContexts: /* @__PURE__ */ new Map(),
1005
1064
  selection: void 0,
1006
1065
  record,
1007
1066
  ctx: {
@@ -1027,6 +1086,7 @@ const useStatewireSession = (options) => {
1027
1086
  documentStore,
1028
1087
  shared: store.lock !== void 0 ? {
1029
1088
  txq: Promise.resolve(),
1089
+ syncQueued: false,
1030
1090
  parked: [],
1031
1091
  unsubscribe: null
1032
1092
  } : false,
@@ -1095,6 +1155,11 @@ const useStatewireSession = (options) => {
1095
1155
  return session;
1096
1156
  });
1097
1157
  if (store.lock !== void 0 !== (s.shared !== false)) throw new Error("statewire: session storage changed sharedness across renders — lock/subscribe must be present from the first render or never");
1158
+ useEffect(() => {
1159
+ if (s.configuredContext === options.context) return;
1160
+ s.configuredContext = options.context;
1161
+ for (const [protocol_0, value_0] of Object.entries(options.context ?? {})) setContext(s, protocol_0, value_0);
1162
+ }, [s, options.context]);
1098
1163
  s.options = options;
1099
1164
  s.store = store;
1100
1165
  s.documentStore = documentStore;
@@ -1118,6 +1183,6 @@ const useStatewireSession = (options) => {
1118
1183
  return s;
1119
1184
  };
1120
1185
  //#endregion
1121
- export { discardFailed, documentSource, failedCommands, invokeProtocol, retryFailed, setTransport, useStatewireSession };
1186
+ export { MAX_FAILED_RETAINED, discardFailed, documentSource, failedCommands, invokeProtocol, retryFailed, setContext, setTransport, useStatewireSession };
1122
1187
 
1123
1188
  //# sourceMappingURL=session.js.map