statewire 0.16.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 (83) hide show
  1. package/README.md +12 -4
  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 +232 -50
  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 +3 -1
  31. package/dist/statewire.d.ts +33 -2
  32. package/dist/statewire.d.ts.map +1 -1
  33. package/dist/statewire.js +34 -3
  34. package/dist/statewire.js.map +1 -1
  35. package/dist/transports/fixtures.js +1 -0
  36. package/dist/transports/fixtures.js.map +1 -1
  37. package/dist/transports/remote.d.ts.map +1 -1
  38. package/dist/transports/remote.js +15 -8
  39. package/dist/transports/remote.js.map +1 -1
  40. package/dist/transports/sse.d.ts.map +1 -1
  41. package/dist/transports/sse.js +8 -8
  42. package/dist/transports/sse.js.map +1 -1
  43. package/dist/transports/testing.d.ts +1 -1
  44. package/dist/transports/testing.d.ts.map +1 -1
  45. package/dist/transports/testing.js +12 -4
  46. package/dist/transports/testing.js.map +1 -1
  47. package/dist/transports/ws.js +2 -2
  48. package/dist/transports/ws.js.map +1 -1
  49. package/dist/types.d.ts +7 -0
  50. package/dist/types.d.ts.map +1 -1
  51. package/dist/types.js.map +1 -1
  52. package/dist/wire/documents.d.ts +25 -2
  53. package/dist/wire/documents.d.ts.map +1 -1
  54. package/dist/wire/documents.js +27 -7
  55. package/dist/wire/documents.js.map +1 -1
  56. package/dist/wire/protocol.d.ts +55 -0
  57. package/dist/wire/protocol.d.ts.map +1 -0
  58. package/dist/wire/protocol.js +221 -0
  59. package/dist/wire/protocol.js.map +1 -0
  60. package/dist/wire/version.d.ts +1 -3
  61. package/dist/wire/version.d.ts.map +1 -1
  62. package/dist/wire/version.js +1 -3
  63. package/dist/wire/version.js.map +1 -1
  64. package/package.json +1 -1
  65. package/src/client/connection.ts +43 -13
  66. package/src/client/core.ts +6 -0
  67. package/src/client/session.ts +338 -39
  68. package/src/client/sessionStore.ts +8 -0
  69. package/src/host/host.ts +1 -0
  70. package/src/host/http.ts +36 -7
  71. package/src/host/socket.ts +141 -50
  72. package/src/host-internal.ts +12 -1
  73. package/src/index.ts +3 -0
  74. package/src/statewire.ts +109 -6
  75. package/src/transports/fixtures.ts +1 -1
  76. package/src/transports/remote.ts +16 -7
  77. package/src/transports/sse.ts +18 -8
  78. package/src/transports/testing.ts +10 -2
  79. package/src/transports/ws.ts +1 -1
  80. package/src/types.ts +11 -0
  81. package/src/wire/documents.ts +126 -8
  82. package/src/wire/protocol.ts +327 -0
  83. package/src/wire/version.ts +0 -2
@@ -1,12 +1,13 @@
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
@@ -79,7 +80,21 @@ const transact = (s, fn) => {
79
80
  const run = shared.txq.then(() => s.store.lock(async () => {
80
81
  const stored = s.store.load();
81
82
  if (stored !== null && stored.clientId !== s.record.clientId) adoptReset(s, stored);
82
- 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
+ }
83
98
  fn();
84
99
  }));
85
100
  shared.txq = run.then(() => {}, () => {});
@@ -103,6 +118,8 @@ const retainFailedInner = (s, entry, failure) => {
103
118
  setRecord(s, {
104
119
  ...s.record,
105
120
  failed: [...s.record.failed ?? [], {
121
+ protocol: entry.command.protocol,
122
+ ...entry.command.produced !== void 0 && { produced: entry.command.produced },
106
123
  command: entry.command.method,
107
124
  params: entry.command.params,
108
125
  ...entry.command.meta !== void 0 && { meta: entry.command.meta },
@@ -294,8 +311,8 @@ const handleCmd = (s, cmd) => {
294
311
  if (entry) cover(s, entry);
295
312
  return;
296
313
  }
297
- parkVerdict(s, cmd);
298
314
  if (!entry) return;
315
+ parkVerdict(s, cmd);
299
316
  cover(s, entry);
300
317
  settleEntry(s, entry, outcomeOf(cmd));
301
318
  };
@@ -324,17 +341,36 @@ const handleCmds = (s, cmds) => {
324
341
  wake(s);
325
342
  };
326
343
  const ingestOps = (s, ops) => {
327
- const applied = applyDocumentOps(s.documents, ops);
344
+ const applied = applyDocumentOps(s.documents, ops, s.selection?.protocols);
328
345
  s.documents = applied;
329
- s.canonical = applied.documents.get(0)?.value;
330
- 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
+ }
331
364
  };
332
365
  const publishIngested = (s) => {
333
366
  publish(s);
367
+ cacheDocuments(s);
368
+ for (const listener of s.documentListeners) listener();
334
369
  wake(s);
335
370
  };
336
371
  const snapCtxClock = (s, ctxWm) => {
337
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");
338
374
  setRecord(s, {
339
375
  ...s.record,
340
376
  ctxNextSeq: ctxWm + 1
@@ -350,17 +386,21 @@ const sessionLostInner = (s, seqWm, ctxWm) => {
350
386
  setRecord(s, {
351
387
  clientId: s.record.clientId,
352
388
  nextSeq: seqWm + 1,
353
- ctxNextSeq: ctxWm + 1,
389
+ ctxNextSeq: s.record.ctxNextSeq,
354
390
  commands: [],
355
391
  ...s.record.failed !== void 0 && { failed: s.record.failed }
356
392
  });
357
- restateContext(s);
393
+ snapCtxClock(s, ctxWm);
358
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, () => {}, () => {});
359
395
  publish(s);
360
396
  wake(s);
361
397
  };
362
398
  const handleHello = (s, { ops, syn }) => {
363
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
+ };
364
404
  s.documents = {
365
405
  mode: null,
366
406
  documents: /* @__PURE__ */ new Map()
@@ -382,6 +422,7 @@ const handleHello = (s, { ops, syn }) => {
382
422
  s.ctxPending = s.ctxPending.filter((c) => c.seq > ctxWatermark);
383
423
  for (const c of s.ctxPending) c.posted = attachCtx.has(c.seq);
384
424
  snapCtxClock(s, ctxWatermark);
425
+ if (s.ctxOps !== void 0) persistContext(s);
385
426
  syncBacklogInner(s);
386
427
  s.sessionKnown = true;
387
428
  };
@@ -411,6 +452,7 @@ const handleRefusal = (s, syn) => {
411
452
  if (typeof ctxSeq === "number") {
412
453
  s.ctxPending = s.ctxPending.filter((c) => c.seq > ctxSeq);
413
454
  for (const c of s.ctxPending) c.posted = false;
455
+ if (s.ctxOps !== void 0) persistContext(s);
414
456
  }
415
457
  if (typeof syn.retry === "number") {
416
458
  s.resumeAt = Date.now() + syn.retry;
@@ -470,10 +512,13 @@ const buildLanes = (s) => {
470
512
  ops: c.ops,
471
513
  seq: c.seq
472
514
  })) },
473
- ...cmdEntries.length > 0 && { cmd: cmdEntries.map((p) => ({
474
- ...p.command,
475
- seq: p.seq
476
- })) }
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
+ }) }
477
522
  },
478
523
  cmdEntries,
479
524
  ctxEntries
@@ -592,6 +637,7 @@ const enqueueInner = (s, command, params, resolve, reject) => {
592
637
  seq,
593
638
  command,
594
639
  inv: {
640
+ protocol: command.protocol,
595
641
  method: command.method,
596
642
  params,
597
643
  ts: Date.now(),
@@ -645,11 +691,14 @@ const adoptStored = (s) => {
645
691
  const stored = s.record.commands.filter((c) => !known.has(c.seq));
646
692
  if (stored.length === 0) return;
647
693
  for (const { seq, ...command } of stored) {
694
+ const offers = protocolOffers(s.options);
695
+ if (offers !== "*" && !offers.some((offer) => offer.name === command.protocol)) continue;
648
696
  const params = command.meta === void 0 ? command.params : reviveValue([...command.params], command.meta);
649
697
  const entry = {
650
698
  seq,
651
699
  command,
652
700
  inv: {
701
+ protocol: command.protocol,
653
702
  method: command.method,
654
703
  params,
655
704
  ts: Date.now(),
@@ -667,7 +716,7 @@ const adoptStored = (s) => {
667
716
  }
668
717
  s.pending.sort((a, b) => a.seq - b.seq);
669
718
  };
670
- const sendCommand = (s, method, params) => {
719
+ const sendCommand = (s, method, params, protocol = s.options.protocol ?? "default") => {
671
720
  if (!s.running) return rejectedSend({
672
721
  fate: "aborted",
673
722
  cause: "unmount"
@@ -679,6 +728,11 @@ const sendCommand = (s, method, params) => {
679
728
  const result = new Promise((resolve, reject) => {
680
729
  const { params: encoded, meta } = encodeParams(params);
681
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
+ },
682
736
  method,
683
737
  params: encoded,
684
738
  ...meta !== void 0 && { meta }
@@ -745,6 +799,8 @@ const retryFailed = (s, entry) => {
745
799
  }
746
800
  takeFailed(s, current);
747
801
  enqueueInner(s, {
802
+ protocol: current.protocol,
803
+ ...current.produced !== void 0 && { produced: current.produced },
748
804
  method: current.command,
749
805
  params: [...current.params],
750
806
  ...current.meta !== void 0 && { meta: current.meta }
@@ -764,59 +820,167 @@ const retryFailed = (s, entry) => {
764
820
  result.catch(() => {});
765
821
  return result;
766
822
  };
767
- const setContext = (s, value) => {
768
- if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("statewire: context must be a plain object");
769
- if (!s.running || isFinished(s)) return;
770
- const { value: encoded, meta } = encodeValue(value);
771
- const ops = [{
772
- op: "replace",
773
- path: [],
774
- value: encoded,
775
- ...meta !== void 0 && { meta }
776
- }];
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);
777
837
  s.ctxOps = ops;
778
- 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);
779
862
  const seq = s.record.ctxNextSeq;
780
- setRecord(s, {
781
- ...s.record,
782
- ctxNextSeq: seq + 1
783
- });
784
- s.ctxPending.push({
863
+ s.ctxPending = [...s.ctxPending, {
785
864
  seq,
786
- 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
+ }),
787
871
  posted: false
788
- });
872
+ }];
873
+ s.record = {
874
+ ...s.record,
875
+ ctxNextSeq: seq + 1
876
+ };
877
+ persistContext(s);
789
878
  };
790
879
  if (!s.shared) {
791
- mint();
880
+ submit();
792
881
  wake(s);
793
882
  pump(s);
794
- return;
795
- }
796
- transact(s, () => {
797
- if (!s.running || isFinished(s)) return;
798
- mint();
799
- }).then(() => {
883
+ } else transact(s, submit).then(() => {
800
884
  wake(s);
801
885
  pump(s);
802
886
  });
803
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
+ };
804
945
  const useStatewireSession = (options) => {
805
946
  const [defaultStore] = useState(createMemorySessionStore);
806
947
  const mounted = useContext(SessionStorageContext);
948
+ const documentStore = useContext(DocumentStorageContext);
807
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");
808
950
  const store = options.sessionStore ?? mounted ?? defaultStore;
809
951
  if (store.lock === void 0 !== (store.subscribe === void 0)) throw new Error("statewire: session storage must provide lock and subscribe together");
810
952
  const [s] = useState(() => {
811
- 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
+ }
812
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);
813
973
  const session = {
814
- record: stored ?? {
815
- clientId: randomId(),
816
- nextSeq: 1,
817
- ctxNextSeq: 1,
818
- commands: []
819
- },
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,
820
984
  ctx: {
821
985
  get clientId() {
822
986
  return session.record.clientId;
@@ -832,6 +996,7 @@ const useStatewireSession = (options) => {
832
996
  },
833
997
  recordPersisted: stored !== null,
834
998
  store,
999
+ documentStore,
835
1000
  shared: store.lock !== void 0 ? {
836
1001
  txq: Promise.resolve(),
837
1002
  parked: [],
@@ -845,13 +1010,17 @@ const useStatewireSession = (options) => {
845
1010
  idle: false,
846
1011
  onSessionLost: void 0,
847
1012
  onDone: void 0,
848
- ctxOps: void 0,
849
- ctxPending: [],
1013
+ ctxOps: stored?.context?.ops,
1014
+ ctxPending: stored?.context?.pending.map((p) => ({
1015
+ ...p,
1016
+ posted: false
1017
+ })) ?? [],
850
1018
  lanesOnAttach: false,
851
1019
  attachLaneSeqs: [/* @__PURE__ */ new Set(), /* @__PURE__ */ new Set()],
852
1020
  frameFailures: 0,
853
1021
  pending: [],
854
1022
  canonical: void 0,
1023
+ documentListeners: /* @__PURE__ */ new Set(),
855
1024
  documents: {
856
1025
  mode: null,
857
1026
  documents: /* @__PURE__ */ new Map()
@@ -885,9 +1054,22 @@ const useStatewireSession = (options) => {
885
1054
  return session;
886
1055
  });
887
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;
888
1058
  s.store = store;
1059
+ s.documentStore = documentStore;
889
1060
  s.onSessionLost = options.onSessionLost;
890
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]);
891
1073
  useEffect(() => {
892
1074
  start(s);
893
1075
  return () => stop(s);
@@ -895,6 +1077,6 @@ const useStatewireSession = (options) => {
895
1077
  return s;
896
1078
  };
897
1079
  //#endregion
898
- export { discardFailed, failedCommands, retryFailed, setContext, setTransport, useStatewireSession };
1080
+ export { discardFailed, documentSource, failedCommands, invokeProtocol, retryFailed, setContext, setTransport, updateContext, useStatewireSession };
899
1081
 
900
1082
  //# sourceMappingURL=session.js.map