statewire 0.15.0 → 0.17.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 (90) hide show
  1. package/README.md +14 -6
  2. package/dist/client/connection.d.ts +5 -1
  3. package/dist/client/connection.d.ts.map +1 -1
  4. package/dist/client/connection.js +19 -4
  5. package/dist/client/connection.js.map +1 -1
  6. package/dist/client/core.d.ts.map +1 -1
  7. package/dist/client/core.js +2 -0
  8. package/dist/client/core.js.map +1 -1
  9. package/dist/client/session.d.ts +15 -4
  10. package/dist/client/session.d.ts.map +1 -1
  11. package/dist/client/session.js +233 -52
  12. package/dist/client/session.js.map +1 -1
  13. package/dist/client/sessionStore.d.ts +5 -1
  14. package/dist/client/sessionStore.d.ts.map +1 -1
  15. package/dist/client/sessionStore.js +3 -1
  16. package/dist/client/sessionStore.js.map +1 -1
  17. package/dist/host/host.d.ts +1 -0
  18. package/dist/host/host.d.ts.map +1 -1
  19. package/dist/host/host.js.map +1 -1
  20. package/dist/host/http.d.ts.map +1 -1
  21. package/dist/host/http.js +27 -9
  22. package/dist/host/http.js.map +1 -1
  23. package/dist/host/socket.d.ts +10 -3
  24. package/dist/host/socket.d.ts.map +1 -1
  25. package/dist/host/socket.js +103 -39
  26. package/dist/host/socket.js.map +1 -1
  27. package/dist/host-internal.d.ts +9 -1
  28. package/dist/host-internal.d.ts.map +1 -1
  29. package/dist/host-internal.js +1 -1
  30. package/dist/index.d.ts +5 -3
  31. package/dist/index.js +3 -3
  32. package/dist/statewire.d.ts +33 -2
  33. package/dist/statewire.d.ts.map +1 -1
  34. package/dist/statewire.js +34 -3
  35. package/dist/statewire.js.map +1 -1
  36. package/dist/transports/fixtures.js +1 -0
  37. package/dist/transports/fixtures.js.map +1 -1
  38. package/dist/transports/remote.d.ts.map +1 -1
  39. package/dist/transports/remote.js +15 -8
  40. package/dist/transports/remote.js.map +1 -1
  41. package/dist/transports/sse.d.ts +2 -2
  42. package/dist/transports/sse.d.ts.map +1 -1
  43. package/dist/transports/sse.js +11 -11
  44. package/dist/transports/sse.js.map +1 -1
  45. package/dist/transports/testing.d.ts +1 -1
  46. package/dist/transports/testing.d.ts.map +1 -1
  47. package/dist/transports/testing.js +14 -6
  48. package/dist/transports/testing.js.map +1 -1
  49. package/dist/transports/ws.d.ts +2 -2
  50. package/dist/transports/ws.d.ts.map +1 -1
  51. package/dist/transports/ws.js +5 -5
  52. package/dist/transports/ws.js.map +1 -1
  53. package/dist/types.d.ts +7 -0
  54. package/dist/types.d.ts.map +1 -1
  55. package/dist/types.js.map +1 -1
  56. package/dist/util/randomId.js +1 -1
  57. package/dist/util/randomId.js.map +1 -1
  58. package/dist/wire/documents.d.ts +25 -2
  59. package/dist/wire/documents.d.ts.map +1 -1
  60. package/dist/wire/documents.js +27 -7
  61. package/dist/wire/documents.js.map +1 -1
  62. package/dist/wire/protocol.d.ts +55 -0
  63. package/dist/wire/protocol.d.ts.map +1 -0
  64. package/dist/wire/protocol.js +221 -0
  65. package/dist/wire/protocol.js.map +1 -0
  66. package/dist/wire/version.d.ts +1 -3
  67. package/dist/wire/version.d.ts.map +1 -1
  68. package/dist/wire/version.js +1 -3
  69. package/dist/wire/version.js.map +1 -1
  70. package/package.json +1 -1
  71. package/src/client/connection.ts +43 -13
  72. package/src/client/core.ts +6 -0
  73. package/src/client/session.ts +339 -42
  74. package/src/client/sessionStore.ts +8 -0
  75. package/src/host/host.ts +1 -0
  76. package/src/host/http.ts +36 -7
  77. package/src/host/socket.ts +141 -50
  78. package/src/host-internal.ts +12 -1
  79. package/src/index.ts +5 -2
  80. package/src/statewire.ts +109 -6
  81. package/src/transports/fixtures.ts +1 -1
  82. package/src/transports/remote.ts +16 -7
  83. package/src/transports/sse.ts +28 -18
  84. package/src/transports/testing.ts +12 -4
  85. package/src/transports/ws.ts +8 -8
  86. package/src/types.ts +11 -0
  87. package/src/util/randomId.ts +3 -3
  88. package/src/wire/documents.ts +126 -8
  89. package/src/wire/protocol.ts +327 -0
  90. package/src/wire/version.ts +0 -2
@@ -1,19 +1,19 @@
1
+ import { inRange, protocolOffers } from "../wire/protocol.js";
2
+ import { encodeParams, encodeValue, reviveResponse, reviveValue } from "../wire/meta.js";
3
+ import { applyDocumentOps, findDocument, mainDocument, resolveDocumentKey } from "../wire/documents.js";
1
4
  import { createMethodProxy } from "../util/methodProxy.js";
2
5
  import { StatewireSendError } from "../types.js";
3
6
  import { bindCommandsHost } from "../host/host.js";
4
- import { encodeParams, encodeValue, reviveResponse, reviveValue } from "../wire/meta.js";
5
7
  import "../wire/limits.js";
6
- import { SessionStorageContext, createMemorySessionStore } from "./sessionStore.js";
8
+ import { DocumentStorageContext, SessionStorageContext, createMemorySessionStore } from "./sessionStore.js";
7
9
  import { backoff } from "./timing.js";
8
10
  import { TERMINAL } from "./connection.js";
9
- import { applyDocumentOps } from "../wire/documents.js";
10
11
  import { randomId } from "../util/randomId.js";
11
12
  import { useContext, useEffect, useState } from "@assistant-ui/tap/react-shim";
12
13
  //#region src/client/session.ts
13
14
  const protocolError = (message) => {
14
15
  throw new Error(`statewire: protocol error: ${message}`);
15
16
  };
16
- const newClientId = () => `c-${randomId()}`;
17
17
  const rejectedSend = (failure) => {
18
18
  const result = Promise.reject(new StatewireSendError(failure));
19
19
  result.catch(() => {});
@@ -80,7 +80,21 @@ const transact = (s, fn) => {
80
80
  const run = shared.txq.then(() => s.store.lock(async () => {
81
81
  const stored = s.store.load();
82
82
  if (stored !== null && stored.clientId !== s.record.clientId) adoptReset(s, stored);
83
- else if (stored !== null) s.record = stored;
83
+ else if (stored !== null) {
84
+ s.record = stored;
85
+ if (stored.context !== void 0) {
86
+ const posted = new Set(s.ctxPending.filter((p) => p.posted).map((p) => p.seq));
87
+ s.ctxOps = stored.context.ops;
88
+ s.contextDocuments = applyDocumentOps({
89
+ mode: "mounted",
90
+ documents: /* @__PURE__ */ new Map()
91
+ }, stored.context.ops);
92
+ s.ctxPending = stored.context.pending.map((p) => ({
93
+ ...p,
94
+ posted: posted.has(p.seq)
95
+ }));
96
+ }
97
+ }
84
98
  fn();
85
99
  }));
86
100
  shared.txq = run.then(() => {}, () => {});
@@ -104,6 +118,8 @@ const retainFailedInner = (s, entry, failure) => {
104
118
  setRecord(s, {
105
119
  ...s.record,
106
120
  failed: [...s.record.failed ?? [], {
121
+ protocol: entry.command.protocol,
122
+ ...entry.command.produced !== void 0 && { produced: entry.command.produced },
107
123
  command: entry.command.method,
108
124
  params: entry.command.params,
109
125
  ...entry.command.meta !== void 0 && { meta: entry.command.meta },
@@ -225,7 +241,7 @@ const adoptReset = (s, stored) => rebaseRecord(s, () => {
225
241
  const ephemeralDetach = (s) => {
226
242
  s.ephemeral = false;
227
243
  s.sessionKnown = false;
228
- resetRecord(s, newClientId());
244
+ resetRecord(s, randomId());
229
245
  };
230
246
  const rejectedFailure = (cmd) => {
231
247
  return {
@@ -295,8 +311,8 @@ const handleCmd = (s, cmd) => {
295
311
  if (entry) cover(s, entry);
296
312
  return;
297
313
  }
298
- parkVerdict(s, cmd);
299
314
  if (!entry) return;
315
+ parkVerdict(s, cmd);
300
316
  cover(s, entry);
301
317
  settleEntry(s, entry, outcomeOf(cmd));
302
318
  };
@@ -325,17 +341,36 @@ const handleCmds = (s, cmds) => {
325
341
  wake(s);
326
342
  };
327
343
  const ingestOps = (s, ops) => {
328
- const applied = applyDocumentOps(s.documents, ops);
344
+ const applied = applyDocumentOps(s.documents, ops, s.selection?.protocols);
329
345
  s.documents = applied;
330
- s.canonical = applied.documents.get(0)?.value;
331
- s.changed.push(...applied.changedPaths);
346
+ const next = s.selection === void 0 ? applied.documents.get(0)?.value : s.options.applicationProtocols === "*" ? Object.fromEntries([...applied.documents].map(([id, d]) => [`${d.protocol}:${d.key === null ? id : JSON.stringify(d.key)}`, d.value])) : mainDocument(applied, s.options.protocol ?? "default")?.value;
347
+ if (next !== s.canonical) s.changed.push(...applied.changedPaths.length ? applied.changedPaths : [[]]);
348
+ s.canonical = next;
349
+ };
350
+ const cacheDocuments = (s) => {
351
+ if (s.documentStore && s.selection) for (const document of s.documents.documents.values()) {
352
+ if (!document.protocol || !document.key) continue;
353
+ const key = resolveDocumentKey(document.key, s.options.keyPrefixes?.[document.protocol]);
354
+ const version = s.selection.protocols.find((p) => p.name === document.protocol)?.version;
355
+ if (key !== void 0 && version !== void 0) s.documentStore.save([
356
+ s.documentStore.namespace ?? "",
357
+ document.protocol,
358
+ ...key
359
+ ], {
360
+ version,
361
+ value: document.value
362
+ });
363
+ }
332
364
  };
333
365
  const publishIngested = (s) => {
334
366
  publish(s);
367
+ cacheDocuments(s);
368
+ for (const listener of s.documentListeners) listener();
335
369
  wake(s);
336
370
  };
337
371
  const snapCtxClock = (s, ctxWm) => {
338
372
  if (!(s.ctxPending.every((c, i) => c.seq === ctxWm + 1 + i) && s.record.ctxNextSeq === ctxWm + s.ctxPending.length + 1)) {
373
+ if (ctxWm > 0) throw new Error("statewire: context history is missing; use a new client id");
339
374
  setRecord(s, {
340
375
  ...s.record,
341
376
  ctxNextSeq: ctxWm + 1
@@ -351,17 +386,21 @@ const sessionLostInner = (s, seqWm, ctxWm) => {
351
386
  setRecord(s, {
352
387
  clientId: s.record.clientId,
353
388
  nextSeq: seqWm + 1,
354
- ctxNextSeq: ctxWm + 1,
389
+ ctxNextSeq: s.record.ctxNextSeq,
355
390
  commands: [],
356
391
  ...s.record.failed !== void 0 && { failed: s.record.failed }
357
392
  });
358
- restateContext(s);
393
+ snapCtxClock(s, ctxWm);
359
394
  if (held.length > 0 && s.onSessionLost?.(held.map((p) => p.inv)) === "requeue") for (const old of held) enqueueInner(s, old.command, old.inv.params, () => {}, () => {});
360
395
  publish(s);
361
396
  wake(s);
362
397
  };
363
398
  const handleHello = (s, { ops, syn }) => {
364
399
  if (syn.record !== void 0 && syn.record !== "ephemeral") protocolError(`unknown record: ${String(syn.record)}`);
400
+ s.selection = syn.version === void 0 || syn.protocols === void 0 ? void 0 : {
401
+ version: syn.version,
402
+ protocols: syn.protocols
403
+ };
365
404
  s.documents = {
366
405
  mode: null,
367
406
  documents: /* @__PURE__ */ new Map()
@@ -383,6 +422,7 @@ const handleHello = (s, { ops, syn }) => {
383
422
  s.ctxPending = s.ctxPending.filter((c) => c.seq > ctxWatermark);
384
423
  for (const c of s.ctxPending) c.posted = attachCtx.has(c.seq);
385
424
  snapCtxClock(s, ctxWatermark);
425
+ if (s.ctxOps !== void 0) persistContext(s);
386
426
  syncBacklogInner(s);
387
427
  s.sessionKnown = true;
388
428
  };
@@ -412,6 +452,7 @@ const handleRefusal = (s, syn) => {
412
452
  if (typeof ctxSeq === "number") {
413
453
  s.ctxPending = s.ctxPending.filter((c) => c.seq > ctxSeq);
414
454
  for (const c of s.ctxPending) c.posted = false;
455
+ if (s.ctxOps !== void 0) persistContext(s);
415
456
  }
416
457
  if (typeof syn.retry === "number") {
417
458
  s.resumeAt = Date.now() + syn.retry;
@@ -471,10 +512,13 @@ const buildLanes = (s) => {
471
512
  ops: c.ops,
472
513
  seq: c.seq
473
514
  })) },
474
- ...cmdEntries.length > 0 && { cmd: cmdEntries.map((p) => ({
475
- ...p.command,
476
- seq: p.seq
477
- })) }
515
+ ...cmdEntries.length > 0 && { cmd: cmdEntries.map((p) => {
516
+ const { produced: _produced, ...command } = p.command;
517
+ return {
518
+ ...command,
519
+ seq: p.seq
520
+ };
521
+ }) }
478
522
  },
479
523
  cmdEntries,
480
524
  ctxEntries
@@ -593,6 +637,7 @@ const enqueueInner = (s, command, params, resolve, reject) => {
593
637
  seq,
594
638
  command,
595
639
  inv: {
640
+ protocol: command.protocol,
596
641
  method: command.method,
597
642
  params,
598
643
  ts: Date.now(),
@@ -646,11 +691,14 @@ const adoptStored = (s) => {
646
691
  const stored = s.record.commands.filter((c) => !known.has(c.seq));
647
692
  if (stored.length === 0) return;
648
693
  for (const { seq, ...command } of stored) {
694
+ const offers = protocolOffers(s.options);
695
+ if (offers !== "*" && !offers.some((offer) => offer.name === command.protocol)) continue;
649
696
  const params = command.meta === void 0 ? command.params : reviveValue([...command.params], command.meta);
650
697
  const entry = {
651
698
  seq,
652
699
  command,
653
700
  inv: {
701
+ protocol: command.protocol,
654
702
  method: command.method,
655
703
  params,
656
704
  ts: Date.now(),
@@ -668,7 +716,7 @@ const adoptStored = (s) => {
668
716
  }
669
717
  s.pending.sort((a, b) => a.seq - b.seq);
670
718
  };
671
- const sendCommand = (s, method, params) => {
719
+ const sendCommand = (s, method, params, protocol = s.options.protocol ?? "default") => {
672
720
  if (!s.running) return rejectedSend({
673
721
  fate: "aborted",
674
722
  cause: "unmount"
@@ -680,6 +728,11 @@ const sendCommand = (s, method, params) => {
680
728
  const result = new Promise((resolve, reject) => {
681
729
  const { params: encoded, meta } = encodeParams(params);
682
730
  enqueue(s, {
731
+ protocol,
732
+ produced: {
733
+ wire: s.options.wireVersion ?? { version: "2026-09-07" },
734
+ protocols: protocolOffers(s.options) === "*" && s.selection ? s.selection.protocols : protocolOffers(s.options)
735
+ },
683
736
  method,
684
737
  params: encoded,
685
738
  ...meta !== void 0 && { meta }
@@ -746,6 +799,8 @@ const retryFailed = (s, entry) => {
746
799
  }
747
800
  takeFailed(s, current);
748
801
  enqueueInner(s, {
802
+ protocol: current.protocol,
803
+ ...current.produced !== void 0 && { produced: current.produced },
749
804
  method: current.command,
750
805
  params: [...current.params],
751
806
  ...current.meta !== void 0 && { meta: current.meta }
@@ -765,59 +820,167 @@ const retryFailed = (s, entry) => {
765
820
  result.catch(() => {});
766
821
  return result;
767
822
  };
768
- const setContext = (s, value) => {
769
- if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("statewire: context must be a plain object");
770
- if (!s.running || isFinished(s)) return;
771
- const { value: encoded, meta } = encodeValue(value);
772
- const ops = [{
773
- op: "replace",
774
- path: [],
775
- value: encoded,
776
- ...meta !== void 0 && { meta }
777
- }];
823
+ const contextSnapshot = (documents) => [...documents.documents].map(([doc, d]) => {
824
+ const encoded = encodeValue(d.value);
825
+ return {
826
+ op: "mount",
827
+ ...doc === 0 ? {} : { doc },
828
+ protocol: d.protocol,
829
+ ...d.main ? { main: true } : {},
830
+ ...d.key === null ? {} : { key: d.key },
831
+ value: encoded.value,
832
+ ...encoded.meta === void 0 ? {} : { meta: encoded.meta }
833
+ };
834
+ });
835
+ const persistContext = (s) => {
836
+ const ops = contextSnapshot(s.contextDocuments);
778
837
  s.ctxOps = ops;
779
- const mint = () => {
838
+ setRecord(s, {
839
+ ...s.record,
840
+ context: {
841
+ ops,
842
+ pending: s.ctxPending.map(({ seq, ops }) => ({
843
+ seq,
844
+ ops
845
+ }))
846
+ }
847
+ });
848
+ };
849
+ const changeContext = (s, build) => {
850
+ if (!s.running || isFinished(s)) return;
851
+ const submit = () => {
852
+ if (!s.running || isFinished(s)) return;
853
+ const previous = s.record.context === void 0 ? s.contextDocuments : applyDocumentOps({
854
+ mode: "mounted",
855
+ documents: /* @__PURE__ */ new Map()
856
+ }, s.record.context.ops);
857
+ const operations = build(previous);
858
+ if (!operations.length) throw new Error("statewire: context operations must not be empty");
859
+ const offers = protocolOffers(s.options);
860
+ const protocols = s.selection?.protocols ?? (offers === "*" ? void 0 : offers);
861
+ s.contextDocuments = applyDocumentOps(previous, operations, protocols, true);
780
862
  const seq = s.record.ctxNextSeq;
781
- setRecord(s, {
782
- ...s.record,
783
- ctxNextSeq: seq + 1
784
- });
785
- s.ctxPending.push({
863
+ s.ctxPending = [...s.ctxPending, {
786
864
  seq,
787
- ops,
865
+ ops: operations.map((op) => {
866
+ const value = op;
867
+ if (value["doc"] !== 0) return value;
868
+ const { doc: _doc, ...rest } = value;
869
+ return rest;
870
+ }),
788
871
  posted: false
789
- });
872
+ }];
873
+ s.record = {
874
+ ...s.record,
875
+ ctxNextSeq: seq + 1
876
+ };
877
+ persistContext(s);
790
878
  };
791
879
  if (!s.shared) {
792
- mint();
880
+ submit();
793
881
  wake(s);
794
882
  pump(s);
795
- return;
796
- }
797
- transact(s, () => {
798
- if (!s.running || isFinished(s)) return;
799
- mint();
800
- }).then(() => {
883
+ } else transact(s, submit).then(() => {
801
884
  wake(s);
802
885
  pump(s);
803
886
  });
804
887
  };
888
+ const updateContext = (s, operations) => changeContext(s, () => operations);
889
+ const setContext = (s, value, protocol = s.options.protocol ?? "default") => {
890
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("statewire: context must be a plain object");
891
+ const encoded = encodeValue(value);
892
+ changeContext(s, (documents) => {
893
+ const found = [...documents.documents].find(([, d]) => d.protocol === protocol && d.main);
894
+ const doc = found?.[0] ?? Math.max(-1, ...documents.documents.keys()) + 1;
895
+ return [{
896
+ ...found === void 0 ? {
897
+ op: "mount",
898
+ protocol,
899
+ main: true
900
+ } : {
901
+ op: "replace",
902
+ path: []
903
+ },
904
+ ...doc === 0 ? {} : { doc },
905
+ value: encoded.value,
906
+ ...encoded.meta === void 0 ? {} : { meta: encoded.meta }
907
+ }];
908
+ });
909
+ };
910
+ const documentSource = (s) => ({
911
+ get: (protocol, key, prefix = s.options.keyPrefixes?.[protocol]) => {
912
+ const mounted = findDocument(s.documents, protocol, key, prefix);
913
+ if (mounted !== void 0) return mounted.value;
914
+ if (!s.documentStore || key === void 0) return void 0;
915
+ const absolute = resolveDocumentKey(key, prefix);
916
+ if (absolute === void 0) return void 0;
917
+ const cached = s.documentStore.load([
918
+ s.documentStore.namespace ?? "",
919
+ protocol,
920
+ ...absolute
921
+ ]);
922
+ if (cached === void 0) return void 0;
923
+ const offers = protocolOffers(s.options);
924
+ const offer = offers === "*" ? s.selection?.protocols.find((p) => p.name === protocol) : offers.find((p) => p.name === protocol);
925
+ return offer !== void 0 && inRange(cached.version, offer) ? cached.value : void 0;
926
+ },
927
+ list: () => s.documents.documents,
928
+ subscribe: (listener) => {
929
+ s.documentListeners.add(listener);
930
+ return () => {
931
+ s.documentListeners.delete(listener);
932
+ };
933
+ }
934
+ });
935
+ const invokeProtocol = (s, protocol, method, params) => sendCommand(s, method, params, protocol);
936
+ const storedCommandCompatible = (command, current) => {
937
+ if (!command.produced || !command.protocol || !inRange(command.produced.wire.version, current.wire)) return false;
938
+ const previous = command.produced.protocols;
939
+ if (current.protocols === "*") return true;
940
+ if (previous === "*") return false;
941
+ const produced = previous.find((p) => p.name === command.protocol);
942
+ const offer = current.protocols.find((p) => p.name === command.protocol);
943
+ return produced !== void 0 && offer !== void 0 && inRange(produced.version, offer);
944
+ };
805
945
  const useStatewireSession = (options) => {
806
946
  const [defaultStore] = useState(createMemorySessionStore);
807
947
  const mounted = useContext(SessionStorageContext);
948
+ const documentStore = useContext(DocumentStorageContext);
808
949
  if (options.sessionStore !== void 0 && mounted !== void 0) throw new Error("statewire: session storage is set twice — drop the transport's sessionStore or the client's storage.session");
809
950
  const store = options.sessionStore ?? mounted ?? defaultStore;
810
951
  if (store.lock === void 0 !== (store.subscribe === void 0)) throw new Error("statewire: session storage must provide lock and subscribe together");
811
952
  const [s] = useState(() => {
812
- const stored = store.load();
953
+ let stored = store.load();
954
+ if (stored !== null && !Array.isArray(stored.commands)) throw new Error("statewire: session storage loaded a record without a commands array");
955
+ const current = {
956
+ wire: options.wireVersion ?? { version: "2026-09-07" },
957
+ protocols: protocolOffers(options)
958
+ };
959
+ if (stored !== null && !stored.commands.every((command_0) => storedCommandCompatible(command_0, current))) {
960
+ if (!store.migrate) throw new Error("statewire: stored commands require a storage migration");
961
+ stored = store.migrate(stored, current);
962
+ if (stored !== null && !stored.commands.every((command) => storedCommandCompatible(command, current))) throw new Error("statewire: migrated commands are incompatible");
963
+ if (stored !== null) store.save(stored);
964
+ }
813
965
  if (stored !== null && !Array.isArray(stored.commands)) throw new Error("statewire: session storage loaded a record without a commands array");
966
+ const record = stored ?? {
967
+ clientId: randomId(),
968
+ nextSeq: 1,
969
+ ctxNextSeq: 1,
970
+ commands: []
971
+ };
972
+ if (stored === null) store.save(record);
814
973
  const session = {
815
- record: stored ?? {
816
- clientId: newClientId(),
817
- nextSeq: 1,
818
- ctxNextSeq: 1,
819
- commands: []
820
- },
974
+ options,
975
+ selection: void 0,
976
+ contextDocuments: stored?.context === void 0 ? {
977
+ mode: "mounted",
978
+ documents: /* @__PURE__ */ new Map()
979
+ } : applyDocumentOps({
980
+ mode: "mounted",
981
+ documents: /* @__PURE__ */ new Map()
982
+ }, stored.context.ops),
983
+ record,
821
984
  ctx: {
822
985
  get clientId() {
823
986
  return session.record.clientId;
@@ -833,6 +996,7 @@ const useStatewireSession = (options) => {
833
996
  },
834
997
  recordPersisted: stored !== null,
835
998
  store,
999
+ documentStore,
836
1000
  shared: store.lock !== void 0 ? {
837
1001
  txq: Promise.resolve(),
838
1002
  parked: [],
@@ -846,13 +1010,17 @@ const useStatewireSession = (options) => {
846
1010
  idle: false,
847
1011
  onSessionLost: void 0,
848
1012
  onDone: void 0,
849
- ctxOps: void 0,
850
- ctxPending: [],
1013
+ ctxOps: stored?.context?.ops,
1014
+ ctxPending: stored?.context?.pending.map((p) => ({
1015
+ ...p,
1016
+ posted: false
1017
+ })) ?? [],
851
1018
  lanesOnAttach: false,
852
1019
  attachLaneSeqs: [/* @__PURE__ */ new Set(), /* @__PURE__ */ new Set()],
853
1020
  frameFailures: 0,
854
1021
  pending: [],
855
1022
  canonical: void 0,
1023
+ documentListeners: /* @__PURE__ */ new Set(),
856
1024
  documents: {
857
1025
  mode: null,
858
1026
  documents: /* @__PURE__ */ new Map()
@@ -886,9 +1054,22 @@ const useStatewireSession = (options) => {
886
1054
  return session;
887
1055
  });
888
1056
  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");
1057
+ s.options = options;
889
1058
  s.store = store;
1059
+ s.documentStore = documentStore;
890
1060
  s.onSessionLost = options.onSessionLost;
891
1061
  s.onDone = options.onDone;
1062
+ const keyPrefixes = JSON.stringify(options.keyPrefixes);
1063
+ useEffect(() => {
1064
+ cacheDocuments(s);
1065
+ }, [
1066
+ s,
1067
+ documentStore,
1068
+ keyPrefixes
1069
+ ]);
1070
+ useEffect(() => documentStore?.subscribe?.(() => {
1071
+ for (const listener_1 of s.documentListeners) listener_1();
1072
+ }), [s, documentStore]);
892
1073
  useEffect(() => {
893
1074
  start(s);
894
1075
  return () => stop(s);
@@ -896,6 +1077,6 @@ const useStatewireSession = (options) => {
896
1077
  return s;
897
1078
  };
898
1079
  //#endregion
899
- export { discardFailed, failedCommands, retryFailed, setContext, setTransport, useStatewireSession };
1080
+ export { discardFailed, documentSource, failedCommands, invokeProtocol, retryFailed, setContext, setTransport, updateContext, useStatewireSession };
900
1081
 
901
1082
  //# sourceMappingURL=session.js.map