statewire 0.17.2 → 0.18.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.
@@ -8,6 +8,7 @@ import { bindCommandsHost } from "../host/host.js";
8
8
  import "../wire/limits.js";
9
9
  import { DocumentStorageContext, SessionStorageContext, createMemorySessionStore } from "./sessionStore.js";
10
10
  import { backoff } from "./timing.js";
11
+ import { wrapStorageError } from "./storageError.js";
11
12
  import { TERMINAL } from "./connection.js";
12
13
  import { randomId } from "../util/randomId.js";
13
14
  import { useContext, useEffect, useState } from "@assistant-ui/tap/react-shim";
@@ -42,9 +43,20 @@ const sleepUntilWake = (s, delayMs) => new Promise((resolve) => {
42
43
  }, delayMs);
43
44
  s.waiters.push(waiter);
44
45
  });
46
+ const storageFailed = (s, cause) => {
47
+ const error = wrapStorageError(cause);
48
+ s.storageError = error;
49
+ for (const entry of s.pending) entry.reject(error);
50
+ s.transport?.storageFailed(error);
51
+ };
45
52
  const setRecord = (s, record) => {
46
53
  s.record = record;
47
- if (!s.deactivated) s.store.save(record);
54
+ if (!s.deactivated) try {
55
+ s.store.save(record);
56
+ } catch (cause) {
57
+ storageFailed(s, cause);
58
+ throw s.storageError;
59
+ }
48
60
  };
49
61
  const backlog = (s) => s.pending.filter((p) => p.seq > s.durableSeq).map((p) => ({
50
62
  seq: p.seq,
@@ -70,7 +82,7 @@ const syncBacklogInner = (s) => {
70
82
  };
71
83
  const syncBacklog = (s) => {
72
84
  if (!s.shared) return syncBacklogInner(s);
73
- transact(s, () => syncBacklogInner(s));
85
+ transactBackground(s, () => syncBacklogInner(s));
74
86
  };
75
87
  const transact = (s, fn) => {
76
88
  if (!s.shared || s.deactivated) {
@@ -78,7 +90,7 @@ const transact = (s, fn) => {
78
90
  return Promise.resolve();
79
91
  }
80
92
  const shared = s.shared;
81
- const run = shared.txq.then(() => s.store.lock(async () => {
93
+ const result = shared.txq.then(() => s.store.lock(async () => {
82
94
  const stored = s.store.load();
83
95
  if (stored !== null && stored.clientId !== s.record.clientId) adoptReset(s, stored);
84
96
  else if (stored !== null) {
@@ -97,9 +109,17 @@ const transact = (s, fn) => {
97
109
  }
98
110
  }
99
111
  fn();
100
- }));
101
- shared.txq = run.then(() => {}, () => {});
102
- return run;
112
+ const persistence = s.store.flush?.();
113
+ if (persistence) await persistence;
114
+ })).catch((cause) => {
115
+ storageFailed(s, cause);
116
+ throw s.storageError;
117
+ });
118
+ shared.txq = result.then(() => {}, () => {});
119
+ return result;
120
+ };
121
+ const transactBackground = (s, fn, done) => {
122
+ transact(s, fn).then(done, () => {});
103
123
  };
104
124
  const setIdle = (s, level) => {
105
125
  if (level === s.idle) return;
@@ -136,7 +156,7 @@ const retainFailed = (s, entry, failure) => {
136
156
  retainFailedInner(s, entry, failure);
137
157
  return;
138
158
  }
139
- transact(s, () => retainFailedInner(s, entry, failure));
159
+ transactBackground(s, () => retainFailedInner(s, entry, failure));
140
160
  };
141
161
  const settleEntry = (s, entry, outcome) => {
142
162
  s.pending = s.pending.filter((p) => p !== entry);
@@ -463,7 +483,7 @@ const handleHello = (s, { ops, syn }) => {
463
483
  pump(s);
464
484
  return;
465
485
  }
466
- transact(s, reconcile).then(() => {
486
+ transactBackground(s, reconcile, () => {
467
487
  publishIngested(s);
468
488
  pump(s);
469
489
  });
@@ -496,10 +516,10 @@ const handleRefusal = (s, syn) => {
496
516
  const cmdGap = typeof seq === "number" && firstCmd !== void 0 && firstCmd.seq > Math.max(seq, s.durableSeq) + 1;
497
517
  const ctxGap = typeof ctxSeq === "number" && firstCtx !== void 0 && firstCtx.seq > ctxSeq + 1;
498
518
  if (cmdGap || ctxGap) {
499
- transact(s, () => {
519
+ transactBackground(s, () => {
500
520
  if (cmdGap) sessionLostInner(s, seq, ctxSeq ?? 0);
501
521
  else snapCtxClock(s, ctxSeq);
502
- }).then(() => {
522
+ }, () => {
503
523
  wake(s);
504
524
  pump(s);
505
525
  });
@@ -521,10 +541,11 @@ const handleFinish = (s, { reason }) => {
521
541
  wake(s);
522
542
  };
523
543
  const isFinished = (s) => s.attach === "finished";
524
- const buildLanes = (s) => {
544
+ const buildLanes = (s, persisted = false) => {
525
545
  const ctxEntries = [];
526
546
  for (const entry of s.ctxPending) {
527
547
  if (entry.posted) continue;
548
+ if (persisted && (s.persistedRecord?.clientId !== s.record.clientId || entry.seq >= s.persistedRecord.ctxNextSeq)) break;
528
549
  if (ctxEntries.length >= 256) break;
529
550
  if (ctxEntries.length > 0 && entry.seq !== ctxEntries.at(-1).seq + 1) break;
530
551
  ctxEntries.push(entry);
@@ -532,6 +553,7 @@ const buildLanes = (s) => {
532
553
  const cmdEntries = [];
533
554
  for (const entry of s.pending) {
534
555
  if (entry.posted) continue;
556
+ if (persisted && (s.persistedRecord?.clientId !== s.record.clientId || entry.seq >= s.persistedRecord.nextSeq)) break;
535
557
  if (cmdEntries.length >= 256) break;
536
558
  if (cmdEntries.length > 0 && entry.seq !== cmdEntries.at(-1).seq + 1) break;
537
559
  cmdEntries.push(entry);
@@ -560,21 +582,38 @@ const takeAttachLanes = (s) => {
560
582
  const wanted = s.lanesOnAttach;
561
583
  s.lanesOnAttach = false;
562
584
  if (!wanted || !s.sessionKnown || s.record.context?.recover !== void 0) return null;
563
- const lanes = buildLanes(s);
585
+ const lanes = buildLanes(s, true);
564
586
  if (!lanes) return null;
565
587
  s.attachLaneSeqs = [new Set(lanes.cmdEntries.map((p) => p.seq)), new Set(lanes.ctxEntries.map((c) => c.seq))];
566
588
  for (const entry of lanes.cmdEntries) entry.attempted = true;
567
589
  return lanes.frame;
568
590
  };
591
+ const persistBeforeSend = (s) => {
592
+ if (s.storageError !== void 0) throw s.storageError;
593
+ if (!s.hydrated) return s.ready.then(() => persistBeforeSend(s));
594
+ const record = s.record;
595
+ try {
596
+ const pending = s.store.flush?.();
597
+ if (pending) return pending.then(() => {
598
+ s.persistedRecord = record;
599
+ }, (cause) => {
600
+ throw wrapStorageError(cause);
601
+ });
602
+ s.persistedRecord = record;
603
+ } catch (cause) {
604
+ throw wrapStorageError(cause);
605
+ }
606
+ };
569
607
  const pump = async (s) => {
570
608
  if (s.pumping) return;
571
609
  s.pumping = true;
572
610
  try {
573
611
  while (s.running && !isFinished(s)) {
612
+ if (!s.hydrated) await s.ready;
574
613
  const transport = s.transport;
575
614
  if (buildLanes(s) === null) return;
576
615
  if (s.attach !== "attached" || !transport) {
577
- if (transport?.connection.status === "disconnected" && transport.connection.reason === "idle" || transport?.parked && s.pending.length > 0) transport.connection.reconnect();
616
+ if (transport?.connection.status === "standby" && transport.connection.reason === "idle" || transport?.parked && s.pending.length > 0) transport.connection.reconnect();
578
617
  await waitForWake(s);
579
618
  continue;
580
619
  }
@@ -586,6 +625,10 @@ const pump = async (s) => {
586
625
  const lanes = buildLanes(s);
587
626
  if (!lanes) continue;
588
627
  const epoch = s.attachEpoch;
628
+ const persistence = persistBeforeSend(s);
629
+ if (persistence) await persistence;
630
+ if (!s.running || isFinished(s)) return;
631
+ if (epoch !== s.attachEpoch || s.attach !== "attached") continue;
589
632
  for (const entry of lanes.cmdEntries) entry.attempted = true;
590
633
  let result;
591
634
  try {
@@ -622,22 +665,31 @@ const pump = async (s) => {
622
665
  }
623
666
  }
624
667
  }
668
+ } catch (error) {
669
+ storageFailed(s, error);
625
670
  } finally {
626
671
  s.pumping = false;
627
672
  }
628
673
  };
629
674
  const start = (s) => {
675
+ s.running = true;
676
+ if (!s.hydrated) {
677
+ s.ready.then(() => {
678
+ if (s.running) start(s);
679
+ }, () => {});
680
+ return;
681
+ }
630
682
  if (s.shared) {
631
683
  s.shared.unsubscribe = s.store.subscribe(() => {
632
- transact(s, () => consumeVerdicts(s));
684
+ transactBackground(s, () => consumeVerdicts(s));
633
685
  });
634
686
  s.running = true;
635
- transact(s, () => {
687
+ transactBackground(s, () => {
636
688
  if (s.store.load() === null) s.store.save(s.record);
637
689
  s.recordPersisted = true;
638
690
  adoptStored(s);
639
691
  consumeVerdicts(s);
640
- }).then(() => void pump(s));
692
+ }, () => void pump(s));
641
693
  return;
642
694
  }
643
695
  if (!s.recordPersisted) {
@@ -715,7 +767,7 @@ const enqueue = (s, command, params, resolve, reject) => {
715
767
  }).then(() => {
716
768
  wake(s);
717
769
  pump(s);
718
- });
770
+ }, reject);
719
771
  };
720
772
  const adoptStored = (s) => {
721
773
  const known = new Set(s.pending.map((p) => p.seq));
@@ -748,6 +800,16 @@ const adoptStored = (s) => {
748
800
  s.pending.sort((a, b) => a.seq - b.seq);
749
801
  };
750
802
  const sendCommand = (s, method, params, protocol = s.options.protocol ?? "default") => {
803
+ if (!s.hydrated) {
804
+ const result = s.ready.then(() => sendCommand(s, method, params, protocol));
805
+ result.catch(() => {});
806
+ return result;
807
+ }
808
+ if (s.storageError !== void 0) {
809
+ const result = Promise.reject(s.storageError);
810
+ result.catch(() => {});
811
+ return result;
812
+ }
751
813
  if (!s.running) return rejectedSend({
752
814
  fate: "aborted",
753
815
  cause: "unmount"
@@ -791,7 +853,7 @@ const discardFailed = (s, entry) => {
791
853
  return;
792
854
  }
793
855
  if (findFailed(s, entry) === void 0) throw unknownFailed(entry);
794
- transact(s, () => {
856
+ transactBackground(s, () => {
795
857
  if (findFailed(s, entry) !== void 0) takeFailed(s, entry);
796
858
  });
797
859
  };
@@ -879,6 +941,14 @@ const persistContext = (s) => {
879
941
  });
880
942
  };
881
943
  const changeContext = (s, build) => {
944
+ if (!s.hydrated) {
945
+ s.ready.then(() => changeContext(s, build)).catch((cause) => {
946
+ const error = wrapStorageError(cause);
947
+ s.storageError = error;
948
+ s.transport?.storageFailed(error);
949
+ });
950
+ return;
951
+ }
882
952
  if (!s.running || isFinished(s)) return;
883
953
  const submit = () => {
884
954
  if (!s.running || isFinished(s)) return;
@@ -912,7 +982,7 @@ const changeContext = (s, build) => {
912
982
  submit();
913
983
  wake(s);
914
984
  pump(s);
915
- } else transact(s, submit).then(() => {
985
+ } else transactBackground(s, submit, () => {
916
986
  wake(s);
917
987
  pump(s);
918
988
  });
@@ -974,6 +1044,32 @@ const storedCommandCompatible = (command, current) => {
974
1044
  const offer = current.protocols.find((p) => p.name === command.protocol);
975
1045
  return produced !== void 0 && offer !== void 0 && inRange(produced.version, offer);
976
1046
  };
1047
+ const loadSessionRecord = (store, options) => {
1048
+ let stored = store.load();
1049
+ if (stored !== null && !Array.isArray(stored.commands)) throw new Error("statewire: session storage loaded a record without a commands array");
1050
+ const current = {
1051
+ wire: options.wireVersion ?? { version: "2026-09-07" },
1052
+ protocols: protocolOffers(options)
1053
+ };
1054
+ if (stored !== null && !stored.commands.every((command) => storedCommandCompatible(command, current))) {
1055
+ if (!store.migrate) throw new Error("statewire: stored commands require a storage migration");
1056
+ stored = store.migrate(stored, current);
1057
+ if (stored !== null && !stored.commands.every((command) => storedCommandCompatible(command, current))) throw new Error("statewire: migrated commands are incompatible");
1058
+ if (stored !== null) store.save(stored);
1059
+ }
1060
+ if (stored !== null && !Array.isArray(stored.commands)) throw new Error("statewire: session storage loaded a record without a commands array");
1061
+ const record = stored ?? {
1062
+ clientId: randomId(),
1063
+ nextSeq: 1,
1064
+ ctxNextSeq: 1,
1065
+ commands: []
1066
+ };
1067
+ if (stored === null) store.save(record);
1068
+ return {
1069
+ record,
1070
+ stored
1071
+ };
1072
+ };
977
1073
  const useStatewireSession = (options) => {
978
1074
  const [defaultStore] = useState(createMemorySessionStore);
979
1075
  const mounted = useContext(SessionStorageContext);
@@ -982,26 +1078,15 @@ const useStatewireSession = (options) => {
982
1078
  const store = options.sessionStore ?? mounted ?? defaultStore;
983
1079
  if (store.lock === void 0 !== (store.subscribe === void 0)) throw new Error("statewire: session storage must provide lock and subscribe together");
984
1080
  const [s] = useState(() => {
985
- let stored = store.load();
986
- if (stored !== null && !Array.isArray(stored.commands)) throw new Error("statewire: session storage loaded a record without a commands array");
987
- const current = {
988
- wire: options.wireVersion ?? { version: "2026-09-07" },
989
- protocols: protocolOffers(options)
990
- };
991
- if (stored !== null && !stored.commands.every((command_0) => storedCommandCompatible(command_0, current))) {
992
- if (!store.migrate) throw new Error("statewire: stored commands require a storage migration");
993
- stored = store.migrate(stored, current);
994
- if (stored !== null && !stored.commands.every((command) => storedCommandCompatible(command, current))) throw new Error("statewire: migrated commands are incompatible");
995
- if (stored !== null) store.save(stored);
996
- }
997
- if (stored !== null && !Array.isArray(stored.commands)) throw new Error("statewire: session storage loaded a record without a commands array");
998
- const record = stored ?? {
999
- clientId: randomId(),
1000
- nextSeq: 1,
1001
- ctxNextSeq: 1,
1002
- commands: []
1081
+ const { record, stored } = store.ready === void 0 ? loadSessionRecord(store, options) : {
1082
+ record: {
1083
+ clientId: randomId(),
1084
+ nextSeq: 1,
1085
+ ctxNextSeq: 1,
1086
+ commands: []
1087
+ },
1088
+ stored: null
1003
1089
  };
1004
- if (stored === null) store.save(record);
1005
1090
  const session = {
1006
1091
  options,
1007
1092
  selection: void 0,
@@ -1014,6 +1099,7 @@ const useStatewireSession = (options) => {
1014
1099
  }, stored.context.ops),
1015
1100
  record,
1016
1101
  ctx: {
1102
+ beforeAttach: () => persistBeforeSend(session),
1017
1103
  get clientId() {
1018
1104
  return session.record.clientId;
1019
1105
  },
@@ -1026,7 +1112,11 @@ const useStatewireSession = (options) => {
1026
1112
  onDetach: () => handleDetach(session),
1027
1113
  onFinish: (args_1) => handleFinish(session, args_1)
1028
1114
  },
1029
- recordPersisted: stored !== null,
1115
+ recordPersisted: store.ready === void 0,
1116
+ hydrated: store.ready === void 0,
1117
+ ready: void 0,
1118
+ storageError: void 0,
1119
+ persistedRecord: store.ready === void 0 ? record : null,
1030
1120
  store,
1031
1121
  documentStore,
1032
1122
  shared: store.lock !== void 0 ? {
@@ -1080,6 +1170,34 @@ const useStatewireSession = (options) => {
1080
1170
  notify: () => {},
1081
1171
  commands: createMethodProxy((method, params) => sendCommand(session, method, params))
1082
1172
  };
1173
+ if (store.ready !== void 0) {
1174
+ session.ready = store.ready.then(() => {
1175
+ const { record: record_0 } = loadSessionRecord(store, session.options);
1176
+ session.record = record_0;
1177
+ session.contextDocuments = record_0.context === void 0 ? {
1178
+ mode: "mounted",
1179
+ documents: /* @__PURE__ */ new Map()
1180
+ } : applyDocumentOps({
1181
+ mode: "mounted",
1182
+ documents: /* @__PURE__ */ new Map()
1183
+ }, record_0.context.ops);
1184
+ session.ctxOps = record_0.context?.ops;
1185
+ session.ctxPending = record_0.context?.pending.map((entry) => ({
1186
+ ...entry,
1187
+ posted: false
1188
+ })) ?? [];
1189
+ session.recordPersisted = true;
1190
+ session.hydrated = true;
1191
+ if (session.running) session.notify();
1192
+ });
1193
+ session.ready = session.ready.catch((cause) => {
1194
+ const error = wrapStorageError(cause);
1195
+ session.storageError = error;
1196
+ session.transport?.storageFailed(error);
1197
+ throw error;
1198
+ });
1199
+ session.ready.catch(() => {});
1200
+ }
1083
1201
  bindCommandsHost(session.commands, { setListener: (listener_0) => {
1084
1202
  session.lifecycle = listener_0;
1085
1203
  } });