@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.d.mts +383 -70
- package/dist/index.mjs +307 -148
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
|
|
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
|
-
|
|
403
|
-
|
|
404
|
-
|
|
387
|
+
store?.discard(e.id);
|
|
388
|
+
removeActive(e);
|
|
389
|
+
changed = true;
|
|
405
390
|
}
|
|
406
|
-
if (
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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(
|
|
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(
|
|
430
|
+
function scheduleFlush() {
|
|
444
431
|
if (disposed || !hasQueuedEntries()) return;
|
|
445
432
|
if (inFlight) return;
|
|
446
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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: [...
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|