statewire 0.23.0 → 0.24.1

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 (43) hide show
  1. package/dist/client/core.d.ts +4 -3
  2. package/dist/client/core.d.ts.map +1 -1
  3. package/dist/client/core.js +56 -45
  4. package/dist/client/core.js.map +1 -1
  5. package/dist/client/session.d.ts +56 -12
  6. package/dist/client/session.d.ts.map +1 -1
  7. package/dist/client/session.js +632 -260
  8. package/dist/client/session.js.map +1 -1
  9. package/dist/client/sessionStore.d.ts +1 -0
  10. package/dist/client/sessionStore.d.ts.map +1 -1
  11. package/dist/client/sessionStore.js +1 -0
  12. package/dist/client/sessionStore.js.map +1 -1
  13. package/dist/host/host.d.ts +6 -5
  14. package/dist/host/host.d.ts.map +1 -1
  15. package/dist/host/host.js.map +1 -1
  16. package/dist/indexedDb.d.ts +15 -0
  17. package/dist/indexedDb.d.ts.map +1 -0
  18. package/dist/indexedDb.js +171 -0
  19. package/dist/indexedDb.js.map +1 -0
  20. package/dist/statewire.d.ts +117 -59
  21. package/dist/statewire.d.ts.map +1 -1
  22. package/dist/statewire.js +13 -25
  23. package/dist/statewire.js.map +1 -1
  24. package/dist/storage.d.ts +42 -0
  25. package/dist/storage.d.ts.map +1 -1
  26. package/dist/storage.js +387 -125
  27. package/dist/storage.js.map +1 -1
  28. package/dist/transports/remote.d.ts.map +1 -1
  29. package/dist/transports/remote.js +7 -9
  30. package/dist/transports/remote.js.map +1 -1
  31. package/dist/types.d.ts +6 -7
  32. package/dist/types.d.ts.map +1 -1
  33. package/dist/types.js.map +1 -1
  34. package/package.json +2 -1
  35. package/src/client/core.ts +81 -64
  36. package/src/client/session.ts +886 -424
  37. package/src/client/sessionStore.ts +2 -0
  38. package/src/host/host.ts +10 -3
  39. package/src/indexedDb.ts +167 -0
  40. package/src/statewire.ts +133 -86
  41. package/src/storage.ts +548 -112
  42. package/src/transports/remote.ts +13 -14
  43. package/src/types.ts +6 -7
@@ -43,10 +43,11 @@ const laneEntries = (s, protocol) => s.pending.filter((p) => isStamped(p) && p.c
43
43
  const sortPending = (s) => {
44
44
  s.pending.sort((a, b) => (a.seq ?? Infinity) - (b.seq ?? Infinity) || 0);
45
45
  };
46
- const marks = (s, lane, removed) => {
46
+ const marks = (s, lane, removed, failure) => {
47
47
  const added = removed.filter((c) => c.by !== void 0 && c.by !== s.store.owner?.id).map((c) => ({
48
48
  id: c.id,
49
- by: c.by
49
+ by: c.by,
50
+ ...failure && { failure }
50
51
  }));
51
52
  const next = [...lane.removed ?? [], ...added];
52
53
  return next.length > 0 ? { removed: next } : {};
@@ -59,12 +60,11 @@ const withOutbox = (lane, outbox) => {
59
60
  } : rest;
60
61
  };
61
62
  const outboxOf = (lane) => lane.outbox ?? [];
62
- const sameHold = (a, b) => a === b || JSON.stringify(a) === JSON.stringify(b);
63
63
  const changed = (s, p) => s.lifecycle?.onChange?.(p.inv);
64
64
  const stamp = (s, p, seq) => {
65
65
  p.seq = seq;
66
66
  p.inv.seq = seq;
67
- delete p.inv.hold;
67
+ delete p.inv.held;
68
68
  p.away = false;
69
69
  p.attempted = false;
70
70
  p.posted = false;
@@ -78,12 +78,31 @@ const unstamp = (s, p) => {
78
78
  p.posted = false;
79
79
  changed(s, p);
80
80
  };
81
- const setHold = (s, p, hold) => {
82
- if (sameHold(p.inv.hold, hold)) return;
83
- if (hold === void 0) delete p.inv.hold;
84
- else p.inv.hold = hold;
85
- changed(s, p);
81
+ const follow = (s, p, stored) => {
82
+ let dirty = false;
83
+ if (p.inv.held === true !== (stored.held === true)) {
84
+ if (stored.held) p.inv.held = true;
85
+ else delete p.inv.held;
86
+ dirty = true;
87
+ }
88
+ if (p.inv.localMeta !== stored.localMeta) {
89
+ if (stored.localMeta === void 0) delete p.inv.localMeta;
90
+ else p.inv.localMeta = stored.localMeta;
91
+ dirty = true;
92
+ }
93
+ if (p.command.params !== stored.params) {
94
+ p.command = {
95
+ ...p.command,
96
+ params: stored.params,
97
+ ...stored.meta === void 0 ? {} : { meta: stored.meta }
98
+ };
99
+ if (stored.meta === void 0) delete p.command.meta;
100
+ p.inv.params = paramsOf(stored);
101
+ dirty = true;
102
+ }
103
+ if (dirty) changed(s, p);
86
104
  };
105
+ const paramsOf = (stored) => stored.meta === void 0 ? stored.params : reviveDocumentValue(structuredClone(stored.params), stored.meta);
87
106
  const dropPending = (s, p) => {
88
107
  s.pending = s.pending.filter((q) => q !== p);
89
108
  s.lifecycle?.onCovered?.(p.inv);
@@ -120,10 +139,12 @@ const setRecord = (s, record) => {
120
139
  if (!s.deactivated) try {
121
140
  s.store.save(record);
122
141
  } catch (cause) {
142
+ if (isQuota(cause)) throw cause;
123
143
  storageFailed(s, cause);
124
144
  throw s.storageError;
125
145
  }
126
146
  s.record = record;
147
+ s.notify();
127
148
  };
128
149
  const toOutbox = (p) => {
129
150
  const { protocol: _protocol, ...command } = p.command;
@@ -132,7 +153,8 @@ const toOutbox = (p) => {
132
153
  ...command,
133
154
  ts: p.inv.ts,
134
155
  ...p.by !== void 0 && { by: p.by },
135
- ...p.inv.hold !== void 0 && { hold: p.inv.hold }
156
+ ...p.inv.held && { held: true },
157
+ ...p.inv.localMeta !== void 0 && { localMeta: p.inv.localMeta }
136
158
  };
137
159
  };
138
160
  const toStored = (s, p) => {
@@ -143,7 +165,8 @@ const toStored = (s, p) => {
143
165
  ...s.store.owner !== void 0 && { owner: s.store.owner.id },
144
166
  ...p.by !== void 0 && { by: p.by },
145
167
  ...command,
146
- ts: p.inv.ts
168
+ ts: p.inv.ts,
169
+ ...p.inv.localMeta !== void 0 && { localMeta: p.inv.localMeta }
147
170
  };
148
171
  };
149
172
  const backlog = (s, protocol) => laneEntries(s, protocol).map((p) => toStored(s, p));
@@ -154,20 +177,25 @@ const mergeBacklog = (s, protocol) => {
154
177
  };
155
178
  const durableLanes = (s) => [.../* @__PURE__ */ new Set([...Object.keys(s.record.lanes), ...s.pending.map((p) => p.command.protocol).filter((protocol) => !isEphemeral(s, protocol))])];
156
179
  const syncBacklogInner = (s) => {
180
+ if (s.shared && s.shared.notices.length > 0) noticeInner(s, s.shared.notices.splice(0));
157
181
  const parked = s.shared ? s.shared.parked.splice(0) : [];
182
+ const settled = s.shared ? new Set(s.shared.settled) : /* @__PURE__ */ new Set();
183
+ if (s.shared) s.shared.settled.clear();
158
184
  let lanes = s.record.lanes;
159
185
  for (const protocol of durableLanes(s)) {
160
186
  const lane = laneOf(s.record, protocol);
161
- const parkedHere = parked.filter((v) => v.protocol === protocol).map(({ verdict }) => {
187
+ const parkedHere = parked.filter((v) => v.protocol === protocol).flatMap(({ verdict }) => {
162
188
  const id = verdict.id ?? lane.commands.find((c) => c.seq === verdict.seq)?.id;
163
- return id === void 0 ? verdict : {
189
+ if (id === void 0 || settled.has(id)) return [];
190
+ return [{
164
191
  ...verdict,
165
192
  id
166
- };
193
+ }];
167
194
  });
168
195
  const parkedSeqs = new Set(parkedHere.map((v) => v.seq));
169
196
  const commands = mergeBacklog(s, protocol).filter((c) => !parkedSeqs.has(c.seq));
170
- const verdicts = parkedHere.length > 0 ? [...lane.verdicts ?? [], ...parkedHere].slice(-64) : lane.verdicts;
197
+ const kept = (lane.verdicts ?? []).filter((v) => v.id === void 0 || !settled.has(v.id));
198
+ const verdicts = parkedHere.length > 0 || kept.length !== (lane.verdicts ?? []).length ? [...kept, ...parkedHere].slice(-64) : lane.verdicts;
171
199
  if (verdicts === lane.verdicts && commands.length === lane.commands.length && commands.every((c, i) => c.seq === lane.commands[i].seq)) continue;
172
200
  lanes = {
173
201
  ...lanes,
@@ -202,8 +230,14 @@ const transact = (s, fn) => {
202
230
  }
203
231
  const shared = s.shared;
204
232
  const result = shared.txq.then(() => s.store.lock(async () => {
205
- refresh(s);
206
- fn();
233
+ const stored = await readStored(s);
234
+ shared.inTx = true;
235
+ try {
236
+ refresh(s, stored);
237
+ fn();
238
+ } finally {
239
+ shared.inTx = false;
240
+ }
207
241
  watchOwners(s);
208
242
  s.notify();
209
243
  const persistence = s.store.flush?.();
@@ -215,14 +249,15 @@ const transact = (s, fn) => {
215
249
  shared.txq = result.then(() => {}, () => {});
216
250
  return result;
217
251
  };
218
- const refresh = (s) => {
219
- const stored = s.store.load();
252
+ const readStored = async (s) => s.store.read ? await s.store.read() : s.store.load();
253
+ const refresh = (s, stored) => {
220
254
  if (stored !== null && stored.clientId !== s.record.clientId) {
221
255
  adoptReset(s, stored);
222
256
  return;
223
257
  }
224
- if (stored !== null) s.record = boundRecord(stored);
258
+ if (stored !== null) s.record = boundInbox(s.store, stored);
225
259
  reconcileLocal(s);
260
+ followReleases(s);
226
261
  };
227
262
  const reconcileLocal = (s) => {
228
263
  const me = s.store.owner?.id;
@@ -240,7 +275,7 @@ const reconcileLocal = (s) => {
240
275
  }
241
276
  unstamp(s, p);
242
277
  }
243
- setHold(s, p, queued.hold);
278
+ follow(s, p, queued);
244
279
  } else if (stamped && stamped.owner !== me) {
245
280
  if (p.adopted) dropPending(s, p);
246
281
  else if (!p.away) {
@@ -249,12 +284,20 @@ const reconcileLocal = (s) => {
249
284
  }
250
285
  } else if (stamped) {
251
286
  if (p.seq !== stamped.seq) stamp(s, p, stamped.seq);
252
- } else if (isOutboxed(p)) {
253
- if (!lane.removed?.some((m) => m.id === p.id)) {
287
+ } else if (!p.adopted && (isOutboxed(p) || p.away)) {
288
+ const mark = lane.removed?.find((m) => m.id === p.id);
289
+ if (mark === void 0) {
254
290
  p.away = true;
255
291
  continue;
256
292
  }
257
- settleRemoved(s, p);
293
+ s.lifecycle?.onCovered?.(p.inv);
294
+ settleEntry(s, p, {
295
+ ok: false,
296
+ failure: mark.failure ?? {
297
+ fate: "aborted",
298
+ cause: "removed"
299
+ }
300
+ }, mark.failure ? "noticed" : "memory");
258
301
  const { removed: _removed, ...rest } = lane;
259
302
  const left = lane.removed.filter((m) => m.id !== p.id);
260
303
  setRecord(s, withLane(s.record, protocol, {
@@ -264,6 +307,48 @@ const reconcileLocal = (s) => {
264
307
  }
265
308
  }
266
309
  };
310
+ const locate = (s, id) => {
311
+ for (const [protocol, lane] of Object.entries(s.record.lanes)) {
312
+ const stored = outboxOf(lane).find((c) => c.id === id);
313
+ if (stored) return {
314
+ protocol,
315
+ stored
316
+ };
317
+ if (lane.commands.some((c) => c.id === id)) return { protocol };
318
+ }
319
+ const p = s.pending.find((q) => q.id === id);
320
+ if (p === void 0) return void 0;
321
+ return isOutboxed(p) && isEphemeral(s, p.command.protocol) ? {
322
+ protocol: p.command.protocol,
323
+ stored: toOutbox(p)
324
+ } : { protocol: p.command.protocol };
325
+ };
326
+ const settleReleases = (s, id, outcome) => {
327
+ const waiters = s.releases.get(id);
328
+ if (waiters === void 0) return;
329
+ s.releases.delete(id);
330
+ for (const waiter of waiters) if (outcome.ok) waiter.resolve(outcome.payload);
331
+ else waiter.reject(new StatewireSendError(outcome.failure));
332
+ };
333
+ const followReleases = (s) => {
334
+ for (const id of [...s.releases.keys()]) {
335
+ if (locate(s, id) !== void 0) continue;
336
+ const lanes = Object.values(s.record.lanes);
337
+ const notice = lanes.flatMap((lane) => lane.inbox ?? []).find((n) => n.id === id);
338
+ const mark = lanes.flatMap((lane) => lane.removed ?? []).find((m) => m.id === id);
339
+ const failure = notice?.failure ?? mark?.failure;
340
+ settleReleases(s, id, failure ? {
341
+ ok: false,
342
+ failure
343
+ } : {
344
+ ok: true,
345
+ payload: notice?.result === void 0 ? void 0 : paramsOf({
346
+ params: notice.result.value,
347
+ meta: notice.result.meta
348
+ })[0]
349
+ });
350
+ }
351
+ };
267
352
  const watchOwners = (s) => {
268
353
  const owner = s.store.owner;
269
354
  const shared = s.shared;
@@ -385,69 +470,125 @@ const cover = (s, entry) => {
385
470
  s.lifecycle?.onApplied(entry.inv);
386
471
  s.lifecycle?.onCovered?.(entry.inv);
387
472
  };
388
- const failedCommands = (s) => {
389
- const parts = Object.values(s.record.lanes).flatMap((lane) => lane.failed === void 0 ? [] : [lane.failed]);
390
- if (!(parts.length === s.failedParts.length && parts.every((part, i) => part === s.failedParts[i]))) {
391
- s.failedParts = parts;
392
- s.failedList = parts.flat();
393
- }
394
- return s.failedList;
395
- };
396
- const MAX_FAILED_RETAINED = 256;
397
- const boundFailed = (failed) => failed.length > 256 ? [...failed].sort((a, b) => a.ts - b.ts).slice(-256) : failed;
398
- const boundRecord = (record) => {
473
+ const MAX_INBOX = 256;
474
+ const updateOf = (protocol, stored) => ({
475
+ protocol,
476
+ id: stored.id,
477
+ method: stored.method,
478
+ params: paramsOf(stored),
479
+ ...stored.seq !== void 0 && { seq: stored.seq },
480
+ status: "settled",
481
+ ...stored.result !== void 0 && { result: paramsOf({
482
+ params: stored.result.value,
483
+ meta: stored.result.meta
484
+ })[0] },
485
+ ...stored.failure !== void 0 && { failure: stored.failure },
486
+ ...stored.localMeta !== void 0 && { localMeta: stored.localMeta }
487
+ });
488
+ const boundInbox = (store, record) => {
399
489
  let lanes = record.lanes;
400
- for (const [protocol, lane] of Object.entries(lanes)) if (lane.failed !== void 0 && lane.failed.length > 256) lanes = {
401
- ...lanes,
402
- [protocol]: {
403
- ...lane,
404
- failed: boundFailed(lane.failed)
405
- }
406
- };
490
+ const dropped = [];
491
+ for (const [protocol, lane] of Object.entries(lanes)) {
492
+ const inbox = lane.inbox;
493
+ if (inbox === void 0 || inbox.length <= 256) continue;
494
+ const sorted = [...inbox].sort((x, y) => x.ts - y.ts);
495
+ const cut = sorted.length - 256;
496
+ dropped.push(...sorted.slice(0, cut).map((n) => updateOf(protocol, n)));
497
+ lanes = {
498
+ ...lanes,
499
+ [protocol]: {
500
+ ...lane,
501
+ inbox: sorted.slice(cut)
502
+ }
503
+ };
504
+ }
505
+ if (dropped.length > 0) store.onEvict?.({
506
+ reason: "inbox",
507
+ dropped
508
+ });
407
509
  return lanes === record.lanes ? record : {
408
510
  ...record,
409
511
  lanes
410
512
  };
411
513
  };
412
- const retainFailedInner = (s, entry, failure) => {
413
- const lane = laneOf(s.record, entry.command.protocol);
414
- setRecord(s, withLane(s.record, entry.command.protocol, {
514
+ const toNotice = (stored, outcome) => {
515
+ const result = outcome.ok ? encodeParams(outcome.payload === void 0 ? [] : [outcome.payload]) : void 0;
516
+ return {
517
+ id: stored.id,
518
+ method: stored.method,
519
+ params: stored.params,
520
+ ...stored.meta !== void 0 && { meta: stored.meta },
521
+ ts: stored.ts,
522
+ ...stored.seq !== void 0 && { seq: stored.seq },
523
+ ...stored.localMeta !== void 0 && { localMeta: stored.localMeta },
524
+ ...result !== void 0 && { result: {
525
+ value: result.params,
526
+ ...result.meta !== void 0 && { meta: result.meta }
527
+ } },
528
+ ...!outcome.ok && { failure: outcome.failure }
529
+ };
530
+ };
531
+ const lostNotice = (stored) => toNotice(stored, {
532
+ ok: false,
533
+ failure: LOST
534
+ });
535
+ const LOST = {
536
+ fate: "lost",
537
+ cause: "session-lost"
538
+ };
539
+ const withNotices = (lane, notices) => {
540
+ const known = new Set((lane.inbox ?? []).map((n) => n.id));
541
+ const added = notices.filter((n) => !known.has(n.id));
542
+ return added.length === 0 ? lane : {
415
543
  ...lane,
416
- failed: boundFailed([...lane.failed ?? [], {
417
- protocol: entry.command.protocol,
418
- ...entry.command.produced !== void 0 && { produced: entry.command.produced },
419
- command: entry.command.method,
420
- params: entry.command.params,
421
- ...entry.command.meta !== void 0 && { meta: entry.command.meta },
422
- failure,
423
- ts: entry.inv.ts,
424
- seq: entry.seq
425
- }])
426
- }));
427
- s.notify();
544
+ inbox: [...lane.inbox ?? [], ...added]
545
+ };
428
546
  };
429
- const retainFailed = (s, entry, failure) => {
430
- if (!s.shared) {
431
- retainFailedInner(s, entry, failure);
547
+ const noticeInner = (s, notices) => {
548
+ let record = s.record;
549
+ for (const { entry, outcome } of notices) {
550
+ const protocol = entry.command.protocol;
551
+ const lane = laneOf(record, protocol);
552
+ record = withLane(record, protocol, {
553
+ ...withNotices(lane, [toNotice(toStored(s, entry), outcome)]),
554
+ commands: lane.commands.filter((c) => c.id !== entry.id)
555
+ });
556
+ }
557
+ setRecord(s, boundInbox(s.store, record));
558
+ deliver(s);
559
+ };
560
+ const notice = (s, entry, outcome) => {
561
+ if (!s.shared || s.shared.inTx) {
562
+ noticeInner(s, [{
563
+ entry,
564
+ outcome
565
+ }]);
432
566
  return;
433
567
  }
434
- transactBackground(s, () => retainFailedInner(s, entry, failure));
568
+ s.shared.notices.push({
569
+ entry,
570
+ outcome
571
+ });
572
+ syncBacklog(s);
435
573
  };
436
- const settleEntry = (s, entry, outcome) => {
574
+ const settleEntry = (s, entry, outcome, mode = "notice") => {
437
575
  s.pending = s.pending.filter((p) => p !== entry);
576
+ if (s.shared && !entry.adopted) s.shared.settled.add(entry.id);
577
+ settleReleases(s, entry.id, outcome);
578
+ const durable = mode === "noticed" || mode === "notice" && (outcome.ok || outcome.failure.cause !== "unmount") && isStamped(entry) && !isEphemeral(s, entry.command.protocol);
579
+ if (durable && mode === "notice") notice(s, entry, outcome);
438
580
  if (outcome.ok) {
439
581
  entry.resolve(outcome.payload);
440
582
  s.lifecycle?.onSettle(entry.inv, {
441
583
  type: "accepted",
442
584
  payload: outcome.payload
443
- });
585
+ }, durable);
444
586
  } else {
445
- if (!(outcome.failure.cause === "unmount") && isStamped(entry) && !entry.adopted && !isEphemeral(s, entry.command.protocol)) retainFailed(s, entry, outcome.failure);
446
587
  entry.reject(new StatewireSendError(outcome.failure));
447
588
  s.lifecycle?.onSettle(entry.inv, {
448
589
  type: "rejected",
449
590
  error: new StatewireSendError(outcome.failure)
450
- });
591
+ }, durable);
451
592
  }
452
593
  };
453
594
  const settleAll = (s, detach) => {
@@ -508,7 +649,10 @@ const settleRemoved = (s, entry) => {
508
649
  const requeue = (s, survivors) => {
509
650
  for (const old of survivors) {
510
651
  s.lifecycle?.onCovered?.(old.inv);
511
- enqueueInner(s, old.command, old.inv.params, old.resolve, old.reject, old.inv.hold);
652
+ enqueueInner(s, old.command, old.inv.params, old.resolve, old.reject, {
653
+ ...old.inv.held && { hold: true },
654
+ ...old.inv.localMeta !== void 0 && { localMeta: old.inv.localMeta }
655
+ });
512
656
  }
513
657
  };
514
658
  const rebaseRecord = (s, adopt) => {
@@ -522,15 +666,14 @@ const rebaseRecord = (s, adopt) => {
522
666
  adopt();
523
667
  let lanes = s.record.lanes;
524
668
  for (const [protocol, lane] of Object.entries(previous)) {
525
- const failed = lane.failed;
526
- if (failed === void 0 || laneOf(s.record, protocol).failed === failed) continue;
527
- const current = laneOf(s.record, protocol);
669
+ const current = laneOf({
670
+ ...s.record,
671
+ lanes
672
+ }, protocol);
673
+ if (!lane.inbox?.length || current.inbox === lane.inbox) continue;
528
674
  lanes = {
529
675
  ...lanes,
530
- [protocol]: {
531
- ...current,
532
- failed: boundFailed([...current.failed ?? [], ...failed])
533
- }
676
+ [protocol]: withNotices(current, lane.inbox)
534
677
  };
535
678
  }
536
679
  if (lanes !== s.record.lanes) setRecord(s, {
@@ -548,7 +691,7 @@ const resetRecord = (s, clientId) => rebaseRecord(s, () => setRecord(s, {
548
691
  }));
549
692
  const adoptReset = (s, stored) => {
550
693
  rebaseRecord(s, () => {
551
- s.record = boundRecord(stored);
694
+ s.record = boundInbox(s.store, stored);
552
695
  });
553
696
  if (s.attach === "attached") s.transport?.connection.reconnect();
554
697
  };
@@ -725,17 +868,13 @@ const publishIngested = (s) => {
725
868
  wake(s);
726
869
  };
727
870
  const sessionLostInner = (s, protocol, seqWm) => {
871
+ const lane = laneOf(s.record, protocol);
872
+ const failure = {
873
+ fate: "lost",
874
+ cause: "session-lost"
875
+ };
728
876
  const held = laneEntries(s, protocol);
729
- settleLost(s, held);
730
877
  s.durableSeq[protocol] = seqWm;
731
- const { failed, outbox, removed } = laneOf(s.record, protocol);
732
- setRecord(s, withLane(s.record, protocol, {
733
- nextSeq: seqWm + 1,
734
- commands: [],
735
- ...outbox !== void 0 && { outbox },
736
- ...removed !== void 0 && { removed },
737
- ...failed !== void 0 && { failed }
738
- }));
739
878
  if (held.length > 0 && s.onSessionLost?.(held.map((p) => ({
740
879
  id: p.id,
741
880
  method: p.inv.method,
@@ -743,11 +882,51 @@ const sessionLostInner = (s, protocol, seqWm) => {
743
882
  ts: p.inv.ts,
744
883
  seq: p.seq,
745
884
  ...p.inv.clientId !== void 0 && { clientId: p.inv.clientId }
746
- }))) === "requeue") for (const old of held) enqueueInner(s, old.command, old.inv.params, () => {}, () => {});
885
+ }))) === "requeue") {
886
+ if (s.onDeprecatedPath) s.onDeprecatedPath({
887
+ kind: "session-lost-requeue",
888
+ count: held.length
889
+ });
890
+ else console.warn(`statewire: onSessionLost "requeue" re-enqueued ${held.length} commands; it is deprecated and will be removed`);
891
+ for (const p of held) {
892
+ s.lifecycle?.onCovered?.(p.inv);
893
+ settleEntry(s, p, {
894
+ ok: false,
895
+ failure
896
+ }, "memory");
897
+ }
898
+ setRecord(s, withLane(s.record, protocol, {
899
+ ...lane,
900
+ nextSeq: seqWm + 1,
901
+ commands: []
902
+ }));
903
+ for (const old of held) enqueueInner(s, old.command, old.inv.params, () => {}, () => {}, { ...old.inv.localMeta !== void 0 && { localMeta: old.inv.localMeta } });
904
+ } else {
905
+ const stamps = mergeBacklog(s, protocol);
906
+ const queued = outboxOf(lane);
907
+ const { outbox: _outbox, ...rest } = lane;
908
+ setRecord(s, withLane(s.record, protocol, {
909
+ ...withNotices(rest, [...stamps.map((c) => lostNotice(c)), ...queued.map((c) => lostNotice(c))]),
910
+ nextSeq: seqWm + 1,
911
+ commands: [],
912
+ ...marks(s, lane, [...stamps.map((c) => ({
913
+ id: c.id,
914
+ by: c.by ?? c.owner
915
+ })), ...queued], failure)
916
+ }));
917
+ for (const p of s.pending.filter((q) => q.command.protocol === protocol)) {
918
+ s.lifecycle?.onCovered?.(p.inv);
919
+ settleEntry(s, p, {
920
+ ok: false,
921
+ failure
922
+ }, "noticed");
923
+ }
924
+ deliver(s);
925
+ }
747
926
  publish(s);
748
927
  wake(s);
749
928
  };
750
- const demote = (s, protocol, watermark, demoted) => {
929
+ const demote = (s, protocol, watermark, demoted, waiting) => {
751
930
  const lane = laneOf(s.record, protocol);
752
931
  const local = laneEntries(s, protocol).filter((p) => p.seq > watermark);
753
932
  const above = new Map(lane.commands.filter((c) => c.seq > watermark).map((c) => [c.id, c]));
@@ -758,6 +937,12 @@ const demote = (s, protocol, watermark, demoted) => {
758
937
  sessionLostInner(s, protocol, watermark);
759
938
  return;
760
939
  }
940
+ const me = s.store.owner?.id;
941
+ const foreign = moved.find((c) => c.owner !== void 0 && c.owner !== me && !local.some((p) => p.id === c.id));
942
+ if (foreign !== void 0) {
943
+ for (const p of local) if (p.seq > foreign.seq) waiting.add(p);
944
+ return;
945
+ }
761
946
  setRecord(s, withLane(s.record, protocol, {
762
947
  ...withOutbox(lane, [...moved.map(({ seq: _seq, owner: _owner, ...command }) => command), ...outboxOf(lane)]),
763
948
  nextSeq: watermark + 1,
@@ -768,6 +953,19 @@ const demote = (s, protocol, watermark, demoted) => {
768
953
  else unstamp(s, p);
769
954
  sortPending(s);
770
955
  };
956
+ const recheckLater = (s, watermarks) => {
957
+ if (s.rechecks.watermarks !== watermarks) s.rechecks = {
958
+ watermarks,
959
+ count: 0
960
+ };
961
+ s.rechecks.count += 1;
962
+ if (s.recheck !== void 0) return;
963
+ s.recheck = setTimeout(() => {
964
+ s.recheck = void 0;
965
+ if (s.running && s.attach === "attached") s.transport?.connection.reconnect();
966
+ }, backoff(s.rechecks.count + 1));
967
+ };
968
+ const gapFill = (s) => s.selection !== void 0 && s.selection.version >= "2026-11-01";
771
969
  const selectionOf = (syn) => {
772
970
  if (syn.version === void 0 || syn.protocols === void 0) return void 0;
773
971
  return {
@@ -807,18 +1005,24 @@ const handleHello = (s, { ops, syn }) => {
807
1005
  s.attachEntries = /* @__PURE__ */ new Set();
808
1006
  const reconcile = () => {
809
1007
  const demoted = /* @__PURE__ */ new Set();
1008
+ const waiting = /* @__PURE__ */ new Set();
810
1009
  for (const lane of lanes) {
811
1010
  if (lane.record === "ephemeral") {
812
1011
  s.ephemeralSeqs[lane.name] = Math.max(Object.hasOwn(s.ephemeralSeqs, lane.name) ? s.ephemeralSeqs[lane.name] : 0, ...laneEntries(s, lane.name).map((p) => p.seq));
813
1012
  continue;
814
1013
  }
815
- demote(s, lane.name, lane.seq ?? 0, demoted);
1014
+ demote(s, lane.name, lane.seq ?? 0, demoted, waiting);
816
1015
  s.durableSeq[lane.name] = lane.seq ?? 0;
817
1016
  }
818
1017
  for (const entry of s.pending) {
819
- entry.posted = attached.has(entry) || isStamped(entry) && entry.seq <= durableSeqOf(s, entry.command.protocol) && !isEphemeral(s, entry.command.protocol);
1018
+ entry.posted = attached.has(entry) || waiting.has(entry) || isStamped(entry) && entry.seq <= durableSeqOf(s, entry.command.protocol) && !isEphemeral(s, entry.command.protocol);
820
1019
  entry.stale = false;
821
1020
  }
1021
+ if (waiting.size > 0) recheckLater(s, JSON.stringify(lanes.map((lane) => [lane.name, lane.seq])));
1022
+ else s.rechecks = {
1023
+ watermarks: "",
1024
+ count: 0
1025
+ };
822
1026
  syncBacklogInner(s);
823
1027
  s.sessionKnown = true;
824
1028
  flushInner(s, demoted);
@@ -827,11 +1031,13 @@ const handleHello = (s, { ops, syn }) => {
827
1031
  reconcile();
828
1032
  publishIngested(s);
829
1033
  pump(s);
1034
+ deliver(s);
830
1035
  return;
831
1036
  }
832
1037
  transactBackground(s, reconcile, () => {
833
1038
  publishIngested(s);
834
1039
  pump(s);
1040
+ deliver(s);
835
1041
  });
836
1042
  };
837
1043
  const handleOps = (s, { ops, cmds }) => {
@@ -976,12 +1182,13 @@ const pump = async (s) => {
976
1182
  if (!s.hydrated) await s.ready;
977
1183
  const transport = s.transport;
978
1184
  const flushable = flushCandidates(s).length > 0;
979
- const queued = s.pending.some((p) => isOutboxed(p) && p.inv.hold === void 0);
1185
+ const queued = s.pending.some((p) => isOutboxed(p) && !p.inv.held) || queuedInRecord(s);
980
1186
  if (buildLanes(s) === null && !flushable && (!queued || s.attach === "attached")) return;
981
- if (transport?.parked && s.pending.length === 0) return;
1187
+ const waiting = s.pending.length > 0 || queued;
1188
+ if (transport?.parked && !waiting) return;
982
1189
  if (s.attach !== "attached" || !transport) {
983
- if (s.pending.length === 0) return;
984
- if (transport?.connection.status === "standby" && transport.connection.reason === "idle" || transport?.parked && s.pending.length > 0) transport.connection.reconnect();
1190
+ if (!waiting) return;
1191
+ if (transport?.connection.status === "standby" && transport.connection.reason === "idle" || transport?.parked) transport.connection.reconnect();
985
1192
  await waitForWake(s);
986
1193
  continue;
987
1194
  }
@@ -1011,14 +1218,22 @@ const pump = async (s) => {
1011
1218
  for (const entry of built.entries) entry.attempted = true;
1012
1219
  return transport.send(built.frame);
1013
1220
  };
1014
- result = await (s.shared && !s.deactivated && lanes.entries.length > 0 ? s.store.lock(() => {
1015
- refresh(s);
1221
+ result = await (s.shared && !s.deactivated && lanes.entries.length > 0 ? s.store.lock(async () => {
1222
+ refresh(s, await readStored(s));
1016
1223
  lanes = buildLanes(s);
1017
1224
  if (!lanes || epoch !== s.attachEpoch) return null;
1018
1225
  return send(lanes);
1019
1226
  }) : send(lanes));
1020
1227
  } catch {
1021
1228
  if (!s.running || isFinished(s)) return;
1229
+ if (lanes !== null && lanes.entries.length > 0 && gapFill(s)) {
1230
+ if (epoch !== s.attachEpoch) continue;
1231
+ for (const entry of lanes.entries) if (s.pending.includes(entry)) entry.posted = true;
1232
+ s.frameFailures += 1;
1233
+ await sleepUntilWake(s, backoff(s.frameFailures));
1234
+ if (s.running && epoch === s.attachEpoch && s.attach === "attached") transport.connection.reconnect();
1235
+ continue;
1236
+ }
1022
1237
  if (lanes?.frame.ctx?.length) {
1023
1238
  s.lanesOnAttach = true;
1024
1239
  transport.connection.reconnect();
@@ -1077,6 +1292,7 @@ const start = (s) => {
1077
1292
  transactBackground(s, () => consumeVerdicts(s), () => {
1078
1293
  wake(s);
1079
1294
  pump(s);
1295
+ deliver(s);
1080
1296
  });
1081
1297
  });
1082
1298
  s.running = true;
@@ -1085,7 +1301,10 @@ const start = (s) => {
1085
1301
  s.recordPersisted = true;
1086
1302
  adoptOwner(s, void 0);
1087
1303
  consumeVerdicts(s);
1088
- }, () => void pump(s));
1304
+ }, () => {
1305
+ pump(s);
1306
+ deliver(s);
1307
+ });
1089
1308
  return;
1090
1309
  }
1091
1310
  if (!s.recordPersisted) {
@@ -1095,9 +1314,12 @@ const start = (s) => {
1095
1314
  adoptOwner(s, void 0);
1096
1315
  s.running = true;
1097
1316
  pump(s);
1317
+ deliver(s);
1098
1318
  };
1099
1319
  const stop = (s) => {
1100
1320
  s.running = false;
1321
+ clearTimeout(s.recheck);
1322
+ s.recheck = void 0;
1101
1323
  if (s.shared) {
1102
1324
  s.shared.unsubscribe?.();
1103
1325
  s.shared.unsubscribe = null;
@@ -1113,7 +1335,12 @@ const setTransport = (s, transport) => {
1113
1335
  s.transport = transport;
1114
1336
  wake(s);
1115
1337
  };
1116
- const enqueueInner = (s, command, params, resolve, reject, hold) => {
1338
+ const QUOTA = {
1339
+ fate: "aborted",
1340
+ cause: "quota"
1341
+ };
1342
+ const isQuota = (error) => error instanceof Error && error.name === "StatewireQuotaError";
1343
+ const enqueueInner = (s, command, params, resolve, reject, options = {}) => {
1117
1344
  const protocol = command.protocol;
1118
1345
  const id = randomId();
1119
1346
  const entry = {
@@ -1127,7 +1354,8 @@ const enqueueInner = (s, command, params, resolve, reject, hold) => {
1127
1354
  ts: Date.now(),
1128
1355
  id,
1129
1356
  clientId: s.record.clientId,
1130
- ...hold !== void 0 && { hold }
1357
+ ...options.hold && { held: true },
1358
+ ...options.localMeta !== void 0 && { localMeta: options.localMeta }
1131
1359
  },
1132
1360
  adopted: false,
1133
1361
  by: s.store.owner?.id,
@@ -1139,30 +1367,38 @@ const enqueueInner = (s, command, params, resolve, reject, hold) => {
1139
1367
  resolve,
1140
1368
  reject
1141
1369
  };
1142
- s.pending.push(entry);
1143
1370
  if (!isEphemeral(s, protocol)) {
1144
1371
  const lane = laneOf(s.record, protocol);
1145
- setRecord(s, withLane(s.record, protocol, withOutbox(lane, [...outboxOf(lane), toOutbox(entry)])));
1372
+ try {
1373
+ setRecord(s, withLane(s.record, protocol, withOutbox(lane, [...outboxOf(lane), toOutbox(entry)])));
1374
+ } catch (error) {
1375
+ if (!isQuota(error)) throw error;
1376
+ reject(new StatewireSendError(QUOTA));
1377
+ return;
1378
+ }
1379
+ s.pending.push(entry);
1146
1380
  reorder(s, protocol);
1147
- }
1381
+ } else s.pending.push(entry);
1148
1382
  s.lifecycle?.onStart(entry.inv);
1149
1383
  s.notify();
1150
1384
  };
1151
1385
  const negotiated = (s, protocol) => s.selection?.protocols.some((p) => p.name === protocol) ?? false;
1152
- const views = /* @__PURE__ */ new WeakMap();
1153
- const viewOf = (protocol, stored) => {
1154
- const cached = views.get(stored);
1386
+ const viewOf = (s, protocol, stored, state) => {
1387
+ const cached = s.views.get(stored);
1155
1388
  if (cached?.protocol === protocol) return cached;
1156
1389
  const view = {
1157
1390
  id: stored.id,
1158
1391
  protocol,
1159
1392
  command: {
1160
1393
  method: stored.method,
1161
- params: stored.meta === void 0 ? stored.params : reviveDocumentValue(structuredClone(stored.params), stored.meta)
1394
+ params: paramsOf(stored)
1162
1395
  },
1163
- held: stored.hold === void 0 ? null : { meta: stored.hold.meta }
1396
+ ts: stored.ts,
1397
+ state: state ?? (stored.held ? "held" : "queued"),
1398
+ localMeta: stored.localMeta,
1399
+ durable: s.store.durable !== false && !isEphemeral(s, protocol)
1164
1400
  };
1165
- views.set(stored, view);
1401
+ s.views.set(stored, view);
1166
1402
  return view;
1167
1403
  };
1168
1404
  /** The lane's outbox as this client may flush it: the record's on a durable lane, its own on an ephemeral one. */
@@ -1171,7 +1407,7 @@ const bubble = (s, protocol, queue) => {
1171
1407
  const may = s.options.canOvertake?.[protocol];
1172
1408
  const order = [];
1173
1409
  for (const stored of queue) {
1174
- if (stored.hold !== void 0) {
1410
+ if (stored.held) {
1175
1411
  order.push({
1176
1412
  stored,
1177
1413
  blocked: true
@@ -1179,7 +1415,7 @@ const bubble = (s, protocol, queue) => {
1179
1415
  continue;
1180
1416
  }
1181
1417
  let at = order.length;
1182
- while (at > 0 && order[at - 1].blocked && may !== void 0 && may(viewOf(protocol, stored), viewOf(protocol, order[at - 1].stored))) at--;
1418
+ while (at > 0 && order[at - 1].blocked && may !== void 0 && may(viewOf(s, protocol, stored), viewOf(s, protocol, order[at - 1].stored))) at--;
1183
1419
  order.splice(at, 0, {
1184
1420
  stored,
1185
1421
  blocked: at > 0 && order[at - 1].blocked
@@ -1208,8 +1444,30 @@ const flushCandidates = (s) => flushLanes(s).flatMap((protocol) => {
1208
1444
  stored
1209
1445
  }));
1210
1446
  });
1447
+ const queuedInRecord = (s) => Object.keys(s.record.lanes).some((protocol) => (s.selection ? negotiated(s, protocol) : offered(s, protocol)) && bubble(s, protocol, laneQueue(s, protocol)).free > 0);
1211
1448
  const reasonOf = (s, stored, demoted) => stored.released ? "release" : demoted.has(stored.id) ? "reconnect" : s.mountIds.has(stored.id) ? "reload" : "enqueue";
1212
1449
  const NO_IDS = /* @__PURE__ */ new Set();
1450
+ const patched = (stored, patch) => {
1451
+ const { held: _held, released: _released, localMeta: _meta, ...rest } = stored;
1452
+ const held = patch.held ?? stored.held === true;
1453
+ const localMeta = patch.localMeta === void 0 ? stored.localMeta : patch.localMeta.value;
1454
+ let next = {
1455
+ ...rest,
1456
+ ...held && { held: true },
1457
+ ...!held && (stored.held || stored.released) && { released: true },
1458
+ ...localMeta !== void 0 && { localMeta }
1459
+ };
1460
+ if (patch.params !== void 0) {
1461
+ const { params, meta } = encodeParams(patch.params);
1462
+ const { meta: _old, ...plain } = next;
1463
+ next = {
1464
+ ...plain,
1465
+ params,
1466
+ ...meta !== void 0 && { meta }
1467
+ };
1468
+ }
1469
+ return next;
1470
+ };
1213
1471
  const flushInner = (s, demoted = NO_IDS) => {
1214
1472
  if (s.attach !== "attached" || !s.sessionKnown || !s.running || isFinished(s)) return;
1215
1473
  const candidates = flushCandidates(s);
@@ -1217,11 +1475,11 @@ const flushInner = (s, demoted = NO_IDS) => {
1217
1475
  const groups = /* @__PURE__ */ new Map();
1218
1476
  for (const { protocol, stored } of candidates) {
1219
1477
  const reason = reasonOf(s, stored, demoted);
1220
- groups.set(reason, [...groups.get(reason) ?? [], viewOf(protocol, stored)]);
1478
+ groups.set(reason, [...groups.get(reason) ?? [], viewOf(s, protocol, stored)]);
1221
1479
  }
1222
1480
  const flushing = {
1223
1481
  ids: new Set(candidates.map((c) => c.stored.id)),
1224
- held: /* @__PURE__ */ new Map(),
1482
+ patches: /* @__PURE__ */ new Map(),
1225
1483
  removed: /* @__PURE__ */ new Set()
1226
1484
  };
1227
1485
  s.flushing = flushing;
@@ -1250,17 +1508,14 @@ const flushInner = (s, demoted = NO_IDS) => {
1250
1508
  if (own) settleRemoved(s, own);
1251
1509
  continue;
1252
1510
  }
1253
- const hold = flushing.held.get(stored.id);
1254
- if (hold === void 0) {
1511
+ const patch = flushing.patches.get(stored.id);
1512
+ if (patch === void 0) {
1255
1513
  queue.push(stored);
1256
1514
  continue;
1257
1515
  }
1258
- const { released: _released, ...rest } = stored;
1259
- queue.push({
1260
- ...rest,
1261
- hold
1262
- });
1263
- if (own) setHold(s, own, hold);
1516
+ const next = patched(stored, patch);
1517
+ queue.push(next);
1518
+ if (own) follow(s, own, next);
1264
1519
  }
1265
1520
  const { order, free } = bubble(s, protocol, queue);
1266
1521
  let nextSeq = lane.nextSeq;
@@ -1276,7 +1531,7 @@ const flushInner = (s, demoted = NO_IDS) => {
1276
1531
  }
1277
1532
  const seq = Math.max(nextSeq, durableSeqOf(s, protocol) + 1);
1278
1533
  nextSeq = seq + 1;
1279
- const { hold: _hold, released: _released, ...command } = stored;
1534
+ const { held: _held, released: _released, ...command } = stored;
1280
1535
  const stamped = {
1281
1536
  ...command,
1282
1537
  seq,
@@ -1313,81 +1568,262 @@ const flush = (s) => {
1313
1568
  return transact(s, () => flushInner(s));
1314
1569
  };
1315
1570
  const outboxEntries = (s) => {
1316
- const local = s.pending.filter((p) => isOutboxed(p) && isEphemeral(s, p.command.protocol));
1317
- const parts = [
1318
- ...Object.values(s.record.lanes).map((lane) => lane.outbox),
1319
- ...local.map((p) => p.inv.hold),
1320
- ...local
1321
- ];
1571
+ const local = s.pending.filter((p) => isEphemeral(s, p.command.protocol) && !p.away);
1572
+ const parts = [...Object.values(s.record.lanes).flatMap((lane) => [lane.commands, lane.outbox]), ...local.flatMap((p) => [
1573
+ p,
1574
+ p.seq,
1575
+ p.inv.held,
1576
+ p.inv.localMeta
1577
+ ])];
1322
1578
  if (!(parts.length === s.outboxParts.length && parts.every((part, i) => part === s.outboxParts[i]))) {
1323
1579
  s.outboxParts = parts;
1324
- s.outboxList = [...Object.entries(s.record.lanes).flatMap(([protocol, lane]) => outboxOf(lane).map((c) => viewOf(protocol, c))), ...local.map((p) => viewOf(p.command.protocol, toOutbox(p)))];
1580
+ s.outboxList = [...Object.entries(s.record.lanes).flatMap(([protocol, lane]) => [...[...lane.commands].sort((a, b) => a.seq - b.seq).map((c) => viewOf(s, protocol, c, "sending")), ...outboxOf(lane).map((c) => viewOf(s, protocol, c))]), ...[...local].sort((a, b) => (a.seq ?? Infinity) - (b.seq ?? Infinity)).map((p) => isStamped(p) ? viewOf(s, p.command.protocol, toStored(s, p), "sending") : viewOf(s, p.command.protocol, toOutbox(p)))];
1325
1581
  }
1326
1582
  return s.outboxList;
1327
1583
  };
1328
- const applyOutboxOp = (s, protocol, id, op) => {
1329
- const done = op.type === "hold" ? "held" : op.type === "release" ? "released" : "removed";
1584
+ const entryOf = (s, id) => {
1585
+ const at = locate(s, id);
1586
+ if (at === void 0) throw new Error(`outbox: no entry ${id}`);
1587
+ if (at.stored === void 0) throw new Error(`outbox: ${id} is in flight`);
1588
+ return {
1589
+ protocol: at.protocol,
1590
+ stored: at.stored
1591
+ };
1592
+ };
1593
+ const applyOutboxOp = (s, id, op) => {
1594
+ const at = locate(s, id);
1595
+ if (at?.stored === void 0) return;
1596
+ const { protocol, stored } = at;
1330
1597
  const own = s.pending.find((p) => p.id === id && isOutboxed(p));
1331
- const lane = laneOf(s.record, protocol);
1332
- const stored = outboxOf(lane).find((c) => c.id === id);
1333
- if (!stored) {
1334
- if (op.type === "remove" && lane.removed?.some((m) => m.id === id)) return "removed";
1335
- if (!own || !isEphemeral(s, protocol)) return "stamped";
1336
- } else if (op.type === "remove") setRecord(s, withLane(s.record, protocol, {
1337
- ...withOutbox(lane, outboxOf(lane).filter((c) => c.id !== id)),
1338
- ...marks(s, lane, [stored])
1339
- }));
1340
- else {
1341
- const { hold: _hold, released: _released, ...queued } = stored;
1342
- const next = op.type === "hold" ? {
1343
- ...queued,
1344
- hold: op.hold
1345
- } : stored.hold === void 0 && !stored.released ? queued : {
1346
- ...queued,
1347
- released: true
1348
- };
1349
- setRecord(s, withLane(s.record, protocol, withOutbox(lane, outboxOf(lane).map((c) => c.id === id ? next : c))));
1350
- }
1351
- if (own) {
1352
- if (op.type === "remove") settleRemoved(s, own);
1353
- else setHold(s, own, op.type === "hold" ? op.hold : void 0);
1598
+ if (op.type === "remove") {
1599
+ if (!isEphemeral(s, protocol)) {
1600
+ const lane = laneOf(s.record, protocol);
1601
+ setRecord(s, withLane(s.record, protocol, {
1602
+ ...withOutbox(lane, outboxOf(lane).filter((c) => c.id !== id)),
1603
+ ...marks(s, lane, [stored])
1604
+ }));
1605
+ }
1606
+ if (own) settleRemoved(s, own);
1607
+ } else {
1608
+ const next = patched(stored, op.patch(viewOf(s, protocol, stored)));
1609
+ if (!isEphemeral(s, protocol)) {
1610
+ const lane = laneOf(s.record, protocol);
1611
+ setRecord(s, withLane(s.record, protocol, withOutbox(lane, outboxOf(lane).map((c) => c.id === id ? next : c))));
1612
+ }
1613
+ if (own) follow(s, own, next);
1354
1614
  }
1355
1615
  reorder(s, protocol);
1356
1616
  s.notify();
1357
- return done;
1358
1617
  };
1359
- const outboxOp = (s, entry, op) => {
1618
+ const toPatch = (patch) => {
1619
+ if (typeof patch !== "object" || patch === null) throw new Error("outbox: update must return a patch object");
1620
+ if (patch.held !== void 0 && typeof patch.held !== "boolean") throw new Error("outbox: held must be a boolean");
1621
+ if (patch.command !== void 0 && (Object.keys(patch.command).some((k) => k !== "params") || !Array.isArray(patch.command.params))) throw new Error("outbox: a patch may only replace command.params");
1622
+ return {
1623
+ ...patch.held !== void 0 && { held: patch.held },
1624
+ ..."localMeta" in patch && { localMeta: { value: patch.localMeta } },
1625
+ ...patch.command !== void 0 && { params: patch.command.params }
1626
+ };
1627
+ };
1628
+ const outboxOp = (s, id, op) => {
1629
+ entryOf(s, id);
1360
1630
  const flushing = s.flushing;
1361
- if (flushing?.ids.has(entry.id)) {
1362
- if (op.type === "remove") flushing.removed.add(entry.id);
1363
- else if (op.type === "hold") flushing.held.set(entry.id, op.hold);
1364
- else flushing.held.delete(entry.id);
1365
- return Promise.resolve(op.type === "hold" ? "held" : op.type === "release" ? "released" : "removed");
1631
+ if (flushing?.ids.has(id)) {
1632
+ if (op.type === "remove") flushing.removed.add(id);
1633
+ else {
1634
+ const at = entryOf(s, id);
1635
+ const patch = op.patch(viewOf(s, at.protocol, at.stored));
1636
+ flushing.patches.set(id, {
1637
+ ...flushing.patches.get(id),
1638
+ ...patch
1639
+ });
1640
+ }
1641
+ return Promise.resolve();
1366
1642
  }
1367
- let status = "stamped";
1643
+ const done = () => {
1644
+ wake(s);
1645
+ pump(s);
1646
+ };
1647
+ if (!s.shared) {
1648
+ applyOutboxOp(s, id, op);
1649
+ done();
1650
+ return Promise.resolve();
1651
+ }
1652
+ return transact(s, () => applyOutboxOp(s, id, op)).then(done);
1653
+ };
1654
+ const background = (promise) => {
1655
+ promise.catch(() => {});
1656
+ };
1657
+ const holdOutbox = (s, id) => background(outboxOp(s, id, {
1658
+ type: "patch",
1659
+ patch: () => ({ held: true })
1660
+ }));
1661
+ const releaseOutbox = (s, id) => {
1662
+ entryOf(s, id);
1663
+ const result = new Promise((resolve, reject) => {
1664
+ const waiters = s.releases.get(id) ?? [];
1665
+ waiters.push({
1666
+ resolve,
1667
+ reject
1668
+ });
1669
+ s.releases.set(id, waiters);
1670
+ });
1671
+ result.catch(() => {});
1672
+ outboxOp(s, id, {
1673
+ type: "patch",
1674
+ patch: () => ({ held: false })
1675
+ }).catch((error) => settleWaiters(s, id, error));
1676
+ return result;
1677
+ };
1678
+ const settleWaiters = (s, id, error) => {
1679
+ const waiters = s.releases.get(id) ?? [];
1680
+ s.releases.delete(id);
1681
+ for (const waiter of waiters) waiter.reject(error);
1682
+ };
1683
+ const removeOutbox = (s, id) => background(outboxOp(s, id, { type: "remove" }));
1684
+ const updateOutbox = (s, id, fn) => background(outboxOp(s, id, {
1685
+ type: "patch",
1686
+ patch: (entry) => toPatch(fn(entry))
1687
+ }));
1688
+ const producedOf = (s) => ({
1689
+ wire: s.options.wireVersion ?? { version: "2026-11-01" },
1690
+ protocols: protocolOffers(s.options) === "*" && s.selection ? s.selection.protocols : protocolOffers(s.options)
1691
+ });
1692
+ const placed = (queue, entry, { after, before }) => {
1693
+ const anchor = after ?? before;
1694
+ if (anchor === void 0) return [...queue, entry];
1695
+ const index = queue.findIndex((c) => c.id === anchor);
1696
+ if (index === -1) return [...queue, entry];
1697
+ const at = after !== void 0 ? index + 1 : index;
1698
+ return [
1699
+ ...queue.slice(0, at),
1700
+ entry,
1701
+ ...queue.slice(at)
1702
+ ];
1703
+ };
1704
+ const insertOutbox = (s, entry) => {
1705
+ if (!s.hydrated) throw new Error("outbox: storage is not hydrated yet");
1706
+ if (s.storageError !== void 0) throw s.storageError;
1707
+ if (entry.after !== void 0 && entry.before !== void 0) throw new Error("outbox: after and before are exclusive");
1708
+ for (const anchor of [entry.after, entry.before]) if (anchor !== void 0) entryOf(s, anchor);
1709
+ const { protocol } = entry;
1710
+ if (!(s.selection ? negotiated(s, protocol) : offered(s, protocol))) throw new Error(`statewire: protocol "${protocol}" is not negotiated`);
1711
+ const { params, meta } = encodeParams(entry.command.params);
1712
+ const id = entry.from === void 0 ? randomId() : `${entry.from}~`;
1713
+ const stored = {
1714
+ id,
1715
+ produced: producedOf(s),
1716
+ method: entry.command.method,
1717
+ params,
1718
+ ...meta !== void 0 && { meta },
1719
+ ts: Date.now(),
1720
+ ...entry.held && { held: true },
1721
+ ...entry.localMeta !== void 0 && { localMeta: entry.localMeta },
1722
+ ...s.store.owner !== void 0 && { by: s.store.owner.id }
1723
+ };
1368
1724
  const run = () => {
1369
- status = applyOutboxOp(s, entry.protocol, entry.id, op);
1725
+ if (isEphemeral(s, protocol)) {
1726
+ enqueueInner(s, {
1727
+ protocol,
1728
+ produced: stored.produced,
1729
+ method: stored.method,
1730
+ params,
1731
+ ...meta !== void 0 && { meta }
1732
+ }, entry.command.params, () => {}, () => {}, {
1733
+ ...entry.held && { hold: true },
1734
+ ...entry.localMeta !== void 0 && { localMeta: entry.localMeta }
1735
+ });
1736
+ return;
1737
+ }
1738
+ let base = s.record;
1739
+ if (entry.from !== void 0) {
1740
+ const source = Object.entries(base.lanes).find(([, l]) => l.inbox?.some((n) => n.id === entry.from));
1741
+ if (source === void 0 || locate(s, id) !== void 0) return;
1742
+ base = withLane(base, source[0], withoutNotice(source[1], entry.from));
1743
+ }
1744
+ const lane = laneOf(base, protocol);
1745
+ try {
1746
+ setRecord(s, withLane(base, protocol, withOutbox(lane, placed(outboxOf(lane), stored, entry))));
1747
+ } catch (error) {
1748
+ if (!isQuota(error)) throw error;
1749
+ if (entry.from !== void 0) s.skipped.add(entry.from);
1750
+ settleReleases(s, id, {
1751
+ ok: false,
1752
+ failure: QUOTA
1753
+ });
1754
+ return;
1755
+ }
1756
+ s.mountIds.delete(id);
1757
+ reorder(s, protocol);
1758
+ s.notify();
1370
1759
  };
1371
1760
  const done = () => {
1372
1761
  wake(s);
1373
1762
  pump(s);
1374
- return status;
1375
1763
  };
1376
1764
  if (!s.shared) {
1377
1765
  run();
1378
- return Promise.resolve(done());
1766
+ done();
1767
+ } else background(transact(s, run).then(done));
1768
+ return id;
1769
+ };
1770
+ const nextNotice = (s) => {
1771
+ for (const [protocol, lane] of Object.entries(s.record.lanes)) {
1772
+ const found = (lane.inbox ?? []).find((n) => !s.skipped.has(n.id));
1773
+ if (found) return {
1774
+ protocol,
1775
+ found
1776
+ };
1379
1777
  }
1380
- return transact(s, run).then(done);
1381
1778
  };
1382
- const holdOutbox = (s, entry, meta) => outboxOp(s, entry, {
1383
- type: "hold",
1384
- hold: meta === void 0 ? {} : { meta }
1385
- });
1386
- const releaseOutbox = (s, entry) => outboxOp(s, entry, { type: "release" });
1387
- const removeOutbox = (s, entry) => outboxOp(s, entry, { type: "remove" });
1388
- const enqueue = (s, command, params, resolve, reject, hold) => {
1779
+ const withoutNotice = (lane, id) => {
1780
+ const inbox = (lane.inbox ?? []).filter((n) => n.id !== id);
1781
+ const { inbox: _inbox, ...rest } = lane;
1782
+ return inbox.length > 0 ? {
1783
+ ...rest,
1784
+ inbox
1785
+ } : rest;
1786
+ };
1787
+ const ack = (s, protocol, id) => {
1788
+ const lane = laneOf(s.record, protocol);
1789
+ if (s.skipped.has(id) || !lane.inbox?.some((n) => n.id === id)) return;
1790
+ setRecord(s, withLane(s.record, protocol, withoutNotice(lane, id)));
1791
+ };
1792
+ const deliver = (s) => {
1793
+ const handler = s.onSettled;
1794
+ if (handler === void 0 || s.delivering || !s.running || !s.hydrated || !s.sessionKnown) return;
1795
+ if (nextNotice(s) === void 0) return;
1796
+ s.delivering = true;
1797
+ const run = async () => {
1798
+ for (;;) {
1799
+ if (s.shared) await transact(s, () => {});
1800
+ else await null;
1801
+ const next = nextNotice(s);
1802
+ if (next === void 0 || !s.running) return;
1803
+ const { protocol, found } = next;
1804
+ try {
1805
+ await handler(updateOf(protocol, found));
1806
+ } catch (error) {
1807
+ console.error("statewire: onCommandChange threw on a settled update; it stays for the next mount", error);
1808
+ s.skipped.add(found.id);
1809
+ continue;
1810
+ }
1811
+ if (s.shared) await transact(s, () => ack(s, protocol, found.id));
1812
+ else ack(s, protocol, found.id);
1813
+ s.notify();
1814
+ }
1815
+ };
1816
+ (s.store.notifyLock ? s.store.notifyLock(run) : run()).then(() => {
1817
+ s.delivering = false;
1818
+ deliver(s);
1819
+ }, (error) => {
1820
+ s.delivering = false;
1821
+ console.error("statewire: settled update delivery stopped", error);
1822
+ });
1823
+ };
1824
+ const enqueue = (s, command, params, resolve, reject, options) => {
1389
1825
  if (!s.shared) {
1390
- enqueueInner(s, command, params, resolve, reject, hold);
1826
+ enqueueInner(s, command, params, resolve, reject, options);
1391
1827
  wake(s);
1392
1828
  pump(s);
1393
1829
  return;
@@ -1400,7 +1836,7 @@ const enqueue = (s, command, params, resolve, reject, hold) => {
1400
1836
  }));
1401
1837
  return;
1402
1838
  }
1403
- enqueueInner(s, command, params, resolve, reject, hold);
1839
+ enqueueInner(s, command, params, resolve, reject, options);
1404
1840
  }).then(() => {
1405
1841
  wake(s);
1406
1842
  pump(s);
@@ -1412,6 +1848,7 @@ const offered = (s, protocol) => {
1412
1848
  };
1413
1849
  const sendCommand = (s, method, params, protocol = s.options.protocol ?? "default", options) => {
1414
1850
  if (!(s.selection ? s.selection.protocols.some((p) => p.name === protocol) : offered(s, protocol))) throw new Error(`statewire: protocol "${protocol}" is not negotiated`);
1851
+ if (options?.hold !== void 0 && typeof options.hold !== "boolean") throw new Error("statewire: invoke hold must be a boolean");
1415
1852
  if (!s.hydrated) {
1416
1853
  const result = s.ready.then(() => sendCommand(s, method, params, protocol, options));
1417
1854
  result.catch(() => {});
@@ -1434,93 +1871,14 @@ const sendCommand = (s, method, params, protocol = s.options.protocol ?? "defaul
1434
1871
  const { params: encoded, meta } = encodeParams(params);
1435
1872
  enqueue(s, {
1436
1873
  protocol,
1437
- produced: {
1438
- wire: s.options.wireVersion ?? { version: "2026-11-01" },
1439
- protocols: protocolOffers(s.options) === "*" && s.selection ? s.selection.protocols : protocolOffers(s.options)
1440
- },
1874
+ produced: producedOf(s),
1441
1875
  method,
1442
1876
  params: encoded,
1443
1877
  ...meta !== void 0 && { meta }
1444
- }, params, resolve, reject, options?.hold);
1445
- });
1446
- result.catch(() => {});
1447
- return result;
1448
- };
1449
- const findFailed = (s, entry) => (laneOf(s.record, entry.protocol).failed ?? []).find((f) => f.seq === entry.seq && f.ts === entry.ts && f.command === entry.command);
1450
- const unknownFailed = (entry) => /* @__PURE__ */ new Error(`statewire: unknown failed command "${entry.command}"`);
1451
- const takeFailed = (s, entry) => {
1452
- const lane = laneOf(s.record, entry.protocol);
1453
- const found = findFailed(s, entry);
1454
- if (found === void 0) throw unknownFailed(entry);
1455
- setRecord(s, withLane(s.record, entry.protocol, {
1456
- ...lane,
1457
- failed: (lane.failed ?? []).filter((f) => f !== found)
1458
- }));
1459
- s.notify();
1460
- return found;
1461
- };
1462
- const discardFailed = (s, entry) => {
1463
- if (!s.shared) {
1464
- takeFailed(s, entry);
1465
- return;
1466
- }
1467
- if (findFailed(s, entry) === void 0) throw unknownFailed(entry);
1468
- transactBackground(s, () => {
1469
- if (findFailed(s, entry) !== void 0) takeFailed(s, entry);
1470
- });
1471
- };
1472
- const retryFailed = (s, entry) => {
1473
- if (!s.running) return rejectedSend({
1474
- fate: "aborted",
1475
- cause: "unmount"
1476
- });
1477
- if (isFinished(s)) return rejectedSend({
1478
- fate: "aborted",
1479
- cause: "stream-gone"
1480
- });
1481
- const found = findFailed(s, entry);
1482
- if (found === void 0) throw unknownFailed(entry);
1483
- const params = found.meta === void 0 ? found.params : reviveDocumentValue(structuredClone(found.params), found.meta);
1484
- const result = new Promise((resolve, reject) => {
1485
- const retry = () => {
1486
- if (!s.running) {
1487
- reject(new StatewireSendError({
1488
- fate: "aborted",
1489
- cause: "unmount"
1490
- }));
1491
- return;
1492
- }
1493
- if (isFinished(s)) {
1494
- reject(new StatewireSendError({
1495
- fate: "aborted",
1496
- cause: "stream-gone"
1497
- }));
1498
- return;
1499
- }
1500
- const current = findFailed(s, entry);
1501
- if (current === void 0) {
1502
- reject(unknownFailed(entry));
1503
- return;
1504
- }
1505
- takeFailed(s, current);
1506
- enqueueInner(s, {
1507
- protocol: current.protocol,
1508
- ...current.produced !== void 0 && { produced: current.produced },
1509
- method: current.command,
1510
- params: [...current.params],
1511
- ...current.meta !== void 0 && { meta: current.meta }
1512
- }, params, resolve, reject);
1513
- };
1514
- if (!s.shared) {
1515
- retry();
1516
- wake(s);
1517
- pump(s);
1518
- return;
1519
- }
1520
- transact(s, retry).then(() => {
1521
- wake(s);
1522
- pump(s);
1523
- }, reject);
1878
+ }, params, resolve, reject, {
1879
+ ...options?.hold && { hold: true },
1880
+ ...options?.localMeta !== void 0 && { localMeta: options.localMeta }
1881
+ });
1524
1882
  });
1525
1883
  result.catch(() => {});
1526
1884
  return result;
@@ -1583,7 +1941,7 @@ const loadSessionRecord = (store, options) => {
1583
1941
  const record = stored === null ? {
1584
1942
  clientId: randomId(),
1585
1943
  lanes: {}
1586
- } : boundRecord(stored);
1944
+ } : boundInbox(store, stored);
1587
1945
  if (record !== stored) store.save(record);
1588
1946
  return record;
1589
1947
  };
@@ -1601,7 +1959,7 @@ const useStatewireSession = (options) => {
1601
1959
  const documentStore = useContext(DocumentStorageContext);
1602
1960
  if (options.sessionStore !== void 0 && mounted !== void 0) throw new Error("statewire: session storage is set twice — drop the transport's sessionStore or the client's storage.session");
1603
1961
  const store = options.sessionStore ?? mounted ?? defaultStore;
1604
- if (store.lock === void 0 !== (store.subscribe === void 0) || store.lock === void 0 !== (store.owner === void 0)) throw new Error("statewire: session storage must provide lock, subscribe and owner together");
1962
+ if (store.lock === void 0 !== (store.subscribe === void 0) || store.lock === void 0 !== (store.owner === void 0) || store.lock === void 0 !== (store.notifyLock === void 0)) throw new Error("statewire: session storage must provide lock, subscribe, owner and notifyLock together");
1605
1963
  const [s] = useState(() => {
1606
1964
  const record = store.ready === void 0 ? loadSessionRecord(store, options) : {
1607
1965
  clientId: randomId(),
@@ -1640,6 +1998,9 @@ const useStatewireSession = (options) => {
1640
1998
  txq: Promise.resolve(),
1641
1999
  syncQueued: false,
1642
2000
  parked: [],
2001
+ settled: /* @__PURE__ */ new Set(),
2002
+ notices: [],
2003
+ inTx: false,
1643
2004
  unsubscribe: null,
1644
2005
  watched: /* @__PURE__ */ new Set(),
1645
2006
  abort: null
@@ -1652,6 +2013,12 @@ const useStatewireSession = (options) => {
1652
2013
  resumeAt: 0,
1653
2014
  idle: false,
1654
2015
  onSessionLost: void 0,
2016
+ onDeprecatedPath: void 0,
2017
+ onSettled: void 0,
2018
+ releases: /* @__PURE__ */ new Map(),
2019
+ delivering: false,
2020
+ skipped: /* @__PURE__ */ new Set(),
2021
+ views: /* @__PURE__ */ new WeakMap(),
1655
2022
  onFlush: void 0,
1656
2023
  onDone: void 0,
1657
2024
  mountIds: storedIds(record),
@@ -1661,9 +2028,12 @@ const useStatewireSession = (options) => {
1661
2028
  lanesOnAttach: false,
1662
2029
  attachEntries: /* @__PURE__ */ new Set(),
1663
2030
  frameFailures: 0,
2031
+ recheck: void 0,
2032
+ rechecks: {
2033
+ watermarks: "",
2034
+ count: 0
2035
+ },
1664
2036
  pending: [],
1665
- failedList: [],
1666
- failedParts: [],
1667
2037
  canonical: void 0,
1668
2038
  documentListeners: /* @__PURE__ */ new Set(),
1669
2039
  documents: {
@@ -1724,6 +2094,8 @@ const useStatewireSession = (options) => {
1724
2094
  s.store = store;
1725
2095
  s.documentStore = documentStore;
1726
2096
  s.onSessionLost = options.onSessionLost;
2097
+ s.onDeprecatedPath = options.onDeprecatedPath;
2098
+ s.onSettled = options.onSettled;
1727
2099
  s.onFlush = options.onFlush;
1728
2100
  s.onDone = options.onDone;
1729
2101
  const keyPrefixes = JSON.stringify(options.keyPrefixes);
@@ -1743,6 +2115,6 @@ const useStatewireSession = (options) => {
1743
2115
  }, [s]);
1744
2116
  return s;
1745
2117
  };
1746
- export { MAX_FAILED_RETAINED, discardFailed, documentSource, failedCommands, holdOutbox, invokeProtocol, outboxEntries, releaseOutbox, removeOutbox, retryFailed, setContext, setTransport, useStatewireSession };
2118
+ export { MAX_INBOX, documentSource, holdOutbox, insertOutbox, invokeProtocol, outboxEntries, releaseOutbox, removeOutbox, setContext, setTransport, updateOutbox, useStatewireSession };
1747
2119
 
1748
2120
  //# sourceMappingURL=session.js.map