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.
- package/README.md +14 -6
- package/dist/client/connection.d.ts +5 -1
- package/dist/client/connection.d.ts.map +1 -1
- package/dist/client/connection.js +19 -4
- package/dist/client/connection.js.map +1 -1
- package/dist/client/core.d.ts.map +1 -1
- package/dist/client/core.js +2 -0
- package/dist/client/core.js.map +1 -1
- package/dist/client/session.d.ts +15 -4
- package/dist/client/session.d.ts.map +1 -1
- package/dist/client/session.js +233 -52
- package/dist/client/session.js.map +1 -1
- package/dist/client/sessionStore.d.ts +5 -1
- package/dist/client/sessionStore.d.ts.map +1 -1
- package/dist/client/sessionStore.js +3 -1
- package/dist/client/sessionStore.js.map +1 -1
- package/dist/host/host.d.ts +1 -0
- package/dist/host/host.d.ts.map +1 -1
- package/dist/host/host.js.map +1 -1
- package/dist/host/http.d.ts.map +1 -1
- package/dist/host/http.js +27 -9
- package/dist/host/http.js.map +1 -1
- package/dist/host/socket.d.ts +10 -3
- package/dist/host/socket.d.ts.map +1 -1
- package/dist/host/socket.js +103 -39
- package/dist/host/socket.js.map +1 -1
- package/dist/host-internal.d.ts +9 -1
- package/dist/host-internal.d.ts.map +1 -1
- package/dist/host-internal.js +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +3 -3
- package/dist/statewire.d.ts +33 -2
- package/dist/statewire.d.ts.map +1 -1
- package/dist/statewire.js +34 -3
- package/dist/statewire.js.map +1 -1
- package/dist/transports/fixtures.js +1 -0
- package/dist/transports/fixtures.js.map +1 -1
- package/dist/transports/remote.d.ts.map +1 -1
- package/dist/transports/remote.js +15 -8
- package/dist/transports/remote.js.map +1 -1
- package/dist/transports/sse.d.ts +2 -2
- package/dist/transports/sse.d.ts.map +1 -1
- package/dist/transports/sse.js +11 -11
- package/dist/transports/sse.js.map +1 -1
- package/dist/transports/testing.d.ts +1 -1
- package/dist/transports/testing.d.ts.map +1 -1
- package/dist/transports/testing.js +14 -6
- package/dist/transports/testing.js.map +1 -1
- package/dist/transports/ws.d.ts +2 -2
- package/dist/transports/ws.d.ts.map +1 -1
- package/dist/transports/ws.js +5 -5
- package/dist/transports/ws.js.map +1 -1
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/util/randomId.js +1 -1
- package/dist/util/randomId.js.map +1 -1
- package/dist/wire/documents.d.ts +25 -2
- package/dist/wire/documents.d.ts.map +1 -1
- package/dist/wire/documents.js +27 -7
- package/dist/wire/documents.js.map +1 -1
- package/dist/wire/protocol.d.ts +55 -0
- package/dist/wire/protocol.d.ts.map +1 -0
- package/dist/wire/protocol.js +221 -0
- package/dist/wire/protocol.js.map +1 -0
- package/dist/wire/version.d.ts +1 -3
- package/dist/wire/version.d.ts.map +1 -1
- package/dist/wire/version.js +1 -3
- package/dist/wire/version.js.map +1 -1
- package/package.json +1 -1
- package/src/client/connection.ts +43 -13
- package/src/client/core.ts +6 -0
- package/src/client/session.ts +339 -42
- package/src/client/sessionStore.ts +8 -0
- package/src/host/host.ts +1 -0
- package/src/host/http.ts +36 -7
- package/src/host/socket.ts +141 -50
- package/src/host-internal.ts +12 -1
- package/src/index.ts +5 -2
- package/src/statewire.ts +109 -6
- package/src/transports/fixtures.ts +1 -1
- package/src/transports/remote.ts +16 -7
- package/src/transports/sse.ts +28 -18
- package/src/transports/testing.ts +12 -4
- package/src/transports/ws.ts +8 -8
- package/src/types.ts +11 -0
- package/src/util/randomId.ts +3 -3
- package/src/wire/documents.ts +126 -8
- package/src/wire/protocol.ts +327 -0
- package/src/wire/version.ts +0 -2
package/dist/client/session.js
CHANGED
|
@@ -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)
|
|
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,
|
|
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.
|
|
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:
|
|
389
|
+
ctxNextSeq: s.record.ctxNextSeq,
|
|
355
390
|
commands: [],
|
|
356
391
|
...s.record.failed !== void 0 && { failed: s.record.failed }
|
|
357
392
|
});
|
|
358
|
-
|
|
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
|
-
|
|
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
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
880
|
+
submit();
|
|
793
881
|
wake(s);
|
|
794
882
|
pump(s);
|
|
795
|
-
|
|
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
|
-
|
|
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
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
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:
|
|
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
|