@zhushanwen/session-delivery 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -44,12 +44,9 @@ var LruSet = class {
44
44
  // src/delivery.ts
45
45
  var DEFAULT_CONFIG = {
46
46
  intent: "interrupt-at-turn-boundary",
47
- mergeWindowMs: 0,
48
- backoff: { ms: 100, max: 50 },
49
- watchdogMs: 3e4
47
+ mergeWindowMs: 0
50
48
  };
51
49
  var DEFAULT_DELIVERED_WINDOW = 50;
52
- var PORT_SEND_SETTLE_TIMEOUT_MS = 6e4;
53
50
  function isThenable(v) {
54
51
  return !!v && typeof v.then === "function";
55
52
  }
@@ -81,6 +78,10 @@ function buildBatchPayload(messages) {
81
78
  payload: images.length > 0 ? { kind: "text", content, images } : { kind: "text", content }
82
79
  };
83
80
  }
81
+ function isolateUnmarkedEntry(batch) {
82
+ const firstUnmarked = batch.find((e) => e.unmarked);
83
+ return firstUnmarked !== void 0 ? [firstUnmarked] : batch;
84
+ }
84
85
  function settleChecked(batch, err, checkedPending) {
85
86
  const rejected = /* @__PURE__ */ new Set();
86
87
  if (checkedPending.length === 0) return rejected;
@@ -118,21 +119,19 @@ function resolveWarnSink(config) {
118
119
  function createDelivery(port, config) {
119
120
  const cfg = {
120
121
  ...DEFAULT_CONFIG,
121
- ...config,
122
- backoff: config?.backoff ?? DEFAULT_CONFIG.backoff
122
+ ...config
123
123
  };
124
124
  const active = [];
125
125
  const activeIndex = /* @__PURE__ */ new Map();
126
- const tombstones = /* @__PURE__ */ new Map();
126
+ const terminalIndex = /* @__PURE__ */ new Set();
127
+ const terminalRecords = /* @__PURE__ */ new Map();
128
+ let checkedRetainWarned = false;
129
+ let mergeIgnoredWarned = false;
127
130
  const deliveredLog = [];
128
131
  const checkedPending = [];
129
132
  let inflightBatch = [];
130
133
  let inFlight = false;
131
- let sendAttempts = 0;
132
134
  let mergeTimer;
133
- let backoffTimer;
134
- let hangTimer;
135
- let watchdogTimer;
136
135
  let disposed = false;
137
136
  let settledUnsub;
138
137
  let missingKeyWarned = false;
@@ -140,6 +139,40 @@ function createDelivery(port, config) {
140
139
  let idSeq = 0;
141
140
  const dedupSet = config?.dedupe ? new LruSet(config.dedupe.maxKeys) : null;
142
141
  const changeSubs = /* @__PURE__ */ new Set();
142
+ const store = config?.store;
143
+ if (store) {
144
+ for (const id of store.loadTerminalIds()) terminalIndex.add(id);
145
+ }
146
+ function persistRecord(e) {
147
+ if (!store) return;
148
+ store.persist({
149
+ id: e.id,
150
+ lane: e.lane,
151
+ payload: e.payload,
152
+ intent: e.msg.intent ?? cfg.intent,
153
+ recordedAt: e.createdAt,
154
+ receiptAnchor: e.receiptAnchor,
155
+ unmarked: e.unmarked,
156
+ meta: e.msg.meta
157
+ });
158
+ }
159
+ function ackStore(id) {
160
+ if (!store) return;
161
+ try {
162
+ store.ack(id);
163
+ } catch (err) {
164
+ warn(`store.ack failed (terminal record not persisted; id=${id})`, err);
165
+ }
166
+ }
167
+ function safeHasTerminal(id) {
168
+ if (!store) return false;
169
+ try {
170
+ return store.hasTerminal(id);
171
+ } catch (err) {
172
+ warn(`store.hasTerminal threw; treating as not-terminal (id=${id})`, err);
173
+ return false;
174
+ }
175
+ }
143
176
  function now() {
144
177
  return Date.now();
145
178
  }
@@ -155,7 +188,6 @@ function createDelivery(port, config) {
155
188
  payload: e.payload,
156
189
  createdAt: e.createdAt,
157
190
  updatedAt: e.updatedAt,
158
- sendAttempts: e.sendAttempts,
159
191
  ...e.settledAt !== void 0 ? { settledAt: e.settledAt } : {}
160
192
  };
161
193
  }
@@ -205,8 +237,11 @@ function createDelivery(port, config) {
205
237
  const ts = now();
206
238
  e.updatedAt = ts;
207
239
  e.settledAt = ts;
208
- tombstones.set(e.id, { id: e.id, state, lane: e.lane, settledAt: ts });
240
+ terminalIndex.add(e.id);
241
+ terminalRecords.set(e.id, { id: e.id, state, lane: e.lane, settledAt: ts });
242
+ ackStore(e.id);
209
243
  if (state === "delivered") {
244
+ resolveWaitersOf(e);
210
245
  deliveredLog.push(e);
211
246
  if (deliveredLog.length > DEFAULT_DELIVERED_WINDOW) deliveredLog.shift();
212
247
  }
@@ -225,6 +260,14 @@ function createDelivery(port, config) {
225
260
  }
226
261
  }
227
262
  }
263
+ function resolveWaitersOf(e) {
264
+ for (let i = checkedPending.length - 1; i >= 0; i--) {
265
+ if (checkedPending[i].entry === e) {
266
+ checkedPending[i].resolve();
267
+ checkedPending.splice(i, 1);
268
+ }
269
+ }
270
+ }
228
271
  function clearMergeTimer() {
229
272
  if (mergeTimer !== void 0) {
230
273
  clearTimeout(mergeTimer);
@@ -242,23 +285,11 @@ function createDelivery(port, config) {
242
285
  clearMergeTimer();
243
286
  armMergeTimer();
244
287
  }
245
- function clearBackoffTimer() {
246
- if (backoffTimer !== void 0) {
247
- clearTimeout(backoffTimer);
248
- backoffTimer = void 0;
249
- }
250
- }
251
- function clearHangTimer() {
252
- if (hangTimer !== void 0) {
253
- clearTimeout(hangTimer);
254
- hangTimer = void 0;
255
- }
256
- }
257
288
  function ensureSettledSub() {
258
289
  if (settledUnsub || !port.subscribeSettled) return;
259
290
  settledUnsub = port.subscribeSettled(() => {
260
291
  if (disposed) return;
261
- if (!isBusySafe()) {
292
+ if (cfg.dispatchMode === "on-accept" || !isBusySafe()) {
262
293
  flush();
263
294
  }
264
295
  });
@@ -269,30 +300,12 @@ function createDelivery(port, config) {
269
300
  settledUnsub = void 0;
270
301
  }
271
302
  }
272
- function startWatchdog() {
273
- if (watchdogTimer !== void 0) return;
274
- if (!port.subscribeSettled) return;
275
- watchdogTimer = setInterval(() => {
276
- if (disposed || inFlight) return;
277
- if (!hasQueuedEntries()) return;
278
- if (!isBusySafe()) {
279
- flush();
280
- }
281
- }, cfg.watchdogMs);
282
- }
283
- function stopWatchdog() {
284
- if (watchdogTimer !== void 0) {
285
- clearInterval(watchdogTimer);
286
- watchdogTimer = void 0;
287
- }
288
- }
289
303
  function attemptSend() {
290
304
  if (inflightBatch.some((e) => !inRegistry(e))) {
291
305
  inflightBatch = inflightBatch.filter((e) => inRegistry(e));
292
306
  }
293
307
  if (inflightBatch.length === 0) {
294
308
  inFlight = false;
295
- sendAttempts = 0;
296
309
  pump();
297
310
  return;
298
311
  }
@@ -302,30 +315,9 @@ function createDelivery(port, config) {
302
315
  try {
303
316
  const result = port.send(composed, intent);
304
317
  if (isThenable(result)) {
305
- let settled = false;
306
- clearHangTimer();
307
- hangTimer = setTimeout(() => {
308
- if (settled || disposed) return;
309
- settled = true;
310
- hangTimer = void 0;
311
- warn(
312
- `port.send hung; force-failed after ${PORT_SEND_SETTLE_TIMEOUT_MS}ms (retries may duplicate-deliver; see DeliveryPort.send settle contract)`
313
- );
314
- onSendFail(new Error(`port.send promise did not settle within ${PORT_SEND_SETTLE_TIMEOUT_MS}ms`));
315
- }, PORT_SEND_SETTLE_TIMEOUT_MS);
316
318
  result.then(
317
- (receipt) => {
318
- if (settled) return;
319
- settled = true;
320
- clearHangTimer();
321
- onSendReceipt(receipt);
322
- },
323
- (err) => {
324
- if (settled) return;
325
- settled = true;
326
- clearHangTimer();
327
- onSendFail(err);
328
- }
319
+ (receipt) => onSendReceipt(receipt),
320
+ (err) => onSendFail(err)
329
321
  );
330
322
  } else {
331
323
  onSendReceipt(result);
@@ -336,6 +328,10 @@ function createDelivery(port, config) {
336
328
  }
337
329
  function onSendReceipt(receipt) {
338
330
  if (receipt !== void 0 && receipt.accepted === false) {
331
+ if ("deferred" in receipt) {
332
+ onSendDeferred(receipt.deferred);
333
+ return;
334
+ }
339
335
  onSendFail(new Error(receipt.reason ?? "port.send rejected (accepted:false)"));
340
336
  return;
341
337
  }
@@ -346,7 +342,6 @@ function createDelivery(port, config) {
346
342
  const batch = inflightBatch;
347
343
  inFlight = false;
348
344
  inflightBatch = [];
349
- sendAttempts = 0;
350
345
  settleChecked(batch, void 0, checkedPending);
351
346
  let changed = false;
352
347
  for (const e of batch) {
@@ -366,49 +361,44 @@ function createDelivery(port, config) {
366
361
  }
367
362
  function onSendFail(err) {
368
363
  if (disposed) return;
369
- sendAttempts++;
370
- for (const e of inflightBatch) e.sendAttempts = sendAttempts;
371
- const rejected = settleChecked(inflightBatch, err, checkedPending);
372
- if (rejected.size > 0) {
373
- inflightBatch = inflightBatch.filter((e) => !rejected.has(e));
374
- for (const e of rejected) removeActive(e);
375
- notifyChange();
376
- }
377
- if (inflightBatch.length === 0) {
378
- inFlight = false;
379
- inflightBatch = [];
380
- sendAttempts = 0;
381
- warn("port.send failed", err);
382
- pump();
383
- return;
364
+ const batch = inflightBatch;
365
+ inFlight = false;
366
+ inflightBatch = [];
367
+ warn("port.send failed (first-failure stop, no kernel retry)", err);
368
+ const rejected = settleChecked(batch, err, checkedPending);
369
+ let changed = false;
370
+ let retained = false;
371
+ for (const e of batch) {
372
+ if (!inRegistry(e)) continue;
373
+ if (rejected.has(e)) continue;
374
+ if (e.onFailure === "retain") continue;
375
+ callOnSettled(e.msg, "rejected");
384
376
  }
385
- if (sendAttempts > cfg.backoff.max) {
386
- const failedBatch = inflightBatch;
387
- inFlight = false;
388
- inflightBatch = [];
389
- sendAttempts = 0;
390
- warn("port.send failed after max retries", err);
391
- let changed = false;
392
- for (const e of failedBatch) {
393
- if (disposed) break;
394
- if (!inRegistry(e)) continue;
395
- e.state = "failed";
396
- const ts = now();
397
- e.updatedAt = ts;
398
- e.settledAt = ts;
377
+ for (const e of batch) {
378
+ if (!inRegistry(e)) continue;
379
+ if (e.onFailure === "retain") {
380
+ e.state = "queued";
381
+ e.updatedAt = now();
399
382
  changed = true;
400
- callOnSettled(e.msg, "rejected");
383
+ retained = true;
384
+ warn(`entry retained after failure (id=${e.id}); re-dispatch waits for next settled edge or external flush`);
385
+ continue;
401
386
  }
402
- if (changed) notifyChange();
403
- pump();
404
- return;
387
+ store?.discard(e.id);
388
+ removeActive(e);
389
+ changed = true;
405
390
  }
406
- if (sendAttempts === 1) warn("port.send failed, retrying with backoff", err);
407
- backoffTimer = setTimeout(() => {
408
- backoffTimer = void 0;
409
- if (disposed || !inFlight) return;
410
- attemptSend();
411
- }, cfg.backoff.ms);
391
+ if (changed) notifyChange();
392
+ if (!retained) pump();
393
+ }
394
+ function onSendDeferred(reason) {
395
+ if (disposed) return;
396
+ const batch = inflightBatch;
397
+ inFlight = false;
398
+ inflightBatch = [];
399
+ warn(
400
+ `port.send deferred (designed-in wait, reason=${reason}); ${batch.length} entry(ies) stay queued for next dispatch edge`
401
+ );
412
402
  }
413
403
  function pump() {
414
404
  if (disposed || inFlight) return;
@@ -418,49 +408,39 @@ function createDelivery(port, config) {
418
408
  if (inRegistry(w.entry) && w.entry.state === "queued") batch.push(w.entry);
419
409
  }
420
410
  if (batch.length > 0) {
421
- inflightBatch = batch;
411
+ inflightBatch = isolateUnmarkedEntry(batch);
422
412
  inFlight = true;
423
- sendAttempts = 0;
424
413
  attemptSend();
425
414
  return;
426
415
  }
427
416
  }
428
417
  if (hasQueuedEntries()) {
429
- scheduleFlush(0);
418
+ scheduleFlush();
430
419
  return;
431
420
  }
432
- if (checkedPending.length === 0) stopWatchdog();
433
421
  }
434
422
  function doSend() {
435
423
  if (disposed || inFlight) return;
436
- const batch = active.filter((e) => e.state === "queued");
424
+ const batch = isolateUnmarkedEntry(active.filter((e) => e.state === "queued"));
437
425
  if (batch.length === 0) return;
438
426
  inFlight = true;
439
427
  inflightBatch = batch;
440
- sendAttempts = 0;
441
428
  attemptSend();
442
429
  }
443
- function scheduleFlush(attempt) {
430
+ function scheduleFlush() {
444
431
  if (disposed || !hasQueuedEntries()) return;
445
432
  if (inFlight) return;
446
- clearBackoffTimer();
447
- if (isBusySafe() && attempt < cfg.backoff.max) {
448
- if (port.subscribeSettled) {
449
- startWatchdog();
450
- return;
451
- }
452
- backoffTimer = setTimeout(() => {
453
- backoffTimer = void 0;
454
- scheduleFlush(attempt + 1);
455
- }, cfg.backoff.ms);
456
- return;
457
- }
433
+ if (cfg.dispatchMode !== "on-accept" && isBusySafe()) return;
458
434
  doSend();
459
435
  }
460
436
  const warnSink = resolveWarnSink(config);
461
437
  function warn(msg, err) {
462
438
  warnSink(msg, err);
463
439
  }
440
+ if (config?.dispatchMode === "on-accept" && (config.mergeWindowMs ?? 0) > 0 && !mergeIgnoredWarned) {
441
+ mergeIgnoredWarned = true;
442
+ warn("dispatchMode=on-accept ignores mergeWindowMs (batching moves to submit side)");
443
+ }
464
444
  function passDedupe(msg) {
465
445
  if (!dedupSet) return true;
466
446
  if (!msg.dedupeKey) {
@@ -476,7 +456,22 @@ function createDelivery(port, config) {
476
456
  }
477
457
  function seenId(id) {
478
458
  if (id === void 0) return false;
479
- return activeIndex.has(id) || tombstones.has(id);
459
+ if (activeIndex.has(id)) return true;
460
+ if (terminalIndex.has(id)) return true;
461
+ return safeHasTerminal(id);
462
+ }
463
+ function resolveOnFailure(msg, opts) {
464
+ return opts?.onFailure ?? (msg.payload.kind === "custom" ? "retain" : "forget");
465
+ }
466
+ function persistBeforeAccept(e) {
467
+ try {
468
+ persistRecord(e);
469
+ return true;
470
+ } catch (err) {
471
+ warn(`store.persist failed before accept; entry rejected (id=${e.id})`, err);
472
+ removeActive(e);
473
+ return false;
474
+ }
480
475
  }
481
476
  function createEntry(msg, opts) {
482
477
  const ts = now();
@@ -487,10 +482,11 @@ function createDelivery(port, config) {
487
482
  payload: msg.payload,
488
483
  createdAt: ts,
489
484
  updatedAt: ts,
490
- sendAttempts: 0,
491
485
  msg,
492
486
  cancelRequested: false,
493
- receiptAnchor: opts?.receiptAnchor ?? "marker"
487
+ receiptAnchor: opts?.receiptAnchor ?? "marker",
488
+ unmarked: opts?.unmarked ?? false,
489
+ onFailure: resolveOnFailure(msg, opts)
494
490
  };
495
491
  active.push(entry);
496
492
  activeIndex.set(entry.id, entry);
@@ -512,7 +508,10 @@ function createDelivery(port, config) {
512
508
  }
513
509
  if (!passDedupe(msg)) return { kind: "swallowed", reason: "duplicate-content" };
514
510
  const entry = createEntry(msg, opts);
515
- const useMerge = opts?.merge ?? (cfg.mergeWindowMs > 0 && cfg.mergeHoldActive != null && cfg.mergeHoldActive());
511
+ if (!persistBeforeAccept(entry)) {
512
+ return { kind: "rejected", reason: "persist-failed" };
513
+ }
514
+ const useMerge = cfg.dispatchMode !== "on-accept" && (opts?.merge ?? (cfg.mergeWindowMs > 0 && cfg.mergeHoldActive != null && cfg.mergeHoldActive()));
516
515
  if (useMerge) {
517
516
  resetMergeTimer();
518
517
  ensureSettledSub();
@@ -520,7 +519,7 @@ function createDelivery(port, config) {
520
519
  }
521
520
  clearMergeTimer();
522
521
  ensureSettledSub();
523
- scheduleFlush(0);
522
+ scheduleFlush();
524
523
  return { kind: "accepted", id: entry.id };
525
524
  }
526
525
  async function sendChecked(msg, opts) {
@@ -533,14 +532,23 @@ function createDelivery(port, config) {
533
532
  return;
534
533
  }
535
534
  if (!passDedupe(msg)) return;
535
+ if (opts?.onFailure === "retain") {
536
+ if (!checkedRetainWarned) {
537
+ checkedRetainWarned = true;
538
+ warn("sendChecked ignores onFailure=retain (checked entries are always forget)");
539
+ }
540
+ opts = { ...opts, onFailure: "forget" };
541
+ }
536
542
  const entry = createEntry(msg, opts);
543
+ if (!persistBeforeAccept(entry)) {
544
+ throw new Error(`store.persist failed before accept (id=${entry.id})`);
545
+ }
537
546
  ensureSettledSub();
538
547
  return new Promise((resolve, reject) => {
539
548
  checkedPending.push({ entry, resolve, reject });
540
549
  if (!inFlight) {
541
550
  inflightBatch = [entry];
542
551
  inFlight = true;
543
- sendAttempts = 0;
544
552
  attemptSend();
545
553
  }
546
554
  });
@@ -548,7 +556,7 @@ function createDelivery(port, config) {
548
556
  function flush() {
549
557
  if (disposed) return;
550
558
  clearMergeTimer();
551
- scheduleFlush(0);
559
+ scheduleFlush();
552
560
  }
553
561
  function depth() {
554
562
  let n = 0;
@@ -558,7 +566,7 @@ function createDelivery(port, config) {
558
566
  function entriesFull() {
559
567
  return {
560
568
  active: active.map(snapshot),
561
- tombstones: [...tombstones.values()].map((t) => ({ ...t }))
569
+ tombstones: [...terminalRecords.values()].map((t) => ({ ...t }))
562
570
  };
563
571
  }
564
572
  function projection() {
@@ -582,6 +590,28 @@ function createDelivery(port, config) {
582
590
  if (!disposed) callOnSettled(e.msg, "delivered");
583
591
  return true;
584
592
  }
593
+ function confirmDeliveredByProof(id) {
594
+ if (disposed) {
595
+ warn(`confirmDeliveredByProof ignored: delivery handle disposed (id=${id})`);
596
+ return false;
597
+ }
598
+ const e = activeIndex.get(id);
599
+ if (!e || e.receiptAnchor !== "details-id") return false;
600
+ if (e.state !== "queued" && e.state !== "in-flight") return false;
601
+ if (!cfg.proofCheck) {
602
+ warn(`details-id entry has no proofCheck injected; delivery unconfirmed (id=${id})`);
603
+ return false;
604
+ }
605
+ let proven;
606
+ try {
607
+ proven = cfg.proofCheck(id);
608
+ } catch (err) {
609
+ warn(`proofCheck threw; treating as not-delivered (id=${id})`, err);
610
+ return false;
611
+ }
612
+ if (!proven) return false;
613
+ return confirmDelivered(id);
614
+ }
585
615
  function confirmAccepted(id) {
586
616
  if (disposed) return false;
587
617
  const e = activeIndex.get(id);
@@ -616,13 +646,12 @@ function createDelivery(port, config) {
616
646
  const idx = active.indexOf(e);
617
647
  if (idx !== -1) active.splice(idx, 1);
618
648
  e.state = "queued";
619
- e.sendAttempts = 0;
620
649
  e.updatedAt = now();
621
650
  e.settledAt = void 0;
622
651
  }
623
652
  active.unshift(...found);
624
653
  notifyChange();
625
- scheduleFlush(0);
654
+ scheduleFlush();
626
655
  return found.length;
627
656
  }
628
657
  function cancel(id) {
@@ -632,7 +661,7 @@ function createDelivery(port, config) {
632
661
  }
633
662
  const e = activeIndex.get(id);
634
663
  if (!e) {
635
- const tb = tombstones.get(id);
664
+ const tb = terminalRecords.get(id);
636
665
  if (tb) return { kind: "already-final", tombstone: { ...tb } };
637
666
  return { kind: "not-found" };
638
667
  }
@@ -651,40 +680,56 @@ function createDelivery(port, config) {
651
680
  finalizeEntry(e, "cancelled");
652
681
  return { kind: "cancelled", entry: snapshot(e) };
653
682
  }
683
+ function failInFlight(reason) {
684
+ if (disposed) return 0;
685
+ const ts = now();
686
+ let count = 0;
687
+ for (const e of active) {
688
+ if (e.state !== "in-flight") continue;
689
+ const batchIdx = inflightBatch.indexOf(e);
690
+ if (batchIdx !== -1) inflightBatch.splice(batchIdx, 1);
691
+ e.state = "failed";
692
+ e.updatedAt = ts;
693
+ e.settledAt = ts;
694
+ count++;
695
+ callOnSettled(e.msg, "rejected");
696
+ }
697
+ if (count > 0) {
698
+ warn(`failInFlight: ${count} in-flight entry(ies) marked failed (${reason})`);
699
+ notifyChange();
700
+ }
701
+ return count;
702
+ }
654
703
  function drain() {
655
704
  if (disposed) return [];
656
705
  clearMergeTimer();
657
- clearBackoffTimer();
658
- clearHangTimer();
659
706
  const drained = active.slice();
660
707
  active.length = 0;
661
708
  activeIndex.clear();
662
709
  inflightBatch = [];
663
710
  inFlight = false;
664
- sendAttempts = 0;
665
711
  const ts = now();
666
712
  const result = drained.map((e) => {
667
- tombstones.set(e.id, { id: e.id, state: "cancelled", lane: e.lane, settledAt: ts });
713
+ terminalIndex.add(e.id);
714
+ terminalRecords.set(e.id, { id: e.id, state: "cancelled", lane: e.lane, settledAt: ts });
715
+ ackStore(e.id);
668
716
  return { id: e.id, payload: e.payload };
669
717
  });
670
718
  for (const w of checkedPending) {
671
719
  w.reject(new DeliveryReclaimError("drained"));
672
720
  }
673
721
  checkedPending.length = 0;
674
- stopWatchdog();
675
722
  if (drained.length > 0) notifyChange();
676
723
  return result;
677
724
  }
678
725
  function dispose() {
679
726
  disposed = true;
680
727
  clearMergeTimer();
681
- clearBackoffTimer();
682
- clearHangTimer();
683
- stopWatchdog();
684
728
  teardownSettledSub();
685
729
  active.length = 0;
686
730
  activeIndex.clear();
687
- tombstones.clear();
731
+ terminalIndex.clear();
732
+ terminalRecords.clear();
688
733
  deliveredLog.length = 0;
689
734
  inflightBatch = [];
690
735
  inFlight = false;
@@ -695,9 +740,123 @@ function createDelivery(port, config) {
695
740
  checkedPending.length = 0;
696
741
  dedupSet?.clear();
697
742
  }
698
- return { send, sendChecked, flush, depth, entriesFull, projection, onChange, confirmDelivered, confirmAccepted, requeue, cancel, drain, dispose };
743
+ return { send, sendChecked, flush, depth, entriesFull, projection, onChange, confirmDelivered, confirmDeliveredByProof, confirmAccepted, requeue, cancel, failInFlight, drain, dispose };
744
+ }
745
+
746
+ // src/disk-store.ts
747
+ function serializeLine(line) {
748
+ return `${JSON.stringify(line)}
749
+ `;
750
+ }
751
+ function createDiskDeliveryStore(deps) {
752
+ const { fs, dir, file } = deps;
753
+ const log = deps.log ?? ((message, err) => {
754
+ console.warn(message, err ?? "");
755
+ });
756
+ let loaded = false;
757
+ const records = /* @__PURE__ */ new Map();
758
+ const terminalIds = /* @__PURE__ */ new Set();
759
+ const discardedIds = /* @__PURE__ */ new Set();
760
+ let dirCreated = false;
761
+ function applyLine(line) {
762
+ if (line.t === "record") {
763
+ records.set(line.r.id, line.r);
764
+ discardedIds.delete(line.r.id);
765
+ return;
766
+ }
767
+ if (line.t === "ack") {
768
+ terminalIds.add(line.id);
769
+ return;
770
+ }
771
+ if (line.t === "discard") {
772
+ discardedIds.add(line.id);
773
+ records.delete(line.id);
774
+ return;
775
+ }
776
+ const record = records.get(line.id);
777
+ if (record) {
778
+ record.meta = { ...record.meta, segments: line.segments };
779
+ }
780
+ }
781
+ function ensureLoaded() {
782
+ if (loaded) return;
783
+ loaded = true;
784
+ if (!fs.existsSync(file)) return;
785
+ let raw;
786
+ try {
787
+ raw = fs.readFileSync(file, "utf8");
788
+ } catch (err) {
789
+ log(`[disk-delivery-store] read failed (treated as empty), file=${file}`, err);
790
+ return;
791
+ }
792
+ let lineNumber = 0;
793
+ for (const lineText of raw.split("\n")) {
794
+ lineNumber += 1;
795
+ const trimmed = lineText.trim();
796
+ if (trimmed === "") continue;
797
+ let line;
798
+ try {
799
+ line = JSON.parse(trimmed);
800
+ } catch {
801
+ log(`[disk-delivery-store] corrupted line skipped, file=${file}, line=${lineNumber}`);
802
+ continue;
803
+ }
804
+ if (!line || typeof line !== "object" || typeof line.t !== "string") {
805
+ log(`[disk-delivery-store] malformed line skipped, file=${file}, line=${lineNumber}`);
806
+ continue;
807
+ }
808
+ applyLine(line);
809
+ }
810
+ }
811
+ function appendLine(line) {
812
+ if (!dirCreated) {
813
+ fs.mkdirSync(dir, { recursive: true });
814
+ dirCreated = true;
815
+ }
816
+ fs.appendFileSync(file, serializeLine(line));
817
+ }
818
+ return {
819
+ persist(record) {
820
+ ensureLoaded();
821
+ appendLine({ t: "record", r: record });
822
+ applyLine({ t: "record", r: record });
823
+ },
824
+ ack(id) {
825
+ ensureLoaded();
826
+ appendLine({ t: "ack", id, at: Date.now() });
827
+ applyLine({ t: "ack", id, at: 0 });
828
+ },
829
+ discard(id) {
830
+ ensureLoaded();
831
+ appendLine({ t: "discard", id, at: Date.now() });
832
+ applyLine({ t: "discard", id, at: 0 });
833
+ },
834
+ loadUndelivered() {
835
+ ensureLoaded();
836
+ const out = [];
837
+ for (const record of records.values()) {
838
+ if (terminalIds.has(record.id) || discardedIds.has(record.id)) continue;
839
+ out.push(record);
840
+ }
841
+ return out;
842
+ },
843
+ loadTerminalIds() {
844
+ ensureLoaded();
845
+ return [...terminalIds];
846
+ },
847
+ hasTerminal(id) {
848
+ ensureLoaded();
849
+ return terminalIds.has(id);
850
+ },
851
+ attachSegments(id, segments) {
852
+ ensureLoaded();
853
+ appendLine({ t: "meta", id, segments: [...segments] });
854
+ applyLine({ t: "meta", id, segments: [...segments] });
855
+ }
856
+ };
699
857
  }
700
858
  export {
701
859
  DeliveryReclaimError,
702
- createDelivery
860
+ createDelivery,
861
+ createDiskDeliveryStore
703
862
  };