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.
- package/README.md +15 -1
- package/dist/client/connection.d.ts.map +1 -1
- package/dist/client/connection.js +18 -4
- package/dist/client/connection.js.map +1 -1
- package/dist/client/session.d.ts +10 -1
- package/dist/client/session.d.ts.map +1 -1
- package/dist/client/session.js +92 -27
- package/dist/client/session.js.map +1 -1
- package/dist/client/sessionStore.d.ts +2 -0
- package/dist/client/sessionStore.d.ts.map +1 -1
- package/dist/client/sessionStore.js.map +1 -1
- package/dist/host/host.d.ts +2 -1
- package/dist/host/host.d.ts.map +1 -1
- package/dist/host/host.js +20 -12
- package/dist/host/host.js.map +1 -1
- package/dist/host/http.d.ts.map +1 -1
- package/dist/host/http.js +14 -9
- package/dist/host/http.js.map +1 -1
- package/dist/host/socket.d.ts +14 -3
- package/dist/host/socket.d.ts.map +1 -1
- package/dist/host/socket.js +162 -20
- package/dist/host/socket.js.map +1 -1
- package/dist/host-internal.d.ts +5 -2
- package/dist/host-internal.d.ts.map +1 -1
- package/dist/host.d.ts +6 -0
- package/dist/host.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/statewire.d.ts +3 -0
- package/dist/statewire.d.ts.map +1 -1
- package/dist/statewire.js +23 -5
- package/dist/statewire.js.map +1 -1
- package/dist/transports/remote.d.ts.map +1 -1
- package/dist/transports/remote.js +3 -2
- package/dist/transports/remote.js.map +1 -1
- package/dist/transports/sse.d.ts +5 -0
- package/dist/transports/sse.d.ts.map +1 -1
- package/dist/transports/sse.js +12 -0
- package/dist/transports/sse.js.map +1 -1
- package/dist/transports/testing.d.ts +2 -0
- package/dist/transports/testing.d.ts.map +1 -1
- package/dist/transports/testing.js +11 -4
- package/dist/transports/testing.js.map +1 -1
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/wire/context.d.ts +14 -0
- package/dist/wire/context.d.ts.map +1 -0
- package/dist/wire/context.js +31 -0
- package/dist/wire/context.js.map +1 -0
- package/dist/wire/limits.d.ts +2 -1
- package/dist/wire/limits.d.ts.map +1 -1
- package/dist/wire/limits.js +2 -1
- package/dist/wire/limits.js.map +1 -1
- package/dist/wire/protocol.d.ts +1 -1
- package/dist/wire/protocol.js +4 -4
- package/dist/wire/protocol.js.map +1 -1
- package/package.json +1 -1
- package/src/client/connection.ts +16 -4
- package/src/client/session.ts +128 -19
- package/src/client/sessionStore.ts +5 -1
- package/src/host/host.ts +18 -3
- package/src/host/http.ts +37 -5
- package/src/host/socket.ts +232 -13
- package/src/host-internal.ts +4 -0
- package/src/host.ts +5 -0
- package/src/statewire.ts +34 -3
- package/src/transports/remote.ts +3 -0
- package/src/transports/sse.ts +19 -0
- package/src/transports/testing.ts +14 -3
- package/src/types.ts +12 -0
- package/src/wire/context.ts +40 -0
- package/src/wire/limits.ts +2 -0
- package/src/wire/protocol.ts +1 -1
package/dist/client/session.js
CHANGED
|
@@ -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
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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: {
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
...command
|
|
583
|
-
|
|
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(
|
|
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-
|
|
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(
|
|
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-
|
|
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 (
|
|
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
|