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.
Files changed (86) hide show
  1. package/dist/client/connection.d.ts +8 -5
  2. package/dist/client/connection.d.ts.map +1 -1
  3. package/dist/client/connection.js +24 -13
  4. package/dist/client/connection.js.map +1 -1
  5. package/dist/client/session.d.ts +13 -15
  6. package/dist/client/session.d.ts.map +1 -1
  7. package/dist/client/session.js +296 -401
  8. package/dist/client/session.js.map +1 -1
  9. package/dist/client/timing.d.ts +2 -1
  10. package/dist/client/timing.d.ts.map +1 -1
  11. package/dist/client/timing.js +2 -1
  12. package/dist/client/timing.js.map +1 -1
  13. package/dist/host/engine.d.ts +2 -0
  14. package/dist/host/engine.d.ts.map +1 -1
  15. package/dist/host/engine.js +2 -0
  16. package/dist/host/engine.js.map +1 -1
  17. package/dist/host/host.d.ts +3 -1
  18. package/dist/host/host.d.ts.map +1 -1
  19. package/dist/host/host.js +38 -7
  20. package/dist/host/host.js.map +1 -1
  21. package/dist/host/http.d.ts.map +1 -1
  22. package/dist/host/http.js +9 -19
  23. package/dist/host/http.js.map +1 -1
  24. package/dist/host/socket.d.ts +48 -25
  25. package/dist/host/socket.d.ts.map +1 -1
  26. package/dist/host/socket.js +399 -266
  27. package/dist/host/socket.js.map +1 -1
  28. package/dist/host/synced.d.ts +9 -0
  29. package/dist/host/synced.d.ts.map +1 -0
  30. package/dist/host/synced.js +45 -0
  31. package/dist/host/synced.js.map +1 -0
  32. package/dist/host-internal.d.ts +20 -12
  33. package/dist/host-internal.d.ts.map +1 -1
  34. package/dist/host.d.ts +33 -9
  35. package/dist/host.d.ts.map +1 -1
  36. package/dist/host.js +2 -1
  37. package/dist/index.d.ts +2 -2
  38. package/dist/index.js +2 -1
  39. package/dist/statewire.d.ts +8 -20
  40. package/dist/statewire.d.ts.map +1 -1
  41. package/dist/statewire.js +0 -14
  42. package/dist/statewire.js.map +1 -1
  43. package/dist/storage.d.ts +2 -5
  44. package/dist/storage.d.ts.map +1 -1
  45. package/dist/storage.js +54 -89
  46. package/dist/storage.js.map +1 -1
  47. package/dist/transports/remote.d.ts.map +1 -1
  48. package/dist/transports/remote.js +1 -6
  49. package/dist/transports/remote.js.map +1 -1
  50. package/dist/transports/sse.d.ts +2 -5
  51. package/dist/transports/sse.d.ts.map +1 -1
  52. package/dist/transports/sse.js.map +1 -1
  53. package/dist/transports/testing.d.ts +7 -5
  54. package/dist/transports/testing.d.ts.map +1 -1
  55. package/dist/transports/testing.js +19 -18
  56. package/dist/transports/testing.js.map +1 -1
  57. package/dist/types.d.ts +3 -5
  58. package/dist/types.d.ts.map +1 -1
  59. package/dist/types.js.map +1 -1
  60. package/dist/wire/limits.d.ts +2 -2
  61. package/dist/wire/limits.js +2 -2
  62. package/dist/wire/limits.js.map +1 -1
  63. package/dist/wire/protocol.d.ts +6 -1
  64. package/dist/wire/protocol.d.ts.map +1 -1
  65. package/dist/wire/protocol.js +33 -12
  66. package/dist/wire/protocol.js.map +1 -1
  67. package/package.json +1 -1
  68. package/src/client/connection.ts +46 -33
  69. package/src/client/session.ts +442 -517
  70. package/src/client/timing.ts +2 -0
  71. package/src/host/engine.ts +3 -1
  72. package/src/host/host.ts +50 -3
  73. package/src/host/http.ts +23 -31
  74. package/src/host/socket.ts +560 -339
  75. package/src/host/synced.ts +60 -0
  76. package/src/host-internal.ts +21 -7
  77. package/src/host.ts +40 -8
  78. package/src/index.ts +1 -0
  79. package/src/statewire.ts +8 -37
  80. package/src/storage.ts +90 -157
  81. package/src/transports/remote.ts +0 -7
  82. package/src/transports/sse.ts +8 -3
  83. package/src/transports/testing.ts +34 -18
  84. package/src/types.ts +3 -5
  85. package/src/wire/limits.ts +1 -1
  86. package/src/wire/protocol.ts +40 -11
@@ -1,13 +1,13 @@
1
- import { encodeDocument, reviveDocumentValue } from "../document.js";
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 backlog = (s) => s.pending.filter((p) => p.seq > s.durableSeq).map((p) => ({
62
- seq: p.seq,
63
- ...p.command
64
- }));
65
- const mergeBacklog = (s) => {
66
- const own = backlog(s);
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.pending.map((p) => p.seq));
69
- return [...s.record.commands.filter((c) => !known.has(c.seq) && c.seq > s.durableSeq), ...own].sort((a, b) => a.seq - b.seq);
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
- const parkedSeqs = new Set(parked.map((v) => v.seq));
74
- const commands = mergeBacklog(s).filter((c) => !parkedSeqs.has(c.seq));
75
- const verdicts = parked.length > 0 ? [...s.record.verdicts ?? [], ...parked].slice(-64) : s.record.verdicts;
76
- if (verdicts === s.record.verdicts && commands.length === s.record.commands.length && commands.every((c, i) => c.seq === s.record.commands[i].seq)) return;
77
- setRecord(s, {
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
- commands,
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 NO_FAILED = [];
137
- const failedCommands = (s) => s.record.failed ?? NO_FAILED;
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
- setRecord(s, {
140
- ...s.record,
141
- failed: [...s.record.failed ?? [], {
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.durableSeq)) retainFailed(s, entry, outcome.failure);
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 = 0;
238
- const failed = s.record.failed;
254
+ s.durableSeq = {};
255
+ const previous = s.record.lanes;
239
256
  adopt();
240
- if (failed !== void 0 && s.record.failed !== failed) setRecord(s, {
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
- failed: [...s.record.failed ?? [], ...failed]
272
+ lanes
243
273
  });
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
- 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
- nextSeq: 1,
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
- seq: cmd.seq,
316
- type: cmd.type,
317
- ...cmd.code !== void 0 && { code: cmd.code },
318
- ...cmd.message !== void 0 && { message: cmd.message },
319
- ...cmd.payload !== void 0 && { payload: cmd.payload },
320
- ...cmd.meta !== void 0 && { meta: cmd.meta }
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 (!Number.isSafeInteger(cmd.seq) || cmd.seq <= 0) protocolError("cmd.seq must be a positive safe integer");
325
- if (cmd.dur === true) {
326
- s.durableSeq = Math.max(s.durableSeq, cmd.seq);
327
- for (const entry of s.pending) if (entry.seq <= s.durableSeq) entry.posted = true;
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
- cover(s, entry);
338
- settleEntry(s, entry, outcomeOf(cmd));
379
+ settleVerdict(s, entry, cmd);
339
380
  };
340
381
  const consumeVerdicts = (s) => {
341
- const verdicts = s.record.verdicts ?? [];
342
- const consumed = /* @__PURE__ */ new Set();
343
- for (const verdict of verdicts) {
344
- const entry = s.pending.find((p) => p.seq === verdict.seq);
345
- if (!entry) continue;
346
- consumed.add(verdict.seq);
347
- cover(s, entry);
348
- settleEntry(s, entry, outcomeOf(verdict));
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 (consumed.size === 0) return;
405
+ if (lanes === s.record.lanes) return;
351
406
  setRecord(s, {
352
407
  ...s.record,
353
- commands: s.record.commands.filter((c) => !consumed.has(c.seq)),
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 snapCtxClock = (s, ctxWm) => {
393
- if (s.record.context?.recover === "version-1") {
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.pending = [];
436
- s.durableSeq = seqWm;
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
- ...s.record.failed !== void 0 && { failed: s.record.failed }
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 handleHello = (s, { ops, syn }) => {
450
- if (syn.record !== void 0 && syn.record !== "ephemeral") protocolError(`unknown record: ${String(syn.record)}`);
451
- s.selection = syn.version === void 0 || syn.protocols === void 0 ? void 0 : {
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
- s.ephemeral = syn.record === "ephemeral";
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 [attachCmd, attachCtx] = s.attachLaneSeqs;
467
- s.attachLaneSeqs = [/* @__PURE__ */ new Set(), /* @__PURE__ */ new Set()];
488
+ const attached = s.attachEntries;
489
+ s.attachEntries = /* @__PURE__ */ new Set();
468
490
  const reconcile = () => {
469
491
  if (s.shared) adoptStored(s);
470
- s.durableSeq = syn.seq ?? 0;
471
- for (const entry of s.pending) entry.posted = attachCmd.has(entry.seq);
472
- const ctxWatermark = syn.ctxSeq ?? 0;
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 { seq, ctxSeq } = syn;
499
- if (typeof seq === "number") {
500
- const watermark = Math.max(seq, s.durableSeq);
501
- for (const entry of s.pending) entry.posted = entry.seq <= watermark;
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 firstCmd = s.pending.find((p) => !p.posted);
515
- const firstCtx = s.ctxPending.find((c) => !c.posted);
516
- const cmdGap = typeof seq === "number" && firstCmd !== void 0 && firstCmd.seq > Math.max(seq, s.durableSeq) + 1;
517
- const ctxGap = typeof ctxSeq === "number" && firstCtx !== void 0 && firstCtx.seq > ctxSeq + 1;
518
- if (cmdGap || ctxGap) {
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
- if (cmdGap) sessionLostInner(s, seq, ctxSeq ?? 0);
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, persisted = false) => {
545
- const ctxEntries = [];
546
- for (const entry of s.ctxPending) {
547
- if (entry.posted) continue;
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 (entry.posted) continue;
556
- if (persisted && (s.persistedRecord?.clientId !== s.record.clientId || entry.seq >= s.persistedRecord.nextSeq)) break;
557
- if (cmdEntries.length >= 256) break;
558
- if (cmdEntries.length > 0 && entry.seq !== cmdEntries.at(-1).seq + 1) break;
559
- cmdEntries.push(entry);
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 (ctxEntries.length === 0 && cmdEntries.length === 0) return null;
574
+ if (entries.length === 0) return null;
562
575
  return {
563
- frame: {
564
- ...ctxEntries.length > 0 && { ctx: ctxEntries.map((c) => ({
565
- ops: c.ops,
566
- seq: c.seq
567
- })) },
568
- ...cmdEntries.length > 0 && { cmd: cmdEntries.map((p) => {
569
- const { produced: _produced, ...command } = p.command;
570
- return {
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.attachLaneSeqs = [/* @__PURE__ */ new Set(), /* @__PURE__ */ new Set()];
587
+ s.attachEntries = /* @__PURE__ */ new Set();
582
588
  const wanted = s.lanesOnAttach;
583
589
  s.lanesOnAttach = false;
584
- if (!wanted || !s.sessionKnown || s.record.context?.recover !== void 0) return null;
585
- const lanes = buildLanes(s, true);
590
+ const lanes = buildLanes(s, wanted && s.sessionKnown ? "persisted" : "none");
586
591
  if (!lanes) return null;
587
- s.attachLaneSeqs = [new Set(lanes.cmdEntries.map((p) => p.seq)), new Set(lanes.ctxEntries.map((c) => c.seq))];
588
- for (const entry of lanes.cmdEntries) entry.attempted = true;
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.cmdEntries) entry.attempted = true;
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.cmdEntries) if (s.pending.includes(entry)) {
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 seq = Math.max(s.record.nextSeq, s.durableSeq + 1);
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: command.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
- setRecord(s, {
738
- ...s.record,
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
- const known = new Set(s.pending.map((p) => p.seq));
774
- const stored = s.record.commands.filter((c) => !known.has(c.seq));
775
- if (stored.length === 0) return;
776
- for (const { seq, ...command } of stored) {
777
- const offers = protocolOffers(s.options);
778
- if (offers !== "*" && !offers.some((offer) => offer.name === command.protocol)) continue;
779
- const params = command.meta === void 0 ? command.params : reviveDocumentValue([...command.params], command.meta);
780
- const entry = {
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
- clientId: s.record.clientId
790
- },
791
- attempted: true,
792
- posted: false,
793
- applied: false,
794
- resolve: () => {},
795
- reject: () => {}
796
- };
797
- s.pending.push(entry);
798
- s.lifecycle?.onStart(entry.inv);
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-07" },
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 failed = s.record.failed ?? [];
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
- ...s.record,
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 || !command.protocol || !inRange(command.produced.wire.version, current.wire)) return false;
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 === command.protocol);
1044
- const offer = current.protocols.find((p) => p.name === command.protocol);
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 && !Array.isArray(stored.commands)) throw new Error("statewire: session storage loaded a record without a commands array");
973
+ if (stored !== null) validateLanes(stored);
1050
974
  const current = {
1051
- wire: options.wireVersion ?? { version: "2026-09-07" },
975
+ wire: options.wireVersion ?? { version: "2026-09-13" },
1052
976
  protocols: protocolOffers(options)
1053
977
  };
1054
- if (stored !== null && !stored.commands.every((command) => storedCommandCompatible(command, current))) {
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 && !stored.commands.every((command) => storedCommandCompatible(command, current))) throw new Error("statewire: migrated commands are incompatible");
1058
- if (stored !== null) store.save(stored);
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
- nextSeq: 1,
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 { record, stored } = store.ready === void 0 ? loadSessionRecord(store, options) : {
1082
- record: {
1083
- clientId: randomId(),
1084
- nextSeq: 1,
1085
- ctxNextSeq: 1,
1086
- commands: []
1087
- },
1088
- stored: null
1003
+ 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: 0,
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
- attachLaneSeqs: [/* @__PURE__ */ new Set(), /* @__PURE__ */ new Set()],
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
- const { record: record_0 } = loadSessionRecord(store, session.options);
1176
- session.record = record_0;
1177
- session.contextDocuments = record_0.context === void 0 ? {
1178
- mode: "mounted",
1179
- documents: /* @__PURE__ */ new Map()
1180
- } : applyDocumentOps({
1181
- mode: "mounted",
1182
- documents: /* @__PURE__ */ new Map()
1183
- }, record_0.context.ops);
1184
- session.ctxOps = record_0.context?.ops;
1185
- session.ctxPending = record_0.context?.pending.map((entry) => ({
1186
- ...entry,
1187
- posted: false
1188
- })) ?? [];
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, setContext, setTransport, updateContext, useStatewireSession };
1125
+ export { discardFailed, documentSource, failedCommands, invokeProtocol, retryFailed, setTransport, useStatewireSession };
1231
1126
 
1232
1127
  //# sourceMappingURL=session.js.map