statewire 0.17.2 → 0.18.1
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.
- package/dist/client/connection.d.ts +2 -3
- package/dist/client/connection.d.ts.map +1 -1
- package/dist/client/connection.js +61 -25
- package/dist/client/connection.js.map +1 -1
- package/dist/client/session.d.ts +6 -2
- package/dist/client/session.d.ts.map +1 -1
- package/dist/client/session.js +158 -40
- package/dist/client/session.js.map +1 -1
- package/dist/client/storageError.d.ts +6 -0
- package/dist/client/storageError.d.ts.map +1 -0
- package/dist/client/storageError.js +12 -0
- package/dist/client/storageError.js.map +1 -0
- package/dist/statewire.d.ts +23 -11
- package/dist/statewire.d.ts.map +1 -1
- package/dist/statewire.js.map +1 -1
- package/dist/storage.d.ts +33 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +371 -0
- package/dist/storage.js.map +1 -0
- package/dist/transports/sse.d.ts +2 -0
- package/dist/transports/sse.d.ts.map +1 -1
- package/dist/transports/sse.js +3 -0
- package/dist/transports/sse.js.map +1 -1
- package/package.json +5 -1
- package/src/client/connection.ts +57 -28
- package/src/client/session.ts +232 -72
- package/src/client/storageError.ts +9 -0
- package/src/statewire.ts +25 -20
- package/src/storage.ts +601 -0
- package/src/transports/sse.ts +5 -0
package/dist/client/session.js
CHANGED
|
@@ -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)
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
519
|
+
transactBackground(s, () => {
|
|
500
520
|
if (cmdGap) sessionLostInner(s, seq, ctxSeq ?? 0);
|
|
501
521
|
else snapCtxClock(s, ctxSeq);
|
|
502
|
-
}
|
|
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 === "
|
|
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
|
-
|
|
684
|
+
transactBackground(s, () => consumeVerdicts(s));
|
|
633
685
|
});
|
|
634
686
|
s.running = true;
|
|
635
|
-
|
|
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
|
-
}
|
|
692
|
+
}, () => void pump(s));
|
|
641
693
|
return;
|
|
642
694
|
}
|
|
643
695
|
if (!s.recordPersisted) {
|
|
@@ -663,7 +715,7 @@ const setTransport = (s, transport) => {
|
|
|
663
715
|
wake(s);
|
|
664
716
|
};
|
|
665
717
|
const enqueueInner = (s, command, params, resolve, reject) => {
|
|
666
|
-
const seq = s.record.nextSeq;
|
|
718
|
+
const seq = Math.max(s.record.nextSeq, s.durableSeq + 1);
|
|
667
719
|
const entry = {
|
|
668
720
|
seq,
|
|
669
721
|
command,
|
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
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:
|
|
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
|
} });
|