statewire 0.18.1 → 0.19.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/dist/client/connection.d.ts +8 -5
- package/dist/client/connection.d.ts.map +1 -1
- package/dist/client/connection.js +24 -13
- package/dist/client/connection.js.map +1 -1
- package/dist/client/session.d.ts +13 -15
- package/dist/client/session.d.ts.map +1 -1
- package/dist/client/session.js +296 -401
- package/dist/client/session.js.map +1 -1
- package/dist/client/timing.d.ts +2 -1
- package/dist/client/timing.d.ts.map +1 -1
- package/dist/client/timing.js +2 -1
- package/dist/client/timing.js.map +1 -1
- package/dist/host/engine.d.ts +2 -0
- package/dist/host/engine.d.ts.map +1 -1
- package/dist/host/engine.js +2 -0
- package/dist/host/engine.js.map +1 -1
- package/dist/host/host.d.ts +3 -1
- package/dist/host/host.d.ts.map +1 -1
- package/dist/host/host.js +38 -7
- package/dist/host/host.js.map +1 -1
- package/dist/host/http.d.ts.map +1 -1
- package/dist/host/http.js +9 -19
- package/dist/host/http.js.map +1 -1
- package/dist/host/socket.d.ts +48 -25
- package/dist/host/socket.d.ts.map +1 -1
- package/dist/host/socket.js +399 -266
- package/dist/host/socket.js.map +1 -1
- package/dist/host/synced.d.ts +9 -0
- package/dist/host/synced.d.ts.map +1 -0
- package/dist/host/synced.js +45 -0
- package/dist/host/synced.js.map +1 -0
- package/dist/host-internal.d.ts +20 -12
- package/dist/host-internal.d.ts.map +1 -1
- package/dist/host.d.ts +33 -9
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +2 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -1
- package/dist/statewire.d.ts +8 -20
- package/dist/statewire.d.ts.map +1 -1
- package/dist/statewire.js +0 -14
- package/dist/statewire.js.map +1 -1
- package/dist/storage.d.ts +2 -5
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +54 -89
- package/dist/storage.js.map +1 -1
- package/dist/transports/remote.d.ts.map +1 -1
- package/dist/transports/remote.js +1 -6
- package/dist/transports/remote.js.map +1 -1
- package/dist/transports/sse.d.ts +2 -5
- package/dist/transports/sse.d.ts.map +1 -1
- package/dist/transports/sse.js.map +1 -1
- package/dist/transports/testing.d.ts +7 -5
- package/dist/transports/testing.d.ts.map +1 -1
- package/dist/transports/testing.js +19 -18
- package/dist/transports/testing.js.map +1 -1
- package/dist/types.d.ts +3 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/wire/limits.d.ts +2 -2
- package/dist/wire/limits.js +2 -2
- package/dist/wire/limits.js.map +1 -1
- package/dist/wire/protocol.d.ts +6 -1
- package/dist/wire/protocol.d.ts.map +1 -1
- package/dist/wire/protocol.js +33 -12
- package/dist/wire/protocol.js.map +1 -1
- package/package.json +1 -1
- package/src/client/connection.ts +46 -33
- package/src/client/session.ts +442 -517
- package/src/client/timing.ts +2 -0
- package/src/host/engine.ts +3 -1
- package/src/host/host.ts +50 -3
- package/src/host/http.ts +23 -31
- package/src/host/socket.ts +560 -339
- package/src/host/synced.ts +60 -0
- package/src/host-internal.ts +21 -7
- package/src/host.ts +40 -8
- package/src/index.ts +1 -0
- package/src/statewire.ts +8 -37
- package/src/storage.ts +90 -157
- package/src/transports/remote.ts +0 -7
- package/src/transports/sse.ts +8 -3
- package/src/transports/testing.ts +34 -18
- package/src/types.ts +3 -5
- package/src/wire/limits.ts +1 -1
- package/src/wire/protocol.ts +40 -11
package/dist/client/session.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { reviveDocumentValue } from "../document.js";
|
|
2
2
|
import { inRange, protocolOffers } from "../wire/protocol.js";
|
|
3
|
-
import { encodeParams, reviveResponse } from "../wire/meta.js";
|
|
4
|
-
import { applyDocumentOps, findDocument, mainDocument, resolveDocumentKey } from "../wire/documents.js";
|
|
5
3
|
import { createMethodProxy } from "../util/methodProxy.js";
|
|
6
4
|
import { StatewireSendError } from "../types.js";
|
|
7
5
|
import { bindCommandsHost } from "../host/host.js";
|
|
6
|
+
import { encodeParams, reviveResponse } from "../wire/meta.js";
|
|
8
7
|
import "../wire/limits.js";
|
|
8
|
+
import { applyDocumentOps, findDocument, mainDocument, resolveDocumentKey } from "../wire/documents.js";
|
|
9
9
|
import { DocumentStorageContext, SessionStorageContext, createMemorySessionStore } from "./sessionStore.js";
|
|
10
|
-
import { backoff } from "./timing.js";
|
|
10
|
+
import { backoff, retryHint } from "./timing.js";
|
|
11
11
|
import { wrapStorageError } from "./storageError.js";
|
|
12
12
|
import { TERMINAL } from "./connection.js";
|
|
13
13
|
import { randomId } from "../util/randomId.js";
|
|
@@ -21,6 +21,21 @@ const rejectedSend = (failure) => {
|
|
|
21
21
|
result.catch(() => {});
|
|
22
22
|
return result;
|
|
23
23
|
};
|
|
24
|
+
const EMPTY_LANE = {
|
|
25
|
+
nextSeq: 1,
|
|
26
|
+
commands: []
|
|
27
|
+
};
|
|
28
|
+
const laneOf = (record, protocol) => record.lanes[protocol] ?? EMPTY_LANE;
|
|
29
|
+
const withLane = (record, protocol, lane) => ({
|
|
30
|
+
...record,
|
|
31
|
+
lanes: {
|
|
32
|
+
...record.lanes,
|
|
33
|
+
[protocol]: lane
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
const isEphemeral = (s, protocol) => s.selection?.protocols.some((p) => p.name === protocol && p.record === "ephemeral") ?? false;
|
|
37
|
+
const durableSeqOf = (s, protocol) => s.durableSeq[protocol] ?? 0;
|
|
38
|
+
const laneEntries = (s, protocol) => s.pending.filter((p) => p.command.protocol === protocol);
|
|
24
39
|
const publish = (s) => {
|
|
25
40
|
if (s.canonical === s.lastPublished) return;
|
|
26
41
|
s.lastPublished = s.canonical;
|
|
@@ -58,26 +73,43 @@ const setRecord = (s, record) => {
|
|
|
58
73
|
throw s.storageError;
|
|
59
74
|
}
|
|
60
75
|
};
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
76
|
+
const toStored = (p) => {
|
|
77
|
+
const { protocol: _protocol, ...command } = p.command;
|
|
78
|
+
return {
|
|
79
|
+
seq: p.seq,
|
|
80
|
+
...command
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
const backlog = (s, protocol) => laneEntries(s, protocol).filter((p) => p.seq > durableSeqOf(s, protocol)).map(toStored);
|
|
84
|
+
const mergeBacklog = (s, protocol) => {
|
|
85
|
+
const own = backlog(s, protocol);
|
|
67
86
|
if (!s.shared) return own;
|
|
68
|
-
const known = new Set(s.
|
|
69
|
-
return [...s.record.commands.filter((c) => !known.has(c.seq) && c.seq > s
|
|
87
|
+
const known = new Set(laneEntries(s, protocol).map((p) => p.seq));
|
|
88
|
+
return [...laneOf(s.record, protocol).commands.filter((c) => !known.has(c.seq) && c.seq > durableSeqOf(s, protocol)), ...own].sort((a, b) => a.seq - b.seq);
|
|
70
89
|
};
|
|
90
|
+
const durableLanes = (s) => [.../* @__PURE__ */ new Set([...Object.keys(s.record.lanes), ...s.pending.map((p) => p.command.protocol).filter((protocol) => !isEphemeral(s, protocol))])];
|
|
71
91
|
const syncBacklogInner = (s) => {
|
|
72
92
|
const parked = s.shared ? s.shared.parked.splice(0) : [];
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
93
|
+
let lanes = s.record.lanes;
|
|
94
|
+
for (const protocol of durableLanes(s)) {
|
|
95
|
+
const lane = laneOf(s.record, protocol);
|
|
96
|
+
const parkedHere = parked.filter((v) => v.protocol === protocol).map((v) => v.verdict);
|
|
97
|
+
const parkedSeqs = new Set(parkedHere.map((v) => v.seq));
|
|
98
|
+
const commands = mergeBacklog(s, protocol).filter((c) => !parkedSeqs.has(c.seq));
|
|
99
|
+
const verdicts = parkedHere.length > 0 ? [...lane.verdicts ?? [], ...parkedHere].slice(-64) : lane.verdicts;
|
|
100
|
+
if (verdicts === lane.verdicts && commands.length === lane.commands.length && commands.every((c, i) => c.seq === lane.commands[i].seq)) continue;
|
|
101
|
+
lanes = {
|
|
102
|
+
...lanes,
|
|
103
|
+
[protocol]: {
|
|
104
|
+
...lane,
|
|
105
|
+
commands,
|
|
106
|
+
...verdicts !== void 0 && { verdicts }
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
if (lanes !== s.record.lanes) setRecord(s, {
|
|
78
111
|
...s.record,
|
|
79
|
-
|
|
80
|
-
...verdicts !== void 0 && { verdicts }
|
|
112
|
+
lanes
|
|
81
113
|
});
|
|
82
114
|
};
|
|
83
115
|
const syncBacklog = (s) => {
|
|
@@ -93,21 +125,7 @@ const transact = (s, fn) => {
|
|
|
93
125
|
const result = shared.txq.then(() => s.store.lock(async () => {
|
|
94
126
|
const stored = s.store.load();
|
|
95
127
|
if (stored !== null && stored.clientId !== s.record.clientId) adoptReset(s, stored);
|
|
96
|
-
else if (stored !== null)
|
|
97
|
-
s.record = stored;
|
|
98
|
-
if (stored.context !== void 0) {
|
|
99
|
-
const posted = new Set(s.ctxPending.filter((p) => p.posted).map((p) => p.seq));
|
|
100
|
-
s.ctxOps = stored.context.ops;
|
|
101
|
-
s.contextDocuments = applyDocumentOps({
|
|
102
|
-
mode: "mounted",
|
|
103
|
-
documents: /* @__PURE__ */ new Map()
|
|
104
|
-
}, stored.context.ops);
|
|
105
|
-
s.ctxPending = stored.context.pending.map((p) => ({
|
|
106
|
-
...p,
|
|
107
|
-
posted: posted.has(p.seq)
|
|
108
|
-
}));
|
|
109
|
-
}
|
|
110
|
-
}
|
|
128
|
+
else if (stored !== null) s.record = stored;
|
|
111
129
|
fn();
|
|
112
130
|
const persistence = s.store.flush?.();
|
|
113
131
|
if (persistence) await persistence;
|
|
@@ -133,12 +151,19 @@ const cover = (s, entry) => {
|
|
|
133
151
|
s.lifecycle?.onApplied(entry.inv);
|
|
134
152
|
s.lifecycle?.onCovered?.(entry.inv);
|
|
135
153
|
};
|
|
136
|
-
const
|
|
137
|
-
const
|
|
154
|
+
const failedCommands = (s) => {
|
|
155
|
+
const parts = Object.values(s.record.lanes).flatMap((lane) => lane.failed === void 0 ? [] : [lane.failed]);
|
|
156
|
+
if (!(parts.length === s.failedParts.length && parts.every((part, i) => part === s.failedParts[i]))) {
|
|
157
|
+
s.failedParts = parts;
|
|
158
|
+
s.failedList = parts.flat();
|
|
159
|
+
}
|
|
160
|
+
return s.failedList;
|
|
161
|
+
};
|
|
138
162
|
const retainFailedInner = (s, entry, failure) => {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
163
|
+
const lane = laneOf(s.record, entry.command.protocol);
|
|
164
|
+
setRecord(s, withLane(s.record, entry.command.protocol, {
|
|
165
|
+
...lane,
|
|
166
|
+
failed: [...lane.failed ?? [], {
|
|
142
167
|
protocol: entry.command.protocol,
|
|
143
168
|
...entry.command.produced !== void 0 && { produced: entry.command.produced },
|
|
144
169
|
command: entry.command.method,
|
|
@@ -148,7 +173,7 @@ const retainFailedInner = (s, entry, failure) => {
|
|
|
148
173
|
ts: entry.inv.ts,
|
|
149
174
|
seq: entry.seq
|
|
150
175
|
}]
|
|
151
|
-
});
|
|
176
|
+
}));
|
|
152
177
|
s.notify();
|
|
153
178
|
};
|
|
154
179
|
const retainFailed = (s, entry, failure) => {
|
|
@@ -167,7 +192,7 @@ const settleEntry = (s, entry, outcome) => {
|
|
|
167
192
|
payload: outcome.payload
|
|
168
193
|
});
|
|
169
194
|
} else {
|
|
170
|
-
if (!((outcome.failure.cause === "unmount" || outcome.failure.cause === "stream-gone") && entry.seq > s.
|
|
195
|
+
if (!((outcome.failure.cause === "unmount" || outcome.failure.cause === "stream-gone") && entry.seq > durableSeqOf(s, entry.command.protocol)) && !isEphemeral(s, entry.command.protocol)) retainFailed(s, entry, outcome.failure);
|
|
171
196
|
entry.reject(new StatewireSendError(outcome.failure));
|
|
172
197
|
s.lifecycle?.onSettle(entry.inv, {
|
|
173
198
|
type: "rejected",
|
|
@@ -180,7 +205,7 @@ const settleAll = (s, detach) => {
|
|
|
180
205
|
const batch = s.pending;
|
|
181
206
|
const detachFailure = (entry) => {
|
|
182
207
|
if (!entry.attempted) {
|
|
183
|
-
const predecessorLost = batch.some((p) => p.seq < entry.seq && p.attempted);
|
|
208
|
+
const predecessorLost = batch.some((p) => p.command.protocol === entry.command.protocol && p.seq < entry.seq && p.attempted);
|
|
184
209
|
return predecessorLost ? {
|
|
185
210
|
fate: "aborted",
|
|
186
211
|
cause,
|
|
@@ -203,20 +228,6 @@ const settleAll = (s, detach) => {
|
|
|
203
228
|
});
|
|
204
229
|
}
|
|
205
230
|
};
|
|
206
|
-
const restateContext = (s) => {
|
|
207
|
-
s.ctxPending = [];
|
|
208
|
-
if (s.ctxOps === void 0) return;
|
|
209
|
-
const seq = s.record.ctxNextSeq;
|
|
210
|
-
setRecord(s, {
|
|
211
|
-
...s.record,
|
|
212
|
-
ctxNextSeq: seq + 1
|
|
213
|
-
});
|
|
214
|
-
s.ctxPending.push({
|
|
215
|
-
seq,
|
|
216
|
-
ops: s.ctxOps,
|
|
217
|
-
posted: false
|
|
218
|
-
});
|
|
219
|
-
};
|
|
220
231
|
const settleLost = (s, entries) => {
|
|
221
232
|
for (const entry of entries) {
|
|
222
233
|
s.lifecycle?.onCovered?.(entry.inv);
|
|
@@ -229,32 +240,45 @@ const settleLost = (s, entries) => {
|
|
|
229
240
|
});
|
|
230
241
|
}
|
|
231
242
|
};
|
|
243
|
+
const requeue = (s, survivors) => {
|
|
244
|
+
for (const old of survivors) {
|
|
245
|
+
s.lifecycle?.onCovered?.(old.inv);
|
|
246
|
+
enqueueInner(s, old.command, old.inv.params, old.resolve, old.reject);
|
|
247
|
+
}
|
|
248
|
+
};
|
|
232
249
|
const rebaseRecord = (s, adopt) => {
|
|
233
250
|
const held = [...s.pending];
|
|
234
251
|
const survivors = held.filter((p) => !p.attempted && !p.applied);
|
|
235
252
|
settleLost(s, held.filter((p) => p.attempted || p.applied));
|
|
236
253
|
s.pending = [];
|
|
237
|
-
s.durableSeq =
|
|
238
|
-
const
|
|
254
|
+
s.durableSeq = {};
|
|
255
|
+
const previous = s.record.lanes;
|
|
239
256
|
adopt();
|
|
240
|
-
|
|
257
|
+
let lanes = s.record.lanes;
|
|
258
|
+
for (const [protocol, lane] of Object.entries(previous)) {
|
|
259
|
+
const failed = lane.failed;
|
|
260
|
+
if (failed === void 0 || laneOf(s.record, protocol).failed === failed) continue;
|
|
261
|
+
const current = laneOf(s.record, protocol);
|
|
262
|
+
lanes = {
|
|
263
|
+
...lanes,
|
|
264
|
+
[protocol]: {
|
|
265
|
+
...current,
|
|
266
|
+
failed: [...current.failed ?? [], ...failed]
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
if (lanes !== s.record.lanes) setRecord(s, {
|
|
241
271
|
...s.record,
|
|
242
|
-
|
|
272
|
+
lanes
|
|
243
273
|
});
|
|
244
|
-
|
|
245
|
-
s.lifecycle?.onCovered?.(old.inv);
|
|
246
|
-
enqueueInner(s, old.command, old.inv.params, old.resolve, old.reject);
|
|
247
|
-
}
|
|
248
|
-
restateContext(s);
|
|
274
|
+
requeue(s, survivors);
|
|
249
275
|
publish(s);
|
|
250
276
|
wake(s);
|
|
251
277
|
pump(s);
|
|
252
278
|
};
|
|
253
279
|
const resetRecord = (s, clientId) => rebaseRecord(s, () => setRecord(s, {
|
|
254
280
|
clientId,
|
|
255
|
-
|
|
256
|
-
ctxNextSeq: 1,
|
|
257
|
-
commands: []
|
|
281
|
+
lanes: {}
|
|
258
282
|
}));
|
|
259
283
|
const adoptReset = (s, stored) => rebaseRecord(s, () => {
|
|
260
284
|
s.record = stored;
|
|
@@ -264,6 +288,14 @@ const ephemeralDetach = (s) => {
|
|
|
264
288
|
s.sessionKnown = false;
|
|
265
289
|
resetRecord(s, randomId());
|
|
266
290
|
};
|
|
291
|
+
const detachEphemeralLanes = (s) => {
|
|
292
|
+
const held = s.pending.filter((p) => isEphemeral(s, p.command.protocol));
|
|
293
|
+
if (held.length === 0) return;
|
|
294
|
+
s.pending = s.pending.filter((p) => !held.includes(p));
|
|
295
|
+
settleLost(s, held.filter((p) => p.attempted));
|
|
296
|
+
requeue(s, held.filter((p) => !p.attempted));
|
|
297
|
+
publish(s);
|
|
298
|
+
};
|
|
267
299
|
const rejectedFailure = (cmd) => {
|
|
268
300
|
return {
|
|
269
301
|
fate: "rejected",
|
|
@@ -308,50 +340,72 @@ const outcomeOf = (cmd) => {
|
|
|
308
340
|
default: return protocolError(`unknown cmd type: ${String(cmd.type)}`);
|
|
309
341
|
}
|
|
310
342
|
};
|
|
343
|
+
const settleVerdict = (s, entry, cmd) => {
|
|
344
|
+
const outcome = outcomeOf(cmd);
|
|
345
|
+
if (outcome.ok) cover(s, entry);
|
|
346
|
+
else s.lifecycle?.onCovered?.(entry.inv);
|
|
347
|
+
settleEntry(s, entry, outcome);
|
|
348
|
+
};
|
|
311
349
|
const TERMINAL_CMD_TYPES = new Set(TERMINAL);
|
|
312
350
|
const parkVerdict = (s, cmd) => {
|
|
313
351
|
if (!s.shared || !TERMINAL_CMD_TYPES.has(cmd.type)) return;
|
|
352
|
+
if (isEphemeral(s, cmd.protocol)) return;
|
|
314
353
|
s.shared.parked.push({
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
354
|
+
protocol: cmd.protocol,
|
|
355
|
+
verdict: {
|
|
356
|
+
seq: cmd.seq,
|
|
357
|
+
type: cmd.type,
|
|
358
|
+
...cmd.code !== void 0 && { code: cmd.code },
|
|
359
|
+
...cmd.message !== void 0 && { message: cmd.message },
|
|
360
|
+
...cmd.payload !== void 0 && { payload: cmd.payload },
|
|
361
|
+
...cmd.meta !== void 0 && { meta: cmd.meta }
|
|
362
|
+
}
|
|
321
363
|
});
|
|
322
364
|
};
|
|
323
365
|
const handleCmd = (s, cmd) => {
|
|
324
|
-
if (
|
|
325
|
-
|
|
326
|
-
s.durableSeq =
|
|
327
|
-
for (const entry of s.
|
|
366
|
+
if (cmd.dur === true && !isEphemeral(s, cmd.protocol)) {
|
|
367
|
+
const watermark = Math.max(durableSeqOf(s, cmd.protocol), cmd.seq);
|
|
368
|
+
s.durableSeq[cmd.protocol] = watermark;
|
|
369
|
+
for (const entry of laneEntries(s, cmd.protocol)) if (entry.seq <= watermark) entry.posted = true;
|
|
328
370
|
}
|
|
329
371
|
if (cmd.type === void 0) return;
|
|
330
|
-
const entry = s.pending.find((p) => p.seq === cmd.seq);
|
|
372
|
+
const entry = s.pending.find((p) => p.command.protocol === cmd.protocol && p.seq === cmd.seq);
|
|
331
373
|
if (cmd.type === "applied") {
|
|
332
374
|
if (entry) cover(s, entry);
|
|
333
375
|
return;
|
|
334
376
|
}
|
|
335
377
|
if (!entry) return;
|
|
336
378
|
parkVerdict(s, cmd);
|
|
337
|
-
|
|
338
|
-
settleEntry(s, entry, outcomeOf(cmd));
|
|
379
|
+
settleVerdict(s, entry, cmd);
|
|
339
380
|
};
|
|
340
381
|
const consumeVerdicts = (s) => {
|
|
341
|
-
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
382
|
+
let lanes = s.record.lanes;
|
|
383
|
+
for (const [protocol, lane] of Object.entries(s.record.lanes)) {
|
|
384
|
+
const verdicts = lane.verdicts ?? [];
|
|
385
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
386
|
+
for (const verdict of verdicts) {
|
|
387
|
+
const entry = laneEntries(s, protocol).find((p) => p.seq === verdict.seq);
|
|
388
|
+
if (!entry) continue;
|
|
389
|
+
consumed.add(verdict.seq);
|
|
390
|
+
settleVerdict(s, entry, {
|
|
391
|
+
...verdict,
|
|
392
|
+
protocol
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
if (consumed.size === 0) continue;
|
|
396
|
+
lanes = {
|
|
397
|
+
...lanes,
|
|
398
|
+
[protocol]: {
|
|
399
|
+
...lane,
|
|
400
|
+
commands: lane.commands.filter((c) => !consumed.has(c.seq)),
|
|
401
|
+
verdicts: verdicts.filter((v) => !consumed.has(v.seq))
|
|
402
|
+
}
|
|
403
|
+
};
|
|
349
404
|
}
|
|
350
|
-
if (
|
|
405
|
+
if (lanes === s.record.lanes) return;
|
|
351
406
|
setRecord(s, {
|
|
352
407
|
...s.record,
|
|
353
|
-
|
|
354
|
-
verdicts: verdicts.filter((v) => !consumed.has(v.seq))
|
|
408
|
+
lanes
|
|
355
409
|
});
|
|
356
410
|
publish(s);
|
|
357
411
|
wake(s);
|
|
@@ -389,69 +443,36 @@ const publishIngested = (s) => {
|
|
|
389
443
|
for (const listener of s.documentListeners) listener();
|
|
390
444
|
wake(s);
|
|
391
445
|
};
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
const document = s.contextDocuments.documents.get(0);
|
|
395
|
-
if (s.contextDocuments.documents.size !== 1 || !document?.main) throw new Error("statewire: version-1 context recovery requires one main document at doc 0");
|
|
396
|
-
const encoded = encodeDocument(document.value);
|
|
397
|
-
const ops = [{
|
|
398
|
-
...ctxWm === 0 ? {
|
|
399
|
-
op: "mount",
|
|
400
|
-
protocol: document.protocol,
|
|
401
|
-
main: true
|
|
402
|
-
} : {
|
|
403
|
-
op: "replace",
|
|
404
|
-
path: []
|
|
405
|
-
},
|
|
406
|
-
...encoded
|
|
407
|
-
}];
|
|
408
|
-
s.ctxPending = [{
|
|
409
|
-
seq: ctxWm + 1,
|
|
410
|
-
ops,
|
|
411
|
-
posted: false
|
|
412
|
-
}];
|
|
413
|
-
setRecord(s, {
|
|
414
|
-
...s.record,
|
|
415
|
-
ctxNextSeq: ctxWm + 2,
|
|
416
|
-
context: {
|
|
417
|
-
ops: contextSnapshot(s.contextDocuments),
|
|
418
|
-
pending: s.ctxPending
|
|
419
|
-
}
|
|
420
|
-
});
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
423
|
-
if (!(s.ctxPending.every((c, i) => c.seq === ctxWm + 1 + i) && s.record.ctxNextSeq === ctxWm + s.ctxPending.length + 1)) {
|
|
424
|
-
if (ctxWm > 0) throw new Error("statewire: context history is missing; use a new client id");
|
|
425
|
-
setRecord(s, {
|
|
426
|
-
...s.record,
|
|
427
|
-
ctxNextSeq: ctxWm + 1
|
|
428
|
-
});
|
|
429
|
-
restateContext(s);
|
|
430
|
-
}
|
|
431
|
-
};
|
|
432
|
-
const sessionLostInner = (s, seqWm, ctxWm) => {
|
|
433
|
-
const held = [...s.pending];
|
|
446
|
+
const sessionLostInner = (s, protocol, seqWm) => {
|
|
447
|
+
const held = laneEntries(s, protocol);
|
|
434
448
|
settleLost(s, held);
|
|
435
|
-
s.
|
|
436
|
-
s.
|
|
437
|
-
setRecord(s, {
|
|
438
|
-
clientId: s.record.clientId,
|
|
449
|
+
s.durableSeq[protocol] = seqWm;
|
|
450
|
+
const failed = laneOf(s.record, protocol).failed;
|
|
451
|
+
setRecord(s, withLane(s.record, protocol, {
|
|
439
452
|
nextSeq: seqWm + 1,
|
|
440
|
-
ctxNextSeq: s.record.ctxNextSeq,
|
|
441
453
|
commands: [],
|
|
442
|
-
...
|
|
443
|
-
});
|
|
444
|
-
snapCtxClock(s, ctxWm);
|
|
454
|
+
...failed !== void 0 && { failed }
|
|
455
|
+
}));
|
|
445
456
|
if (held.length > 0 && s.onSessionLost?.(held.map((p) => p.inv)) === "requeue") for (const old of held) enqueueInner(s, old.command, old.inv.params, () => {}, () => {});
|
|
446
457
|
publish(s);
|
|
447
458
|
wake(s);
|
|
448
459
|
};
|
|
449
|
-
const
|
|
450
|
-
if (syn.
|
|
451
|
-
|
|
460
|
+
const selectionOf = (syn) => {
|
|
461
|
+
if (syn.version === void 0 || syn.protocols === void 0) return void 0;
|
|
462
|
+
return {
|
|
452
463
|
version: syn.version,
|
|
453
|
-
protocols: syn.protocols
|
|
464
|
+
protocols: syn.protocols.map(({ name, version, record }) => {
|
|
465
|
+
if (version === void 0) return protocolError(`hello lane ${name} has no version`);
|
|
466
|
+
return {
|
|
467
|
+
name,
|
|
468
|
+
version,
|
|
469
|
+
...record !== void 0 && { record }
|
|
470
|
+
};
|
|
471
|
+
})
|
|
454
472
|
};
|
|
473
|
+
};
|
|
474
|
+
const handleHello = (s, { ops, syn }) => {
|
|
475
|
+
s.selection = selectionOf(syn);
|
|
455
476
|
s.documents = {
|
|
456
477
|
mode: null,
|
|
457
478
|
documents: /* @__PURE__ */ new Map()
|
|
@@ -461,19 +482,16 @@ const handleHello = (s, { ops, syn }) => {
|
|
|
461
482
|
s.attachEpoch += 1;
|
|
462
483
|
s.frameFailures = 0;
|
|
463
484
|
s.resumeAt = 0;
|
|
464
|
-
|
|
485
|
+
const lanes = syn.protocols ?? [];
|
|
486
|
+
s.ephemeral = lanes.length > 0 && lanes.every((lane) => lane.record === "ephemeral");
|
|
465
487
|
if (s.ephemeral) s.deactivated = true;
|
|
466
|
-
const
|
|
467
|
-
s.
|
|
488
|
+
const attached = s.attachEntries;
|
|
489
|
+
s.attachEntries = /* @__PURE__ */ new Set();
|
|
468
490
|
const reconcile = () => {
|
|
469
491
|
if (s.shared) adoptStored(s);
|
|
470
|
-
s.
|
|
471
|
-
|
|
472
|
-
const
|
|
473
|
-
s.ctxPending = s.ctxPending.filter((c) => c.seq > ctxWatermark);
|
|
474
|
-
for (const c of s.ctxPending) c.posted = attachCtx.has(c.seq);
|
|
475
|
-
snapCtxClock(s, ctxWatermark);
|
|
476
|
-
if (s.ctxOps !== void 0) persistContext(s);
|
|
492
|
+
for (const lane of lanes) if (lane.record === "ephemeral") s.ephemeralSeqs[lane.name] = Math.max(s.ephemeralSeqs[lane.name] ?? 0, ...laneEntries(s, lane.name).map((p) => p.seq));
|
|
493
|
+
else s.durableSeq[lane.name] = lane.seq ?? 0;
|
|
494
|
+
for (const entry of s.pending) entry.posted = attached.has(entry);
|
|
477
495
|
syncBacklogInner(s);
|
|
478
496
|
s.sessionKnown = true;
|
|
479
497
|
};
|
|
@@ -495,30 +513,25 @@ const handleOps = (s, { ops, cmds }) => {
|
|
|
495
513
|
publishIngested(s);
|
|
496
514
|
};
|
|
497
515
|
const handleRefusal = (s, syn) => {
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
const watermark = Math.max(seq, s
|
|
501
|
-
for (const entry of s
|
|
502
|
-
}
|
|
503
|
-
if (typeof ctxSeq === "number") {
|
|
504
|
-
s.ctxPending = s.ctxPending.filter((c) => c.seq > ctxSeq);
|
|
505
|
-
for (const c of s.ctxPending) c.posted = false;
|
|
506
|
-
if (s.ctxOps !== void 0) persistContext(s);
|
|
516
|
+
const lanes = (syn.protocols ?? []).filter((lane) => lane.seq !== void 0);
|
|
517
|
+
for (const { name, seq } of lanes) {
|
|
518
|
+
const watermark = isEphemeral(s, name) ? seq : Math.max(seq, durableSeqOf(s, name));
|
|
519
|
+
for (const entry of laneEntries(s, name)) entry.posted = entry.seq <= watermark;
|
|
507
520
|
}
|
|
508
521
|
if (typeof syn.retry === "number") {
|
|
509
|
-
s.resumeAt = Date.now() + syn.retry;
|
|
522
|
+
s.resumeAt = Date.now() + retryHint(syn.retry);
|
|
510
523
|
wake(s);
|
|
511
524
|
pump(s);
|
|
512
525
|
return;
|
|
513
526
|
}
|
|
514
|
-
const
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
527
|
+
const gaps = lanes.filter(({ name, seq }) => {
|
|
528
|
+
const first = laneEntries(s, name).find((p) => !p.posted);
|
|
529
|
+
return first !== void 0 && first.seq > Math.max(seq, durableSeqOf(s, name)) + 1;
|
|
530
|
+
});
|
|
531
|
+
for (const { name, seq } of gaps) if (isEphemeral(s, name)) protocolError(`gap on ephemeral lane ${name} after watermark ${seq}`);
|
|
532
|
+
if (gaps.length > 0) {
|
|
519
533
|
transactBackground(s, () => {
|
|
520
|
-
|
|
521
|
-
else snapCtxClock(s, ctxSeq);
|
|
534
|
+
for (const { name, seq } of gaps) sessionLostInner(s, name, seq);
|
|
522
535
|
}, () => {
|
|
523
536
|
wake(s);
|
|
524
537
|
pump(s);
|
|
@@ -530,7 +543,9 @@ const handleRefusal = (s, syn) => {
|
|
|
530
543
|
};
|
|
531
544
|
const handleDetach = (s) => {
|
|
532
545
|
if (s.attach === "attached") s.attach = "detached";
|
|
546
|
+
s.ephemeralSeqs = {};
|
|
533
547
|
if (s.ephemeral) ephemeralDetach(s);
|
|
548
|
+
else detachEphemeralLanes(s);
|
|
534
549
|
wake(s);
|
|
535
550
|
};
|
|
536
551
|
const handleFinish = (s, { reason }) => {
|
|
@@ -541,51 +556,41 @@ const handleFinish = (s, { reason }) => {
|
|
|
541
556
|
wake(s);
|
|
542
557
|
};
|
|
543
558
|
const isFinished = (s) => s.attach === "finished";
|
|
544
|
-
const buildLanes = (s,
|
|
545
|
-
const
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
if (persisted && (s.persistedRecord?.clientId !== s.record.clientId || entry.seq >= s.persistedRecord.ctxNextSeq)) break;
|
|
549
|
-
if (ctxEntries.length >= 256) break;
|
|
550
|
-
if (ctxEntries.length > 0 && entry.seq !== ctxEntries.at(-1).seq + 1) break;
|
|
551
|
-
ctxEntries.push(entry);
|
|
552
|
-
}
|
|
553
|
-
const cmdEntries = [];
|
|
559
|
+
const buildLanes = (s, cmdGate = "all") => {
|
|
560
|
+
const entries = [];
|
|
561
|
+
const last = {};
|
|
562
|
+
const stopped = /* @__PURE__ */ new Set();
|
|
554
563
|
for (const entry of s.pending) {
|
|
555
|
-
if (
|
|
556
|
-
|
|
557
|
-
if (
|
|
558
|
-
if (
|
|
559
|
-
|
|
564
|
+
if (entries.length >= 256) break;
|
|
565
|
+
const protocol = entry.command.protocol;
|
|
566
|
+
if (entry.posted || stopped.has(protocol)) continue;
|
|
567
|
+
if (!isEphemeral(s, protocol) && (cmdGate === "none" || cmdGate === "persisted" && (s.persistedRecord?.clientId !== s.record.clientId || entry.seq >= laneOf(s.persistedRecord, protocol).nextSeq)) || protocol in last && entry.seq !== last[protocol] + 1) {
|
|
568
|
+
stopped.add(protocol);
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
last[protocol] = entry.seq;
|
|
572
|
+
entries.push(entry);
|
|
560
573
|
}
|
|
561
|
-
if (
|
|
574
|
+
if (entries.length === 0) return null;
|
|
562
575
|
return {
|
|
563
|
-
frame: {
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
...command,
|
|
572
|
-
seq: p.seq
|
|
573
|
-
};
|
|
574
|
-
}) }
|
|
575
|
-
},
|
|
576
|
-
cmdEntries,
|
|
577
|
-
ctxEntries
|
|
576
|
+
frame: { cmd: entries.map((p) => {
|
|
577
|
+
const { produced: _produced, ...command } = p.command;
|
|
578
|
+
return {
|
|
579
|
+
...command,
|
|
580
|
+
seq: p.seq
|
|
581
|
+
};
|
|
582
|
+
}) },
|
|
583
|
+
entries
|
|
578
584
|
};
|
|
579
585
|
};
|
|
580
586
|
const takeAttachLanes = (s) => {
|
|
581
|
-
s.
|
|
587
|
+
s.attachEntries = /* @__PURE__ */ new Set();
|
|
582
588
|
const wanted = s.lanesOnAttach;
|
|
583
589
|
s.lanesOnAttach = false;
|
|
584
|
-
|
|
585
|
-
const lanes = buildLanes(s, true);
|
|
590
|
+
const lanes = buildLanes(s, wanted && s.sessionKnown ? "persisted" : "none");
|
|
586
591
|
if (!lanes) return null;
|
|
587
|
-
s.
|
|
588
|
-
for (const entry of lanes.
|
|
592
|
+
s.attachEntries = new Set(lanes.entries);
|
|
593
|
+
for (const entry of lanes.entries) entry.attempted = true;
|
|
589
594
|
return lanes.frame;
|
|
590
595
|
};
|
|
591
596
|
const persistBeforeSend = (s) => {
|
|
@@ -629,7 +634,7 @@ const pump = async (s) => {
|
|
|
629
634
|
if (persistence) await persistence;
|
|
630
635
|
if (!s.running || isFinished(s)) return;
|
|
631
636
|
if (epoch !== s.attachEpoch || s.attach !== "attached") continue;
|
|
632
|
-
for (const entry of lanes.
|
|
637
|
+
for (const entry of lanes.entries) entry.attempted = true;
|
|
633
638
|
let result;
|
|
634
639
|
try {
|
|
635
640
|
const send = () => transport.send(lanes.frame);
|
|
@@ -644,11 +649,10 @@ const pump = async (s) => {
|
|
|
644
649
|
s.frameFailures = 0;
|
|
645
650
|
if (result.type === "ok") {
|
|
646
651
|
if (epoch === s.attachEpoch) {
|
|
647
|
-
for (const entry of lanes.
|
|
652
|
+
for (const entry of lanes.entries) if (s.pending.includes(entry)) {
|
|
648
653
|
entry.posted = true;
|
|
649
654
|
s.lifecycle?.onPosted?.(entry.inv);
|
|
650
655
|
}
|
|
651
|
-
for (const entry of lanes.ctxEntries) entry.posted = true;
|
|
652
656
|
}
|
|
653
657
|
} else if (result.type === "refusal") {
|
|
654
658
|
if (epoch === s.attachEpoch) handleRefusal(s, result.syn);
|
|
@@ -715,12 +719,14 @@ const setTransport = (s, transport) => {
|
|
|
715
719
|
wake(s);
|
|
716
720
|
};
|
|
717
721
|
const enqueueInner = (s, command, params, resolve, reject) => {
|
|
718
|
-
const
|
|
722
|
+
const protocol = command.protocol;
|
|
723
|
+
const ephemeral = isEphemeral(s, protocol);
|
|
724
|
+
const seq = ephemeral ? (s.ephemeralSeqs[protocol] ?? 0) + 1 : Math.max(laneOf(s.record, protocol).nextSeq, durableSeqOf(s, protocol) + 1);
|
|
719
725
|
const entry = {
|
|
720
726
|
seq,
|
|
721
727
|
command,
|
|
722
728
|
inv: {
|
|
723
|
-
protocol
|
|
729
|
+
protocol,
|
|
724
730
|
method: command.method,
|
|
725
731
|
params,
|
|
726
732
|
ts: Date.now(),
|
|
@@ -734,11 +740,12 @@ const enqueueInner = (s, command, params, resolve, reject) => {
|
|
|
734
740
|
reject
|
|
735
741
|
};
|
|
736
742
|
s.pending.push(entry);
|
|
737
|
-
|
|
738
|
-
|
|
743
|
+
if (ephemeral) s.ephemeralSeqs[protocol] = seq;
|
|
744
|
+
else setRecord(s, withLane(s.record, protocol, {
|
|
745
|
+
...laneOf(s.record, protocol),
|
|
739
746
|
nextSeq: seq + 1,
|
|
740
|
-
commands: mergeBacklog(s)
|
|
741
|
-
});
|
|
747
|
+
commands: mergeBacklog(s, protocol)
|
|
748
|
+
}));
|
|
742
749
|
s.lifecycle?.onStart(entry.inv);
|
|
743
750
|
};
|
|
744
751
|
const enqueue = (s, command, params, resolve, reject) => {
|
|
@@ -769,37 +776,47 @@ const enqueue = (s, command, params, resolve, reject) => {
|
|
|
769
776
|
pump(s);
|
|
770
777
|
}, reject);
|
|
771
778
|
};
|
|
779
|
+
const offered = (s, protocol) => {
|
|
780
|
+
const offers = protocolOffers(s.options);
|
|
781
|
+
return offers === "*" || offers.some((offer) => offer.name === protocol);
|
|
782
|
+
};
|
|
772
783
|
const adoptStored = (s) => {
|
|
773
|
-
|
|
774
|
-
const
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
const
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
seq,
|
|
782
|
-
command,
|
|
783
|
-
inv: {
|
|
784
|
-
protocol: command.protocol,
|
|
785
|
-
method: command.method,
|
|
786
|
-
params,
|
|
787
|
-
ts: Date.now(),
|
|
784
|
+
let adopted = false;
|
|
785
|
+
for (const [protocol, lane] of Object.entries(s.record.lanes)) {
|
|
786
|
+
if (!offered(s, protocol)) continue;
|
|
787
|
+
const known = new Set(laneEntries(s, protocol).map((p) => p.seq));
|
|
788
|
+
for (const { seq, ...command } of lane.commands) {
|
|
789
|
+
if (known.has(seq)) continue;
|
|
790
|
+
const params = command.meta === void 0 ? command.params : reviveDocumentValue([...command.params], command.meta);
|
|
791
|
+
const entry = {
|
|
788
792
|
seq,
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
793
|
+
command: {
|
|
794
|
+
protocol,
|
|
795
|
+
...command
|
|
796
|
+
},
|
|
797
|
+
inv: {
|
|
798
|
+
protocol,
|
|
799
|
+
method: command.method,
|
|
800
|
+
params,
|
|
801
|
+
ts: Date.now(),
|
|
802
|
+
seq,
|
|
803
|
+
clientId: s.record.clientId
|
|
804
|
+
},
|
|
805
|
+
attempted: true,
|
|
806
|
+
posted: false,
|
|
807
|
+
applied: false,
|
|
808
|
+
resolve: () => {},
|
|
809
|
+
reject: () => {}
|
|
810
|
+
};
|
|
811
|
+
s.pending.push(entry);
|
|
812
|
+
s.lifecycle?.onStart(entry.inv);
|
|
813
|
+
adopted = true;
|
|
814
|
+
}
|
|
799
815
|
}
|
|
800
|
-
s.pending.sort((a, b) => a.seq - b.seq);
|
|
816
|
+
if (adopted) s.pending.sort((a, b) => a.seq - b.seq);
|
|
801
817
|
};
|
|
802
818
|
const sendCommand = (s, method, params, protocol = s.options.protocol ?? "default") => {
|
|
819
|
+
if (!(s.selection ? s.selection.protocols.some((p) => p.name === protocol) : offered(s, protocol))) throw new Error(`statewire: protocol "${protocol}" is not negotiated`);
|
|
803
820
|
if (!s.hydrated) {
|
|
804
821
|
const result = s.ready.then(() => sendCommand(s, method, params, protocol));
|
|
805
822
|
result.catch(() => {});
|
|
@@ -823,7 +840,7 @@ const sendCommand = (s, method, params, protocol = s.options.protocol ?? "defaul
|
|
|
823
840
|
enqueue(s, {
|
|
824
841
|
protocol,
|
|
825
842
|
produced: {
|
|
826
|
-
wire: s.options.wireVersion ?? { version: "2026-09-
|
|
843
|
+
wire: s.options.wireVersion ?? { version: "2026-09-13" },
|
|
827
844
|
protocols: protocolOffers(s.options) === "*" && s.selection ? s.selection.protocols : protocolOffers(s.options)
|
|
828
845
|
},
|
|
829
846
|
method,
|
|
@@ -834,16 +851,16 @@ const sendCommand = (s, method, params, protocol = s.options.protocol ?? "defaul
|
|
|
834
851
|
result.catch(() => {});
|
|
835
852
|
return result;
|
|
836
853
|
};
|
|
837
|
-
const findFailed = (s, entry) => (s.record.failed ?? []).find((f) => f.seq === entry.seq && f.ts === entry.ts && f.command === entry.command);
|
|
854
|
+
const findFailed = (s, entry) => (laneOf(s.record, entry.protocol).failed ?? []).find((f) => f.seq === entry.seq && f.ts === entry.ts && f.command === entry.command);
|
|
838
855
|
const unknownFailed = (entry) => /* @__PURE__ */ new Error(`statewire: unknown failed command "${entry.command}"`);
|
|
839
856
|
const takeFailed = (s, entry) => {
|
|
840
|
-
const
|
|
857
|
+
const lane = laneOf(s.record, entry.protocol);
|
|
841
858
|
const found = findFailed(s, entry);
|
|
842
859
|
if (found === void 0) throw unknownFailed(entry);
|
|
843
|
-
setRecord(s, {
|
|
844
|
-
...
|
|
845
|
-
failed: failed.filter((f) => f !== found)
|
|
846
|
-
});
|
|
860
|
+
setRecord(s, withLane(s.record, entry.protocol, {
|
|
861
|
+
...lane,
|
|
862
|
+
failed: (lane.failed ?? []).filter((f) => f !== found)
|
|
863
|
+
}));
|
|
847
864
|
s.notify();
|
|
848
865
|
return found;
|
|
849
866
|
};
|
|
@@ -913,102 +930,6 @@ const retryFailed = (s, entry) => {
|
|
|
913
930
|
result.catch(() => {});
|
|
914
931
|
return result;
|
|
915
932
|
};
|
|
916
|
-
const contextSnapshot = (documents) => [...documents.documents].map(([doc, d]) => {
|
|
917
|
-
const encoded = encodeDocument(d.value);
|
|
918
|
-
return {
|
|
919
|
-
op: "mount",
|
|
920
|
-
...doc === 0 ? {} : { doc },
|
|
921
|
-
protocol: d.protocol,
|
|
922
|
-
...d.main ? { main: true } : {},
|
|
923
|
-
...d.key === null ? {} : { key: d.key },
|
|
924
|
-
value: encoded.value,
|
|
925
|
-
...encoded.meta === void 0 ? {} : { meta: encoded.meta }
|
|
926
|
-
};
|
|
927
|
-
});
|
|
928
|
-
const persistContext = (s) => {
|
|
929
|
-
const ops = contextSnapshot(s.contextDocuments);
|
|
930
|
-
s.ctxOps = ops;
|
|
931
|
-
setRecord(s, {
|
|
932
|
-
...s.record,
|
|
933
|
-
context: {
|
|
934
|
-
ops,
|
|
935
|
-
...s.record.context?.recover === void 0 ? {} : { recover: s.record.context.recover },
|
|
936
|
-
pending: s.ctxPending.map(({ seq, ops }) => ({
|
|
937
|
-
seq,
|
|
938
|
-
ops
|
|
939
|
-
}))
|
|
940
|
-
}
|
|
941
|
-
});
|
|
942
|
-
};
|
|
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
|
-
}
|
|
952
|
-
if (!s.running || isFinished(s)) return;
|
|
953
|
-
const submit = () => {
|
|
954
|
-
if (!s.running || isFinished(s)) return;
|
|
955
|
-
const previous = s.record.context === void 0 ? s.contextDocuments : applyDocumentOps({
|
|
956
|
-
mode: "mounted",
|
|
957
|
-
documents: /* @__PURE__ */ new Map()
|
|
958
|
-
}, s.record.context.ops);
|
|
959
|
-
const operations = build(previous);
|
|
960
|
-
if (!operations.length) throw new Error("statewire: context operations must not be empty");
|
|
961
|
-
const offers = protocolOffers(s.options);
|
|
962
|
-
const protocols = s.selection?.protocols ?? (offers === "*" ? void 0 : offers);
|
|
963
|
-
s.contextDocuments = applyDocumentOps(previous, operations, protocols, true);
|
|
964
|
-
const seq = s.record.ctxNextSeq;
|
|
965
|
-
s.ctxPending = [...s.ctxPending, {
|
|
966
|
-
seq,
|
|
967
|
-
ops: operations.map((op) => {
|
|
968
|
-
const value = op;
|
|
969
|
-
if (value["doc"] !== 0) return value;
|
|
970
|
-
const { doc: _doc, ...rest } = value;
|
|
971
|
-
return rest;
|
|
972
|
-
}),
|
|
973
|
-
posted: false
|
|
974
|
-
}];
|
|
975
|
-
s.record = {
|
|
976
|
-
...s.record,
|
|
977
|
-
ctxNextSeq: seq + 1
|
|
978
|
-
};
|
|
979
|
-
persistContext(s);
|
|
980
|
-
};
|
|
981
|
-
if (!s.shared) {
|
|
982
|
-
submit();
|
|
983
|
-
wake(s);
|
|
984
|
-
pump(s);
|
|
985
|
-
} else transactBackground(s, submit, () => {
|
|
986
|
-
wake(s);
|
|
987
|
-
pump(s);
|
|
988
|
-
});
|
|
989
|
-
};
|
|
990
|
-
const updateContext = (s, operations) => changeContext(s, () => operations);
|
|
991
|
-
const setContext = (s, value, protocol = s.options.protocol ?? "default") => {
|
|
992
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("statewire: context must be a plain object");
|
|
993
|
-
const encoded = encodeDocument(value);
|
|
994
|
-
changeContext(s, (documents) => {
|
|
995
|
-
const found = [...documents.documents].find(([, d]) => d.protocol === protocol && d.main);
|
|
996
|
-
const doc = found?.[0] ?? Math.max(-1, ...documents.documents.keys()) + 1;
|
|
997
|
-
return [{
|
|
998
|
-
...found === void 0 ? {
|
|
999
|
-
op: "mount",
|
|
1000
|
-
protocol,
|
|
1001
|
-
main: true
|
|
1002
|
-
} : {
|
|
1003
|
-
op: "replace",
|
|
1004
|
-
path: []
|
|
1005
|
-
},
|
|
1006
|
-
...doc === 0 ? {} : { doc },
|
|
1007
|
-
value: encoded.value,
|
|
1008
|
-
...encoded.meta === void 0 ? {} : { meta: encoded.meta }
|
|
1009
|
-
}];
|
|
1010
|
-
});
|
|
1011
|
-
};
|
|
1012
933
|
const documentSource = (s) => ({
|
|
1013
934
|
get: (protocol, key, prefix = s.options.keyPrefixes?.[protocol]) => {
|
|
1014
935
|
const mounted = findDocument(s.documents, protocol, key, prefix);
|
|
@@ -1035,40 +956,41 @@ const documentSource = (s) => ({
|
|
|
1035
956
|
}
|
|
1036
957
|
});
|
|
1037
958
|
const invokeProtocol = (s, protocol, method, params) => sendCommand(s, method, params, protocol);
|
|
1038
|
-
const storedCommandCompatible = (command, current) => {
|
|
1039
|
-
if (!command.produced || !
|
|
959
|
+
const storedCommandCompatible = (protocol, command, current) => {
|
|
960
|
+
if (!command.produced || !inRange(command.produced.wire.version, current.wire)) return false;
|
|
1040
961
|
const previous = command.produced.protocols;
|
|
1041
962
|
if (current.protocols === "*") return true;
|
|
1042
963
|
if (previous === "*") return false;
|
|
1043
|
-
const produced = previous.find((p) => p.name ===
|
|
1044
|
-
const offer = current.protocols.find((p) => p.name ===
|
|
964
|
+
const produced = previous.find((p) => p.name === protocol);
|
|
965
|
+
const offer = current.protocols.find((p) => p.name === protocol);
|
|
1045
966
|
return produced !== void 0 && offer !== void 0 && inRange(produced.version, offer);
|
|
1046
967
|
};
|
|
968
|
+
const validateLanes = (record) => {
|
|
969
|
+
if (typeof record.lanes !== "object" || record.lanes === null || Object.values(record.lanes).some((lane) => !Array.isArray(lane.commands))) throw new Error("statewire: session storage loaded a record without command lanes");
|
|
970
|
+
};
|
|
1047
971
|
const loadSessionRecord = (store, options) => {
|
|
1048
972
|
let stored = store.load();
|
|
1049
|
-
if (stored !== null
|
|
973
|
+
if (stored !== null) validateLanes(stored);
|
|
1050
974
|
const current = {
|
|
1051
|
-
wire: options.wireVersion ?? { version: "2026-09-
|
|
975
|
+
wire: options.wireVersion ?? { version: "2026-09-13" },
|
|
1052
976
|
protocols: protocolOffers(options)
|
|
1053
977
|
};
|
|
1054
|
-
|
|
978
|
+
const compatible = (record) => Object.entries(record.lanes).every(([protocol, lane]) => lane.commands.every((command) => storedCommandCompatible(protocol, command, current)));
|
|
979
|
+
if (stored !== null && !compatible(stored)) {
|
|
1055
980
|
if (!store.migrate) throw new Error("statewire: stored commands require a storage migration");
|
|
1056
981
|
stored = store.migrate(stored, current);
|
|
1057
|
-
if (stored !== null
|
|
1058
|
-
|
|
982
|
+
if (stored !== null) {
|
|
983
|
+
validateLanes(stored);
|
|
984
|
+
if (!compatible(stored)) throw new Error("statewire: migrated commands are incompatible");
|
|
985
|
+
store.save(stored);
|
|
986
|
+
}
|
|
1059
987
|
}
|
|
1060
|
-
if (stored !== null && !Array.isArray(stored.commands)) throw new Error("statewire: session storage loaded a record without a commands array");
|
|
1061
988
|
const record = stored ?? {
|
|
1062
989
|
clientId: randomId(),
|
|
1063
|
-
|
|
1064
|
-
ctxNextSeq: 1,
|
|
1065
|
-
commands: []
|
|
990
|
+
lanes: {}
|
|
1066
991
|
};
|
|
1067
992
|
if (stored === null) store.save(record);
|
|
1068
|
-
return
|
|
1069
|
-
record,
|
|
1070
|
-
stored
|
|
1071
|
-
};
|
|
993
|
+
return record;
|
|
1072
994
|
};
|
|
1073
995
|
const useStatewireSession = (options) => {
|
|
1074
996
|
const [defaultStore] = useState(createMemorySessionStore);
|
|
@@ -1078,25 +1000,13 @@ const useStatewireSession = (options) => {
|
|
|
1078
1000
|
const store = options.sessionStore ?? mounted ?? defaultStore;
|
|
1079
1001
|
if (store.lock === void 0 !== (store.subscribe === void 0)) throw new Error("statewire: session storage must provide lock and subscribe together");
|
|
1080
1002
|
const [s] = useState(() => {
|
|
1081
|
-
const
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
nextSeq: 1,
|
|
1085
|
-
ctxNextSeq: 1,
|
|
1086
|
-
commands: []
|
|
1087
|
-
},
|
|
1088
|
-
stored: null
|
|
1003
|
+
const record = store.ready === void 0 ? loadSessionRecord(store, options) : {
|
|
1004
|
+
clientId: randomId(),
|
|
1005
|
+
lanes: {}
|
|
1089
1006
|
};
|
|
1090
1007
|
const session = {
|
|
1091
1008
|
options,
|
|
1092
1009
|
selection: void 0,
|
|
1093
|
-
contextDocuments: stored?.context === void 0 ? {
|
|
1094
|
-
mode: "mounted",
|
|
1095
|
-
documents: /* @__PURE__ */ new Map()
|
|
1096
|
-
} : applyDocumentOps({
|
|
1097
|
-
mode: "mounted",
|
|
1098
|
-
documents: /* @__PURE__ */ new Map()
|
|
1099
|
-
}, stored.context.ops),
|
|
1100
1010
|
record,
|
|
1101
1011
|
ctx: {
|
|
1102
1012
|
beforeAttach: () => persistBeforeSend(session),
|
|
@@ -1127,20 +1037,18 @@ const useStatewireSession = (options) => {
|
|
|
1127
1037
|
ephemeral: false,
|
|
1128
1038
|
deactivated: false,
|
|
1129
1039
|
sessionKnown: false,
|
|
1130
|
-
durableSeq:
|
|
1040
|
+
durableSeq: {},
|
|
1041
|
+
ephemeralSeqs: {},
|
|
1131
1042
|
resumeAt: 0,
|
|
1132
1043
|
idle: false,
|
|
1133
1044
|
onSessionLost: void 0,
|
|
1134
1045
|
onDone: void 0,
|
|
1135
|
-
ctxOps: stored?.context?.ops,
|
|
1136
|
-
ctxPending: stored?.context?.pending.map((p) => ({
|
|
1137
|
-
...p,
|
|
1138
|
-
posted: false
|
|
1139
|
-
})) ?? [],
|
|
1140
1046
|
lanesOnAttach: false,
|
|
1141
|
-
|
|
1047
|
+
attachEntries: /* @__PURE__ */ new Set(),
|
|
1142
1048
|
frameFailures: 0,
|
|
1143
1049
|
pending: [],
|
|
1050
|
+
failedList: [],
|
|
1051
|
+
failedParts: [],
|
|
1144
1052
|
canonical: void 0,
|
|
1145
1053
|
documentListeners: /* @__PURE__ */ new Set(),
|
|
1146
1054
|
documents: {
|
|
@@ -1172,20 +1080,7 @@ const useStatewireSession = (options) => {
|
|
|
1172
1080
|
};
|
|
1173
1081
|
if (store.ready !== void 0) {
|
|
1174
1082
|
session.ready = store.ready.then(() => {
|
|
1175
|
-
|
|
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
|
-
})) ?? [];
|
|
1083
|
+
session.record = loadSessionRecord(store, session.options);
|
|
1189
1084
|
session.recordPersisted = true;
|
|
1190
1085
|
session.hydrated = true;
|
|
1191
1086
|
if (session.running) session.notify();
|
|
@@ -1227,6 +1122,6 @@ const useStatewireSession = (options) => {
|
|
|
1227
1122
|
return s;
|
|
1228
1123
|
};
|
|
1229
1124
|
//#endregion
|
|
1230
|
-
export { discardFailed, documentSource, failedCommands, invokeProtocol, retryFailed,
|
|
1125
|
+
export { discardFailed, documentSource, failedCommands, invokeProtocol, retryFailed, setTransport, useStatewireSession };
|
|
1231
1126
|
|
|
1232
1127
|
//# sourceMappingURL=session.js.map
|