statewire 0.18.1 → 0.18.2
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/session.d.ts +1 -1
- package/dist/client/session.d.ts.map +1 -1
- package/dist/client/session.js +48 -188
- package/dist/client/session.js.map +1 -1
- package/dist/host/http.d.ts.map +1 -1
- package/dist/host/http.js +2 -3
- package/dist/host/http.js.map +1 -1
- package/dist/host/socket.d.ts +4 -5
- package/dist/host/socket.d.ts.map +1 -1
- package/dist/host/socket.js +33 -44
- package/dist/host/socket.js.map +1 -1
- package/dist/host-internal.d.ts +0 -4
- package/dist/host-internal.d.ts.map +1 -1
- package/dist/host.d.ts +1 -1
- package/dist/statewire.d.ts +1 -11
- package/dist/statewire.d.ts.map +1 -1
- package/dist/statewire.js.map +1 -1
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +6 -55
- package/dist/storage.js.map +1 -1
- package/dist/transports/sse.d.ts +1 -1
- package/dist/transports/sse.js.map +1 -1
- package/dist/transports/testing.js +2 -1
- package/dist/transports/testing.js.map +1 -1
- package/package.json +1 -1
- package/src/client/session.ts +67 -211
- package/src/host/http.ts +2 -9
- package/src/host/socket.ts +38 -46
- package/src/host-internal.ts +0 -4
- package/src/host.ts +1 -1
- package/src/statewire.ts +1 -8
- package/src/storage.ts +8 -92
- package/src/transports/sse.ts +1 -1
- package/src/transports/testing.ts +2 -1
package/src/host/socket.ts
CHANGED
|
@@ -94,16 +94,15 @@ type Admission = {
|
|
|
94
94
|
export type ClientRec = {
|
|
95
95
|
id: string;
|
|
96
96
|
lastSeq: number;
|
|
97
|
-
ctxSeq: number;
|
|
98
97
|
touched: number;
|
|
99
|
-
context: Record<string, unknown> | null;
|
|
100
|
-
contextDocuments: StatewireDocuments.State;
|
|
101
98
|
results: Map<number, { ts: number; cmd: StatewireCmd }>;
|
|
102
99
|
inflight: Map<number, Admission>;
|
|
103
100
|
/** The auth captured at the record's first attach; later attaches must match. */
|
|
104
101
|
principal: unknown;
|
|
105
102
|
};
|
|
106
103
|
|
|
104
|
+
// The ctx lane is attach scoped: a Conn is born with an empty context table
|
|
105
|
+
// and outlives its socket so an admitted command keeps reading its last value.
|
|
107
106
|
export type Conn = {
|
|
108
107
|
selection: StatewireProtocol.Selection;
|
|
109
108
|
clientId: string;
|
|
@@ -114,6 +113,9 @@ export type Conn = {
|
|
|
114
113
|
lease: string | null;
|
|
115
114
|
/** The awaited `authorize` return captured at this attach's authorization. */
|
|
116
115
|
auth: unknown;
|
|
116
|
+
ctxSeq: number;
|
|
117
|
+
context: Record<string, unknown> | null;
|
|
118
|
+
contextDocuments: StatewireDocuments.State;
|
|
117
119
|
};
|
|
118
120
|
|
|
119
121
|
type PendingConn = {
|
|
@@ -556,10 +558,7 @@ const notifyClientRecords = (h: SocketHostState) => {
|
|
|
556
558
|
[...h.clients.values()].map((rec) => ({
|
|
557
559
|
id: rec.id,
|
|
558
560
|
lastSeq: admittedThrough(rec),
|
|
559
|
-
ctxSeq: rec.ctxSeq,
|
|
560
561
|
touched: rec.touched,
|
|
561
|
-
context: rec.context,
|
|
562
|
-
contextDocuments: [...rec.contextDocuments.documents],
|
|
563
562
|
results: [...rec.results].map(([seq, entry]) => ({
|
|
564
563
|
seq,
|
|
565
564
|
ts: entry.ts,
|
|
@@ -636,12 +635,12 @@ const settleWire = (
|
|
|
636
635
|
const callerOf = (
|
|
637
636
|
h: SocketHostState,
|
|
638
637
|
rec: ClientRec,
|
|
639
|
-
|
|
638
|
+
conn: Conn,
|
|
640
639
|
): StatewireHost.Caller => ({
|
|
641
640
|
clientId: rec.id,
|
|
642
|
-
auth,
|
|
641
|
+
auth: conn.auth,
|
|
643
642
|
get context() {
|
|
644
|
-
return
|
|
643
|
+
return conn.context;
|
|
645
644
|
},
|
|
646
645
|
get isConnected() {
|
|
647
646
|
if (h.clients.get(rec.id) !== rec) return false;
|
|
@@ -668,9 +667,8 @@ const rejectStatement = (
|
|
|
668
667
|
const admitCmd = (
|
|
669
668
|
h: SocketHostState,
|
|
670
669
|
rec: ClientRec,
|
|
670
|
+
conn: Conn,
|
|
671
671
|
statement: CmdStatement,
|
|
672
|
-
auth: unknown,
|
|
673
|
-
selection: StatewireProtocol.Selection,
|
|
674
672
|
) => {
|
|
675
673
|
const adm: Admission = {
|
|
676
674
|
seq: statement.seq,
|
|
@@ -683,7 +681,7 @@ const admitCmd = (
|
|
|
683
681
|
notifyClientRecords(h);
|
|
684
682
|
if (
|
|
685
683
|
statement.protocol !== h.protocol.name ||
|
|
686
|
-
!selection.protocols.some((p) => p.name === statement.protocol)
|
|
684
|
+
!conn.selection.protocols.some((p) => p.name === statement.protocol)
|
|
687
685
|
) {
|
|
688
686
|
settleCmd(h, rec, adm, {
|
|
689
687
|
seq: adm.seq,
|
|
@@ -729,7 +727,7 @@ const admitCmd = (
|
|
|
729
727
|
method: statement.method,
|
|
730
728
|
params,
|
|
731
729
|
clientId: rec.id,
|
|
732
|
-
caller: callerOf(h, rec,
|
|
730
|
+
caller: callerOf(h, rec, conn),
|
|
733
731
|
onApplied: () => {
|
|
734
732
|
if (adm.settled || adm.applied) return;
|
|
735
733
|
adm.applied = true;
|
|
@@ -743,9 +741,10 @@ const admitCmd = (
|
|
|
743
741
|
});
|
|
744
742
|
};
|
|
745
743
|
|
|
746
|
-
// One admission law, both lanes: absorbed statements answer from the record
|
|
747
|
-
// then the new suffix is admitted whole or the
|
|
748
|
-
// {syn} stating both watermarks, nothing
|
|
744
|
+
// One admission law, both lanes: absorbed statements answer from the record
|
|
745
|
+
// (cmd) or the attach (ctx), then the new suffix is admitted whole or the
|
|
746
|
+
// frame is refused whole via a {syn} stating both watermarks, nothing
|
|
747
|
+
// consumed. A gap is never a close.
|
|
749
748
|
export type FrameAdmission =
|
|
750
749
|
| { type: "ok" }
|
|
751
750
|
| {
|
|
@@ -757,29 +756,25 @@ export type FrameAdmission =
|
|
|
757
756
|
|
|
758
757
|
export const admitFrame = (
|
|
759
758
|
h: SocketHostState,
|
|
760
|
-
|
|
759
|
+
conn: Conn,
|
|
761
760
|
frame: ParsedFrame,
|
|
762
|
-
auth: unknown,
|
|
763
|
-
selection: StatewireProtocol.Selection = {
|
|
764
|
-
version: h.wireVersion.version,
|
|
765
|
-
protocols: [{ name: h.protocol.name, version: h.protocol.version }],
|
|
766
|
-
},
|
|
767
761
|
): FrameAdmission => {
|
|
762
|
+
const rec = h.clients.get(conn.clientId)!;
|
|
768
763
|
rec.touched = Date.now();
|
|
769
764
|
const through = admittedThrough(rec);
|
|
770
765
|
for (const statement of frame.cmd) {
|
|
771
766
|
if (statement.seq <= through) reemit(h, rec, statement.seq);
|
|
772
767
|
}
|
|
773
|
-
const ctxNew = frame.ctx.filter((s) => s.seq >
|
|
768
|
+
const ctxNew = frame.ctx.filter((s) => s.seq > conn.ctxSeq);
|
|
774
769
|
const cmdNew = frame.cmd.filter((s) => s.seq > through);
|
|
775
770
|
if (
|
|
776
|
-
(ctxNew.length > 0 && ctxNew[0]!.seq !==
|
|
771
|
+
(ctxNew.length > 0 && ctxNew[0]!.seq !== conn.ctxSeq + 1) ||
|
|
777
772
|
(cmdNew.length > 0 && cmdNew[0]!.seq !== through + 1)
|
|
778
773
|
) {
|
|
779
774
|
notifyClientRecords(h);
|
|
780
775
|
return {
|
|
781
776
|
type: "refused",
|
|
782
|
-
syn: { syn: { seq: through, ctxSeq:
|
|
777
|
+
syn: { syn: { seq: through, ctxSeq: conn.ctxSeq } },
|
|
783
778
|
};
|
|
784
779
|
}
|
|
785
780
|
if (cmdNew.length > 0) {
|
|
@@ -796,7 +791,7 @@ export const admitFrame = (
|
|
|
796
791
|
syn: {
|
|
797
792
|
syn: {
|
|
798
793
|
seq: through,
|
|
799
|
-
ctxSeq:
|
|
794
|
+
ctxSeq: conn.ctxSeq,
|
|
800
795
|
retry: COMMAND_CAPACITY_RETRY_MS,
|
|
801
796
|
},
|
|
802
797
|
},
|
|
@@ -812,17 +807,17 @@ export const admitFrame = (
|
|
|
812
807
|
);
|
|
813
808
|
return {
|
|
814
809
|
type: "refused",
|
|
815
|
-
syn: { syn: { seq: through, ctxSeq:
|
|
810
|
+
syn: { syn: { seq: through, ctxSeq: conn.ctxSeq, retry } },
|
|
816
811
|
};
|
|
817
812
|
}
|
|
818
813
|
}
|
|
819
|
-
let contextDocuments =
|
|
814
|
+
let contextDocuments = conn.contextDocuments;
|
|
820
815
|
try {
|
|
821
816
|
for (const statement of ctxNew)
|
|
822
817
|
contextDocuments = applyDocumentOps(
|
|
823
818
|
contextDocuments,
|
|
824
819
|
statement.ops,
|
|
825
|
-
selection.protocols,
|
|
820
|
+
conn.selection.protocols,
|
|
826
821
|
true,
|
|
827
822
|
);
|
|
828
823
|
if (
|
|
@@ -833,11 +828,11 @@ export const admitFrame = (
|
|
|
833
828
|
} catch {
|
|
834
829
|
return violation("invalid-context");
|
|
835
830
|
}
|
|
836
|
-
|
|
837
|
-
|
|
831
|
+
conn.contextDocuments = contextDocuments;
|
|
832
|
+
conn.context = (mainDocument(contextDocuments, h.protocol.name)?.value ??
|
|
838
833
|
null) as Record<string, unknown> | null;
|
|
839
|
-
if (ctxNew.length)
|
|
840
|
-
for (const statement of cmdNew) admitCmd(h, rec,
|
|
834
|
+
if (ctxNew.length) conn.ctxSeq = ctxNew.at(-1)!.seq;
|
|
835
|
+
for (const statement of cmdNew) admitCmd(h, rec, conn, statement);
|
|
841
836
|
if (cmdNew.length > 0) {
|
|
842
837
|
// Memory host: admitted means retained — dur rides the admitting flush.
|
|
843
838
|
queueCmd(h, rec.id, { seq: cmdNew.at(-1)!.seq, dur: true });
|
|
@@ -885,10 +880,9 @@ const message = (h: SocketHostState, conn: Conn, data: string) => {
|
|
|
885
880
|
return;
|
|
886
881
|
}
|
|
887
882
|
if (frame.ctx.length === 0 && frame.cmd.length === 0) return;
|
|
888
|
-
const rec = h.clients.get(conn.clientId)!;
|
|
889
883
|
let admission: ReturnType<typeof admitFrame>;
|
|
890
884
|
try {
|
|
891
|
-
admission = admitFrame(h,
|
|
885
|
+
admission = admitFrame(h, conn, frame);
|
|
892
886
|
} catch (error) {
|
|
893
887
|
const reason = violationReason(error);
|
|
894
888
|
if (reason === null) throw error;
|
|
@@ -1203,7 +1197,6 @@ const gcClients = (h: SocketHostState) => {
|
|
|
1203
1197
|
};
|
|
1204
1198
|
|
|
1205
1199
|
export type Registration = {
|
|
1206
|
-
rec: ClientRec;
|
|
1207
1200
|
conn: Conn;
|
|
1208
1201
|
connection: StatewireHostInternal.SocketConnection;
|
|
1209
1202
|
};
|
|
@@ -1265,10 +1258,7 @@ export const register = (
|
|
|
1265
1258
|
const created: ClientRec = {
|
|
1266
1259
|
id: clientId,
|
|
1267
1260
|
lastSeq: 0,
|
|
1268
|
-
ctxSeq: 0,
|
|
1269
1261
|
touched: Date.now(),
|
|
1270
|
-
context: null,
|
|
1271
|
-
contextDocuments: { mode: "mounted", documents: new Map() },
|
|
1272
1262
|
results: new Map(),
|
|
1273
1263
|
inflight: new Map(),
|
|
1274
1264
|
principal: auth,
|
|
@@ -1286,6 +1276,9 @@ export const register = (
|
|
|
1286
1276
|
lastSentAt: 0,
|
|
1287
1277
|
lease: null,
|
|
1288
1278
|
auth,
|
|
1279
|
+
ctxSeq: 0,
|
|
1280
|
+
context: null,
|
|
1281
|
+
contextDocuments: { mode: "mounted", documents: new Map() },
|
|
1289
1282
|
};
|
|
1290
1283
|
h.conns.add(conn);
|
|
1291
1284
|
gcClients(h);
|
|
@@ -1305,7 +1298,7 @@ export const register = (
|
|
|
1305
1298
|
syn: {
|
|
1306
1299
|
...selection,
|
|
1307
1300
|
seq: admittedThrough(rec),
|
|
1308
|
-
ctxSeq:
|
|
1301
|
+
ctxSeq: 0,
|
|
1309
1302
|
...(h.idleWire && { idle: true }),
|
|
1310
1303
|
},
|
|
1311
1304
|
};
|
|
@@ -1317,7 +1310,7 @@ export const register = (
|
|
|
1317
1310
|
message: (data) => message(h, conn, data),
|
|
1318
1311
|
close: () => closeConn(h, conn),
|
|
1319
1312
|
};
|
|
1320
|
-
return {
|
|
1313
|
+
return { conn, connection };
|
|
1321
1314
|
};
|
|
1322
1315
|
|
|
1323
1316
|
const connect = (
|
|
@@ -1367,15 +1360,14 @@ const restoreClientRecords = (
|
|
|
1367
1360
|
records.map((record) => [
|
|
1368
1361
|
record.id,
|
|
1369
1362
|
{
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
documents: new Map(record.contextDocuments ?? []),
|
|
1374
|
-
},
|
|
1363
|
+
id: record.id,
|
|
1364
|
+
lastSeq: record.lastSeq,
|
|
1365
|
+
touched: record.touched,
|
|
1375
1366
|
results: new Map(
|
|
1376
1367
|
record.results.map(({ seq, ts, cmd }) => [seq, { ts, cmd }]),
|
|
1377
1368
|
),
|
|
1378
1369
|
inflight: new Map(),
|
|
1370
|
+
principal: record.principal,
|
|
1379
1371
|
},
|
|
1380
1372
|
]),
|
|
1381
1373
|
);
|
package/src/host-internal.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { StatewireProtocol } from "./wire/protocol";
|
|
2
|
-
import type { StatewireDocuments } from "./wire/documents";
|
|
3
2
|
import type { StatewireHost } from "./host";
|
|
4
3
|
import type { StatewireCmd } from "./client/connection";
|
|
5
4
|
|
|
@@ -49,10 +48,7 @@ export namespace StatewireHostInternal {
|
|
|
49
48
|
export type ClientRecord = {
|
|
50
49
|
id: string;
|
|
51
50
|
lastSeq: number;
|
|
52
|
-
ctxSeq: number;
|
|
53
51
|
touched: number;
|
|
54
|
-
context: Record<string, unknown> | null;
|
|
55
|
-
contextDocuments?: readonly [number, StatewireDocuments.Document][];
|
|
56
52
|
results: readonly { seq: number; ts: number; cmd: StatewireCmd }[];
|
|
57
53
|
principal: unknown;
|
|
58
54
|
};
|
package/src/host.ts
CHANGED
|
@@ -47,7 +47,7 @@ export namespace StatewireHost {
|
|
|
47
47
|
* (in-process dispatch has none — reading it throws), `auth` is the value
|
|
48
48
|
* `authorize` returned for the attach that admitted the command (undefined
|
|
49
49
|
* for a void `authorize` and for in-process dispatch), `context` reads the
|
|
50
|
-
*
|
|
50
|
+
* admitting attach's context live (null until that attach sends one),
|
|
51
51
|
* `isConnected` reports whether the client has an active stream attachment.
|
|
52
52
|
*/
|
|
53
53
|
export type Caller = {
|
package/src/statewire.ts
CHANGED
|
@@ -432,17 +432,10 @@ export namespace StatewireClient {
|
|
|
432
432
|
meta?: unknown;
|
|
433
433
|
};
|
|
434
434
|
|
|
435
|
-
/** The persisted session: the lane identity, the
|
|
435
|
+
/** The persisted session: the lane identity, the cmd clock, and the commands the server has not marked durable. */
|
|
436
436
|
export type SessionRecord = {
|
|
437
437
|
clientId: string;
|
|
438
438
|
nextSeq: number;
|
|
439
|
-
ctxNextSeq: number;
|
|
440
|
-
context?: {
|
|
441
|
-
ops: unknown[];
|
|
442
|
-
pending: { seq: number; ops: unknown[] }[];
|
|
443
|
-
/** Recover the original wire's unpersisted single-document context after the next hello. */
|
|
444
|
-
recover?: "version-1";
|
|
445
|
-
};
|
|
446
439
|
commands: readonly SessionCommand[];
|
|
447
440
|
/** Terminal verdicts parked for the sharer that minted the seq (shared storage only). */
|
|
448
441
|
verdicts?: readonly SessionVerdict[];
|
package/src/storage.ts
CHANGED
|
@@ -4,7 +4,6 @@ import { resource } from "@assistant-ui/tap";
|
|
|
4
4
|
import type { StatewireClient } from "./statewire";
|
|
5
5
|
import type { StatewireProtocol } from "./wire/protocol";
|
|
6
6
|
import { negotiationHeaders, protocolToken } from "./wire/protocol";
|
|
7
|
-
import { applyDocumentOps } from "./wire/documents";
|
|
8
7
|
|
|
9
8
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
10
9
|
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -92,9 +91,7 @@ const isSessionRecord = (
|
|
|
92
91
|
isRecord(value) &&
|
|
93
92
|
typeof value["clientId"] === "string" &&
|
|
94
93
|
value["clientId"].length > 0 &&
|
|
95
|
-
isContext(value["context"]) &&
|
|
96
94
|
isPositiveSafeInteger(value["nextSeq"]) &&
|
|
97
|
-
isPositiveSafeInteger(value["ctxNextSeq"]) &&
|
|
98
95
|
Array.isArray(value["commands"]) &&
|
|
99
96
|
value["commands"].every(isSessionCommand) &&
|
|
100
97
|
(value["verdicts"] === undefined ||
|
|
@@ -123,74 +120,6 @@ const isProducer = (value: unknown) => {
|
|
|
123
120
|
}
|
|
124
121
|
};
|
|
125
122
|
|
|
126
|
-
const isContextOperation = (value: unknown) => {
|
|
127
|
-
if (!isRecord(value)) return false;
|
|
128
|
-
if (
|
|
129
|
-
value["doc"] !== undefined &&
|
|
130
|
-
(typeof value["doc"] !== "number" ||
|
|
131
|
-
!Number.isSafeInteger(value["doc"]) ||
|
|
132
|
-
value["doc"] < 0)
|
|
133
|
-
)
|
|
134
|
-
return false;
|
|
135
|
-
if (value["op"] === "mount") {
|
|
136
|
-
try {
|
|
137
|
-
applyDocumentOps(
|
|
138
|
-
{ mode: "mounted", documents: new Map() },
|
|
139
|
-
[value],
|
|
140
|
-
undefined,
|
|
141
|
-
true,
|
|
142
|
-
);
|
|
143
|
-
return true;
|
|
144
|
-
} catch {
|
|
145
|
-
return false;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
if (value["op"] === "unmount") return true;
|
|
149
|
-
if (!["add", "replace", "remove"].includes(value["op"] as string))
|
|
150
|
-
return false;
|
|
151
|
-
return (
|
|
152
|
-
Array.isArray(value["path"]) &&
|
|
153
|
-
value["path"].every(
|
|
154
|
-
(part) =>
|
|
155
|
-
typeof part === "string" ||
|
|
156
|
-
(typeof part === "number" && Number.isSafeInteger(part) && part >= 0),
|
|
157
|
-
) &&
|
|
158
|
-
(value["op"] === "remove" || "value" in value)
|
|
159
|
-
);
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
const isContext = (value: unknown) => {
|
|
163
|
-
if (value === undefined) return true;
|
|
164
|
-
if (
|
|
165
|
-
!isRecord(value) ||
|
|
166
|
-
!Array.isArray(value["ops"]) ||
|
|
167
|
-
!Array.isArray(value["pending"])
|
|
168
|
-
)
|
|
169
|
-
return false;
|
|
170
|
-
if (value["recover"] !== undefined && value["recover"] !== "version-1")
|
|
171
|
-
return false;
|
|
172
|
-
try {
|
|
173
|
-
const documents = applyDocumentOps(
|
|
174
|
-
{ mode: "mounted", documents: new Map() },
|
|
175
|
-
value["ops"],
|
|
176
|
-
undefined,
|
|
177
|
-
true,
|
|
178
|
-
);
|
|
179
|
-
for (const entry of value["pending"]) {
|
|
180
|
-
if (
|
|
181
|
-
!isRecord(entry) ||
|
|
182
|
-
!isPositiveSafeInteger(entry["seq"]) ||
|
|
183
|
-
!Array.isArray(entry["ops"])
|
|
184
|
-
)
|
|
185
|
-
return false;
|
|
186
|
-
if (!entry["ops"].every(isContextOperation)) return false;
|
|
187
|
-
}
|
|
188
|
-
return documents.mode === "mounted";
|
|
189
|
-
} catch {
|
|
190
|
-
return false;
|
|
191
|
-
}
|
|
192
|
-
};
|
|
193
|
-
|
|
194
123
|
const decode = (
|
|
195
124
|
raw: string | null,
|
|
196
125
|
legacyProtocol: KeyValueSessionStorage.Options["legacyProtocol"],
|
|
@@ -202,14 +131,18 @@ const decode = (
|
|
|
202
131
|
const {
|
|
203
132
|
statewireStorageVersion: format,
|
|
204
133
|
savedAt: _savedAt,
|
|
134
|
+
ctxNextSeq: _ctxNextSeq,
|
|
135
|
+
context: _context,
|
|
205
136
|
...value
|
|
206
137
|
} = parsed as StatewireClient.SessionRecord & {
|
|
207
138
|
statewireStorageVersion?: unknown;
|
|
208
139
|
savedAt?: unknown;
|
|
140
|
+
ctxNextSeq?: unknown;
|
|
141
|
+
context?: unknown;
|
|
209
142
|
};
|
|
210
143
|
if (!isSessionRecord(value))
|
|
211
144
|
throw new Error("statewire: invalid stored session record");
|
|
212
|
-
if (format !== undefined && format !== 1)
|
|
145
|
+
if (format !== undefined && format !== 1 && format !== 2)
|
|
213
146
|
throw new Error("statewire: unsupported session storage version");
|
|
214
147
|
if (legacyProtocol === undefined || format !== undefined) return value;
|
|
215
148
|
const produced = {
|
|
@@ -218,22 +151,6 @@ const decode = (
|
|
|
218
151
|
};
|
|
219
152
|
return {
|
|
220
153
|
...value,
|
|
221
|
-
...(value.context === undefined && value.ctxNextSeq > 1
|
|
222
|
-
? {
|
|
223
|
-
context: {
|
|
224
|
-
ops: [
|
|
225
|
-
{
|
|
226
|
-
op: "mount",
|
|
227
|
-
protocol: legacyProtocol.name,
|
|
228
|
-
main: true,
|
|
229
|
-
value: {},
|
|
230
|
-
},
|
|
231
|
-
],
|
|
232
|
-
pending: [],
|
|
233
|
-
recover: "version-1",
|
|
234
|
-
},
|
|
235
|
-
}
|
|
236
|
-
: {}),
|
|
237
154
|
commands: value.commands.map((command) =>
|
|
238
155
|
command.produced === undefined
|
|
239
156
|
? {
|
|
@@ -279,12 +196,11 @@ const sweep = async (
|
|
|
279
196
|
const format = isRecord(value)
|
|
280
197
|
? value["statewireStorageVersion"]
|
|
281
198
|
: undefined;
|
|
282
|
-
if (format !== undefined && format !== 1) return undefined;
|
|
199
|
+
if (format !== undefined && format !== 1 && format !== 2) return undefined;
|
|
283
200
|
if (
|
|
284
201
|
!isSessionRecord(value) ||
|
|
285
202
|
value.commands.length ||
|
|
286
|
-
value.failed?.length
|
|
287
|
-
value.context?.pending.length
|
|
203
|
+
value.failed?.length
|
|
288
204
|
)
|
|
289
205
|
return undefined;
|
|
290
206
|
const savedAt =
|
|
@@ -502,7 +418,7 @@ const useKeyValueSessionStorage = (
|
|
|
502
418
|
throw new Error("statewire: invalid session record");
|
|
503
419
|
const encoded = JSON.stringify({
|
|
504
420
|
...record,
|
|
505
|
-
statewireStorageVersion:
|
|
421
|
+
statewireStorageVersion: 2,
|
|
506
422
|
savedAt: Date.now(),
|
|
507
423
|
});
|
|
508
424
|
state.record = record;
|
package/src/transports/sse.ts
CHANGED
|
@@ -32,7 +32,7 @@ export type StatewireFrame = {
|
|
|
32
32
|
export type StatewireRemoteProps = {
|
|
33
33
|
beforeAttach?: () => void | Promise<void>;
|
|
34
34
|
clientId: string;
|
|
35
|
-
/** Lanes to ride the attach frame
|
|
35
|
+
/** Lanes to ride the attach frame: the context restatement, plus cmds on no-lease supersession; records the included seqs. */
|
|
36
36
|
takeAttachLanes: () => StatewireFrame | null;
|
|
37
37
|
onHello: (args: { ops: unknown[]; syn: StatewireSyn }) => void;
|
|
38
38
|
onOps: (args: { ops: unknown[]; cmds: StatewireCmd[] }) => void;
|
|
@@ -82,6 +82,7 @@ export const createMockTransport = <TState>(
|
|
|
82
82
|
clientId = id;
|
|
83
83
|
if (!clients.has(id)) clients.set(id, { seq: 0, ctxSeq: 0 });
|
|
84
84
|
const rec = clients.get(id)!;
|
|
85
|
+
rec.ctxSeq = 0;
|
|
85
86
|
const frame = JSON.parse(String(init?.body ?? "{}")) as StatewireFrame;
|
|
86
87
|
admit(id, frame);
|
|
87
88
|
return openSSEStream(
|
|
@@ -93,7 +94,7 @@ export const createMockTransport = <TState>(
|
|
|
93
94
|
ops: [{ op: "mount", protocol, main: true, value: state }],
|
|
94
95
|
syn: {
|
|
95
96
|
seq: rec.seq,
|
|
96
|
-
ctxSeq:
|
|
97
|
+
ctxSeq: 0,
|
|
97
98
|
version: "2026-09-07",
|
|
98
99
|
protocols: [{ name: protocol, version: "2026-09-07" }],
|
|
99
100
|
},
|