@real-life/trust-protocol 0.5.0 → 0.6.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/CHANGELOG.md +72 -0
- package/README.md +79 -34
- package/dist/adapters/vti-mediator.d.ts +18 -5
- package/dist/adapters/vti-mediator.d.ts.map +1 -1
- package/dist/adapters/vti-mediator.js +143 -17
- package/dist/adapters/vti-mediator.js.map +1 -1
- package/dist/codec.d.ts +2 -9
- package/dist/codec.d.ts.map +1 -1
- package/dist/codec.js +61 -96
- package/dist/codec.js.map +1 -1
- package/dist/engine.d.ts +62 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +289 -15
- package/dist/engine.js.map +1 -1
- package/dist/holder-api.d.ts +15 -1
- package/dist/holder-api.d.ts.map +1 -1
- package/dist/holder-api.js +28 -3
- package/dist/holder-api.js.map +1 -1
- package/dist/{holder-state-v2.d.ts → holder-state-v3.d.ts} +210 -207
- package/dist/holder-state-v3.d.ts.map +1 -0
- package/dist/holder-state-v3.js +2 -0
- package/dist/holder-state-v3.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +1 -2
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js.map +1 -1
- package/dist/results.d.ts +5 -1
- package/dist/results.d.ts.map +1 -1
- package/dist/results.js.map +1 -1
- package/dist/state.d.ts +22 -9
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js.map +1 -1
- package/package.json +4 -6
- package/schemas/{holder-state-v2.schema.json → holder-state-v3.schema.json} +50 -5
- package/dist/holder-state-v1.d.ts +0 -801
- package/dist/holder-state-v1.d.ts.map +0 -1
- package/dist/holder-state-v1.js +0 -2
- package/dist/holder-state-v1.js.map +0 -1
- package/dist/holder-state-v2.d.ts.map +0 -1
- package/dist/holder-state-v2.js +0 -2
- package/dist/holder-state-v2.js.map +0 -1
- package/schemas/holder-state-v1.schema.json +0 -3880
package/dist/engine.js
CHANGED
|
@@ -75,13 +75,48 @@
|
|
|
75
75
|
// stages again, which re-checks the effect cache at stage 4
|
|
76
76
|
// - an ack is stored sealed (StoredAck: envelope, digest, recipient) in
|
|
77
77
|
// its effect, so every re-send is byte-identical down to the envelope.
|
|
78
|
-
// It stays `sent: false` until
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
78
|
+
// It stays `sent: false` until a service takes it (report accepted or
|
|
79
|
+
// delivered: `sentBy: 'accepted'`), the adapter reports it failed
|
|
80
|
+
// (`'failed'`), or its horizon passes (effect commit + horizonMs:
|
|
81
|
+
// `'horizon'`, no event: acks have no trias) — Delivery RLTP-DEL-6330.
|
|
82
|
+
// An awaiting-transport report keeps it unsent and records `held`; a
|
|
83
|
+
// held ack is not re-offered by resume (the adapter has it) and stays
|
|
84
|
+
// in pendingTransport. Unsent and not held, it has the outbox's retry
|
|
85
|
+
// schedule: attempt 1 at the receive, re-offered by resume when due
|
|
86
|
+
// (attempt + 1, backoff), counted into nextDueAt. Sent or not, it stays
|
|
87
|
+
// with its effect until the effect expires (RLTP-DEL-6335). A
|
|
88
|
+
// duplicate's re-send carries attempt + 1 and does not move the
|
|
89
|
+
// schedule; it commits the answer it makes due (6330), else nothing
|
|
90
|
+
// beyond lastWhen
|
|
91
|
+
// - custody (Delivery RLTP-DEL-6120 to 6135): pendingTransport(when) is
|
|
92
|
+
// the set the holder keeps for transport — every outbox item neither
|
|
93
|
+
// terminal nor accepted, held ones included, and every unsent ack —
|
|
94
|
+
// each with its deadline (notAfter: the horizon from the first
|
|
95
|
+
// submission, or the type's sender lifetime if earlier) and without
|
|
96
|
+
// those whose deadline has passed at `when`, whether or not a resume
|
|
97
|
+
// has recorded the give-up yet. It answers only after a resume of this
|
|
98
|
+
// engine has completed (a resume that throws withdraws that again), and
|
|
99
|
+
// its entries are issued to this engine: mayHandOver(entry, when)
|
|
100
|
+
// accepts only an entry this engine issued, re-reads the store and says
|
|
101
|
+
// yes only while the engine is open and the entry is still pending
|
|
102
|
+
// there with the same envelope before its deadline. handOver(entry,
|
|
103
|
+
// when, start) is the same check with the deposit's start inside the
|
|
104
|
+
// queue: nothing of this engine runs between the check and the start.
|
|
105
|
+
// A duplicate makes an answer due, stored with the ack (answers,
|
|
106
|
+
// answerDue: kept across a lock): a `resend` entry numbered by it,
|
|
107
|
+
// beside the ack's retries and independent of their state, deadline the
|
|
108
|
+
// effect's retention; the begun hand-over ends it (a commit): it is one
|
|
109
|
+
// hand-over with no retries (Delivery 6330), so a deposit the wire
|
|
110
|
+
// refuses is not tried again — the sender's next redelivery asks anew;
|
|
111
|
+
// a duplicate while one is due is answered by it (the same bytes); a resume that throws, its settling
|
|
112
|
+
// included, withdraws the authority until one completes. The check is
|
|
113
|
+
// this engine's: a second engine over the same store commits through
|
|
114
|
+
// CAS but is not serialized with this one's hand-overs, so one opening
|
|
115
|
+
// per store at a time is the app's to keep (its writer lock). close() ends the
|
|
116
|
+
// engine: every later operation rejects with InfraError('closed'),
|
|
117
|
+
// mayHandOver answers false, and no entry issued before authorizes
|
|
118
|
+
// anything. An operation already running when close() is called
|
|
119
|
+
// completes
|
|
85
120
|
// - outbound of one commit, in order: outbox re-emissions, released
|
|
86
121
|
// acks, new documents
|
|
87
122
|
// - resume runs duties after the outbox, ordered by dueAt (absent: due
|
|
@@ -355,6 +390,7 @@ function renameAcks(effects, renames) {
|
|
|
355
390
|
}
|
|
356
391
|
return out ?? effects;
|
|
357
392
|
}
|
|
393
|
+
const closedError = () => new InfraError('closed', 'the holder was closed');
|
|
358
394
|
const checkWhen = (when) => {
|
|
359
395
|
if (!Number.isSafeInteger(when) || when < 0)
|
|
360
396
|
throw new TypeError(`when must be non-negative integer milliseconds, got ${String(when)}`);
|
|
@@ -404,6 +440,12 @@ class HolderEngine {
|
|
|
404
440
|
#chain = Promise.resolve();
|
|
405
441
|
/** a commit whose outcome is unknown, settled before the next operation */
|
|
406
442
|
#unsettled = null;
|
|
443
|
+
/** close() was called: nothing more runs, nothing issued authorizes */
|
|
444
|
+
#closed = false;
|
|
445
|
+
/** a resume of this engine completed (and none threw since) */
|
|
446
|
+
#resumed = false;
|
|
447
|
+
/** the transport entries this engine issued (RLTP-DEL-6130) */
|
|
448
|
+
#issued = new WeakSet();
|
|
407
449
|
constructor(ports, crypto, modules, opts = {}) {
|
|
408
450
|
if (opts.check !== undefined && typeof opts.check !== 'function')
|
|
409
451
|
throw new TypeError('check must be a function');
|
|
@@ -601,7 +643,14 @@ class HolderEngine {
|
|
|
601
643
|
}
|
|
602
644
|
// ── the queue ────────────────────────────────────────────────────────
|
|
603
645
|
#enqueue(fn) {
|
|
604
|
-
|
|
646
|
+
if (this.#closed)
|
|
647
|
+
return Promise.reject(closedError());
|
|
648
|
+
const p = this.#chain.then(async () => {
|
|
649
|
+
if (this.#closed)
|
|
650
|
+
throw closedError();
|
|
651
|
+
await this.#settle();
|
|
652
|
+
return fn();
|
|
653
|
+
});
|
|
605
654
|
this.#chain = p.then(() => undefined, () => undefined);
|
|
606
655
|
return p;
|
|
607
656
|
}
|
|
@@ -1201,11 +1250,14 @@ class HolderEngine {
|
|
|
1201
1250
|
const expiry = Math.min(a.at + p.horizonMs, Number.MAX_SAFE_INTEGER);
|
|
1202
1251
|
effects ??= { ...cur.effects };
|
|
1203
1252
|
if (when >= expiry) {
|
|
1204
|
-
const { nextAttemptAt: _, ...rest } = a;
|
|
1253
|
+
const { nextAttemptAt: _, held: _h, ...rest } = a;
|
|
1205
1254
|
effects[d] = { ...cur.effects[d], ack: { ...rest, sent: true, sentBy: 'horizon' } };
|
|
1206
1255
|
continue;
|
|
1207
1256
|
}
|
|
1208
1257
|
due(expiry);
|
|
1258
|
+
// held: the adapter has it (awaiting-transport); pendingTransport offers it
|
|
1259
|
+
if (a.held !== undefined)
|
|
1260
|
+
continue;
|
|
1209
1261
|
if (a.nextAttemptAt !== undefined && a.nextAttemptAt > when) {
|
|
1210
1262
|
due(a.nextAttemptAt);
|
|
1211
1263
|
continue;
|
|
@@ -1230,21 +1282,57 @@ class HolderEngine {
|
|
|
1230
1282
|
},
|
|
1231
1283
|
};
|
|
1232
1284
|
}
|
|
1285
|
+
/** an answer's hand-over began: it is no longer due (handOver) */
|
|
1286
|
+
#answeredDef() {
|
|
1287
|
+
return {
|
|
1288
|
+
human: false,
|
|
1289
|
+
apply(state, _p, input) {
|
|
1290
|
+
const eff = Object.keys(state.effects).find((d) => state.effects[d].ack?.digest === input.digest);
|
|
1291
|
+
if (eff === undefined)
|
|
1292
|
+
return { state: state };
|
|
1293
|
+
const a = state.effects[eff].ack;
|
|
1294
|
+
if (a.answerDue !== true || a.answers !== input.n)
|
|
1295
|
+
return { state: state };
|
|
1296
|
+
const { answerDue: _d, ...rest } = a;
|
|
1297
|
+
return { state: { ...state, effects: { ...state.effects, [eff]: { ...state.effects[eff], ack: rest } } } };
|
|
1298
|
+
},
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1233
1301
|
#reportDef() {
|
|
1234
1302
|
const failed = (st, digests, when) => this.#failed(st, digests, when);
|
|
1235
1303
|
return {
|
|
1236
1304
|
human: false,
|
|
1237
1305
|
apply(state, _p, report, when) {
|
|
1238
1306
|
if (!Object.hasOwn(state.outbox, report.digest)) {
|
|
1239
|
-
// an ack (outbox-less
|
|
1307
|
+
// an ack (outbox-less, no trias; Delivery RLTP-DEL-6330): sent once
|
|
1308
|
+
// a service took it or the adapter gave it up; awaiting-transport
|
|
1309
|
+
// records that the adapter holds it and ends nothing
|
|
1240
1310
|
const eff = Object.keys(state.effects).find((d) => state.effects[d].ack?.digest === report.digest);
|
|
1241
1311
|
if (eff === undefined)
|
|
1242
1312
|
return { ok: false, code: 'unknown-digest' };
|
|
1243
1313
|
const e = state.effects[eff];
|
|
1244
|
-
|
|
1314
|
+
const a = e.ack;
|
|
1315
|
+
if (a.sent)
|
|
1245
1316
|
return { state: state };
|
|
1246
|
-
const { nextAttemptAt: _, ...
|
|
1247
|
-
|
|
1317
|
+
const { nextAttemptAt: _, held: _h, ...rest } = a;
|
|
1318
|
+
let ack;
|
|
1319
|
+
switch (report.status) {
|
|
1320
|
+
case 'awaiting-transport':
|
|
1321
|
+
if (a.held === report.reason)
|
|
1322
|
+
return { state: state };
|
|
1323
|
+
ack = { ...rest, held: report.reason };
|
|
1324
|
+
break;
|
|
1325
|
+
case 'accepted':
|
|
1326
|
+
case 'delivered':
|
|
1327
|
+
ack = { ...rest, sent: true, sentBy: 'accepted' };
|
|
1328
|
+
break;
|
|
1329
|
+
case 'failed':
|
|
1330
|
+
ack = { ...rest, sent: true, sentBy: 'failed' };
|
|
1331
|
+
break;
|
|
1332
|
+
default:
|
|
1333
|
+
throw new TypeError(`unknown report status ${String(report.status)}`);
|
|
1334
|
+
}
|
|
1335
|
+
return { state: { ...state, effects: { ...state.effects, [eff]: { ...e, ack } } } };
|
|
1248
1336
|
}
|
|
1249
1337
|
const it = state.outbox[report.digest];
|
|
1250
1338
|
const trias = triasOf(it);
|
|
@@ -1311,8 +1399,14 @@ class HolderEngine {
|
|
|
1311
1399
|
case 'duplicate': {
|
|
1312
1400
|
// the stored ack, byte-identical; a terminal document has none
|
|
1313
1401
|
// (its outbound attempt is one past the schedule's, which it does not move)
|
|
1402
|
+
// and an answer due (Delivery RLTP-DEL-6330): a hand-over of its
|
|
1403
|
+
// own, kept across a lock until it begins; while one is due, a
|
|
1404
|
+
// further duplicate is answered by it (the same bytes)
|
|
1314
1405
|
const a = state.effects[plan.digest].ack;
|
|
1315
|
-
|
|
1406
|
+
const next = a && a.answerDue === undefined
|
|
1407
|
+
? { ...state, effects: { ...state.effects, [plan.digest]: { ...state.effects[plan.digest], ack: { ...a, answers: (a.answers ?? 0) + 1, answerDue: true } } } }
|
|
1408
|
+
: state;
|
|
1409
|
+
return { state: next, release: a ? [ackOutbound(a, a.attempt + 1)] : [], detail: { disposition: 'duplicate-known', digest: plan.digest } };
|
|
1316
1410
|
}
|
|
1317
1411
|
case 'ack': {
|
|
1318
1412
|
// terminal: the sender-status update and its cache entry, no ack;
|
|
@@ -1428,12 +1522,16 @@ class HolderEngine {
|
|
|
1428
1522
|
catch (e) {
|
|
1429
1523
|
return Promise.reject(e);
|
|
1430
1524
|
}
|
|
1525
|
+
// fail closed: a resume that throws anywhere — the settling of an
|
|
1526
|
+
// earlier commit included — leaves nothing to go out until one completes
|
|
1431
1527
|
return this.#enqueue(async () => {
|
|
1528
|
+
this.#resumed = false;
|
|
1432
1529
|
const r = await this.#run(this.#builtins.resume, { kind: 'resume' }, null, when);
|
|
1433
1530
|
if (isHalt(r) || !r.ok)
|
|
1434
1531
|
throw new Error('engine: resume does not refuse');
|
|
1532
|
+
this.#resumed = true;
|
|
1435
1533
|
return { outbound: r.outbound, events: r.events, ...(r.nextDueAt !== undefined ? { nextDueAt: r.nextDueAt } : {}) };
|
|
1436
|
-
});
|
|
1534
|
+
}).catch((e) => { this.#resumed = false; throw e; });
|
|
1437
1535
|
}
|
|
1438
1536
|
report(report, when) {
|
|
1439
1537
|
let bound;
|
|
@@ -1491,6 +1589,182 @@ class HolderEngine {
|
|
|
1491
1589
|
return Object.freeze({ state, commitSeq: state.commitSeq });
|
|
1492
1590
|
});
|
|
1493
1591
|
}
|
|
1592
|
+
pendingTransport(when) {
|
|
1593
|
+
try {
|
|
1594
|
+
checkWhen(when);
|
|
1595
|
+
}
|
|
1596
|
+
catch (e) {
|
|
1597
|
+
return Promise.reject(e);
|
|
1598
|
+
}
|
|
1599
|
+
return this.#enqueue(async () => {
|
|
1600
|
+
if (!this.#resumed)
|
|
1601
|
+
return Object.freeze({ ok: false, code: 'not-resumed' });
|
|
1602
|
+
const { state } = await this.#read();
|
|
1603
|
+
// the holder's time never runs back: an earlier `when` reads at lastWhen
|
|
1604
|
+
const at = Math.max(when, state.lastWhen);
|
|
1605
|
+
const entries = transportEntries(state, at, this.#policy.horizonMs).map((e) => {
|
|
1606
|
+
const f = freezeDeep(e);
|
|
1607
|
+
this.#issued.add(f);
|
|
1608
|
+
return f;
|
|
1609
|
+
});
|
|
1610
|
+
return Object.freeze({ ok: true, commitSeq: state.commitSeq, at, entries: Object.freeze(entries) });
|
|
1611
|
+
});
|
|
1612
|
+
}
|
|
1613
|
+
mayHandOver(entry, when) {
|
|
1614
|
+
try {
|
|
1615
|
+
checkWhen(when);
|
|
1616
|
+
}
|
|
1617
|
+
catch (e) {
|
|
1618
|
+
return Promise.reject(e);
|
|
1619
|
+
}
|
|
1620
|
+
if (this.#closed || entry === null || typeof entry !== 'object' || !this.#issued.has(entry))
|
|
1621
|
+
return Promise.resolve(false);
|
|
1622
|
+
return this.#enqueue(() => this.#mayGo(entry, when)).catch((e) => {
|
|
1623
|
+
if (e instanceof InfraError && e.code === 'closed')
|
|
1624
|
+
return false;
|
|
1625
|
+
throw e;
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
handOver(entry, when, start) {
|
|
1629
|
+
try {
|
|
1630
|
+
checkWhen(when);
|
|
1631
|
+
}
|
|
1632
|
+
catch (e) {
|
|
1633
|
+
return Promise.reject(e);
|
|
1634
|
+
}
|
|
1635
|
+
if (typeof start !== 'function')
|
|
1636
|
+
return Promise.reject(new TypeError('handOver needs a start function'));
|
|
1637
|
+
if (this.#closed || entry === null || typeof entry !== 'object' || !this.#issued.has(entry))
|
|
1638
|
+
return Promise.resolve(false);
|
|
1639
|
+
return this.#enqueue(async () => {
|
|
1640
|
+
if (!this.#resumed)
|
|
1641
|
+
return false;
|
|
1642
|
+
const { state } = await this.#read();
|
|
1643
|
+
// no await from here to start(): close() and every other operation
|
|
1644
|
+
// of this engine come before the check or after the start
|
|
1645
|
+
if (!this.#passes(state, entry, when))
|
|
1646
|
+
return false;
|
|
1647
|
+
if (start() !== true)
|
|
1648
|
+
return false;
|
|
1649
|
+
// an answer's hand-over has begun: it is no longer due (a later
|
|
1650
|
+
// duplicate grants the next); a commit that fails leaves it due, and
|
|
1651
|
+
// it may go out once more — the receiver absorbs it
|
|
1652
|
+
if (entry.resend !== undefined) {
|
|
1653
|
+
try {
|
|
1654
|
+
await this.#run(this.#answeredDef(), { kind: 'report', status: 'answered' }, { digest: entry.digest, n: entry.resend }, Math.max(when, state.lastWhen));
|
|
1655
|
+
}
|
|
1656
|
+
catch { /* stays due */ }
|
|
1657
|
+
}
|
|
1658
|
+
return true;
|
|
1659
|
+
}).catch((e) => {
|
|
1660
|
+
if (e instanceof InfraError && e.code === 'closed')
|
|
1661
|
+
return false;
|
|
1662
|
+
throw e;
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
1665
|
+
/** the gate's check, inside the queue: open, resumed, still pending in
|
|
1666
|
+
* the store as issued, before its deadline */
|
|
1667
|
+
async #mayGo(entry, when) {
|
|
1668
|
+
if (!this.#resumed)
|
|
1669
|
+
return false;
|
|
1670
|
+
const { state } = await this.#read();
|
|
1671
|
+
return this.#passes(state, entry, when);
|
|
1672
|
+
}
|
|
1673
|
+
/** synchronous: open, resumed, the entry pending in `state` as issued,
|
|
1674
|
+
* before its deadline at max(when, lastWhen) */
|
|
1675
|
+
#passes(state, entry, when) {
|
|
1676
|
+
if (this.#closed || !this.#resumed)
|
|
1677
|
+
return false;
|
|
1678
|
+
const now = transportEntryOf(state, entry.kind, entry.digest, Math.max(when, state.lastWhen), this.#policy.horizonMs, entry.resend);
|
|
1679
|
+
return now !== null && now.resend === entry.resend && jcs(now.envelope) === jcs(entry.envelope);
|
|
1680
|
+
}
|
|
1681
|
+
close() {
|
|
1682
|
+
this.#closed = true;
|
|
1683
|
+
return this.#chain.then(() => undefined, () => undefined);
|
|
1684
|
+
}
|
|
1685
|
+
get closed() { return this.#closed; }
|
|
1686
|
+
}
|
|
1687
|
+
/** The send deadline of an outbox item: the horizon from its first
|
|
1688
|
+
* submission, or the sender lifetime its type declared if that is
|
|
1689
|
+
* earlier (#giveUp's two bounds as one instant). */
|
|
1690
|
+
const notAfterOf = (it, horizonMs) => {
|
|
1691
|
+
if (!it.history.length)
|
|
1692
|
+
return null;
|
|
1693
|
+
const expiry = Math.min(it.history[0].at + horizonMs, Number.MAX_SAFE_INTEGER);
|
|
1694
|
+
return it.expiresAt !== undefined ? Math.min(it.expiresAt, expiry) : expiry;
|
|
1695
|
+
};
|
|
1696
|
+
/** The transport entry of one pending document or ack at `when`, or null:
|
|
1697
|
+
* terminal, accepted, sent, past its deadline, or absent (RLTP-DEL-6135:
|
|
1698
|
+
* absent means stop). */
|
|
1699
|
+
function transportEntryOf(state, kind, digest, when, horizonMs, resend) {
|
|
1700
|
+
if (kind === 'document') {
|
|
1701
|
+
if (!Object.hasOwn(state.outbox, digest))
|
|
1702
|
+
return null;
|
|
1703
|
+
const it = state.outbox[digest];
|
|
1704
|
+
const trias = triasOf(it);
|
|
1705
|
+
if (trias !== 'submitted')
|
|
1706
|
+
return null;
|
|
1707
|
+
const notAfter = notAfterOf(it, horizonMs);
|
|
1708
|
+
if (notAfter === null || when >= notAfter)
|
|
1709
|
+
return null;
|
|
1710
|
+
const last = it.history[it.history.length - 1];
|
|
1711
|
+
return structuredClone({
|
|
1712
|
+
digest, kind, envelope: it.envelope, type: it.type, threadId: it.threadId, recipient: it.recipient, attempt: it.attempt, since: it.history[0].at, notAfter,
|
|
1713
|
+
...(last.status === 'awaiting-transport' ? { held: last.reason } : it.nextAttemptAt !== undefined ? { nextAttemptAt: it.nextAttemptAt } : {}),
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1716
|
+
for (const d of Object.keys(state.effects)) {
|
|
1717
|
+
const a = state.effects[d].ack;
|
|
1718
|
+
if (a === null || a.digest !== digest)
|
|
1719
|
+
continue;
|
|
1720
|
+
if (resend !== undefined) {
|
|
1721
|
+
// a duplicate's answer: its own hand-over, apart from the retries and
|
|
1722
|
+
// whatever state they are in, while due and the effect keeps the ack (6335)
|
|
1723
|
+
if (a.answerDue !== true || a.answers !== resend || when >= state.effects[d].expiresAt)
|
|
1724
|
+
return null;
|
|
1725
|
+
return structuredClone({
|
|
1726
|
+
digest, kind, envelope: a.envelope, type: ACK_TYPE, threadId: a.threadId, recipient: a.recipient, attempt: a.attempt, since: a.at,
|
|
1727
|
+
notAfter: state.effects[d].expiresAt, resend,
|
|
1728
|
+
});
|
|
1729
|
+
}
|
|
1730
|
+
const notAfter = Math.min(a.at + horizonMs, Number.MAX_SAFE_INTEGER);
|
|
1731
|
+
if (a.sent || when >= notAfter)
|
|
1732
|
+
return null;
|
|
1733
|
+
return structuredClone({
|
|
1734
|
+
digest, kind, envelope: a.envelope, type: ACK_TYPE, threadId: a.threadId, recipient: a.recipient, attempt: a.attempt, since: a.at, notAfter,
|
|
1735
|
+
...(a.held !== undefined ? { held: a.held } : a.nextAttemptAt !== undefined ? { nextAttemptAt: a.nextAttemptAt } : {}),
|
|
1736
|
+
});
|
|
1737
|
+
}
|
|
1738
|
+
return null;
|
|
1739
|
+
}
|
|
1740
|
+
/** Everything the holder keeps for transport at `when`: the outbox items
|
|
1741
|
+
* by due time, then digest, then the unsent acks by effect digest. */
|
|
1742
|
+
function transportEntries(state, when, horizonMs) {
|
|
1743
|
+
const out = [];
|
|
1744
|
+
const order = Object.keys(state.outbox).sort((a, b) => ((state.outbox[a].nextAttemptAt ?? 0) - (state.outbox[b].nextAttemptAt ?? 0)) || (a < b ? -1 : a > b ? 1 : 0));
|
|
1745
|
+
for (const d of order) {
|
|
1746
|
+
const e = transportEntryOf(state, 'document', d, when, horizonMs);
|
|
1747
|
+
if (e)
|
|
1748
|
+
out.push(e);
|
|
1749
|
+
}
|
|
1750
|
+
for (const d of Object.keys(state.effects).sort()) {
|
|
1751
|
+
const a = state.effects[d].ack;
|
|
1752
|
+
if (a === null)
|
|
1753
|
+
continue;
|
|
1754
|
+
const e = transportEntryOf(state, 'ack', a.digest, when, horizonMs);
|
|
1755
|
+
if (e)
|
|
1756
|
+
out.push(e);
|
|
1757
|
+
}
|
|
1758
|
+
// the duplicates' answers due, each its own entry beside the retries
|
|
1759
|
+
for (const d of Object.keys(state.effects).sort()) {
|
|
1760
|
+
const a = state.effects[d].ack;
|
|
1761
|
+
if (a === null || a.answerDue !== true)
|
|
1762
|
+
continue;
|
|
1763
|
+
const e = transportEntryOf(state, 'ack', a.digest, when, horizonMs, a.answers);
|
|
1764
|
+
if (e)
|
|
1765
|
+
out.push(e);
|
|
1766
|
+
}
|
|
1767
|
+
return out;
|
|
1494
1768
|
}
|
|
1495
1769
|
/**
|
|
1496
1770
|
* Build an engine over the ports and modules: resolves and probes
|