@enbox/agent 0.5.15 → 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/dist/browser.mjs +11 -11
- package/dist/browser.mjs.map +4 -4
- package/dist/esm/dwn-api.js +433 -33
- package/dist/esm/dwn-api.js.map +1 -1
- package/dist/esm/dwn-encryption.js +131 -12
- package/dist/esm/dwn-encryption.js.map +1 -1
- package/dist/esm/dwn-key-delivery.js +64 -47
- package/dist/esm/dwn-key-delivery.js.map +1 -1
- package/dist/esm/enbox-connect-protocol.js +400 -3
- package/dist/esm/enbox-connect-protocol.js.map +1 -1
- package/dist/esm/permissions-api.js +11 -1
- package/dist/esm/permissions-api.js.map +1 -1
- package/dist/esm/sync-closure-resolver.js +8 -1
- package/dist/esm/sync-closure-resolver.js.map +1 -1
- package/dist/esm/sync-engine-level.js +407 -6
- package/dist/esm/sync-engine-level.js.map +1 -1
- package/dist/esm/sync-messages.js +10 -3
- package/dist/esm/sync-messages.js.map +1 -1
- package/dist/types/dwn-api.d.ts +159 -0
- package/dist/types/dwn-api.d.ts.map +1 -1
- package/dist/types/dwn-encryption.d.ts +39 -2
- package/dist/types/dwn-encryption.d.ts.map +1 -1
- package/dist/types/dwn-key-delivery.d.ts +1 -9
- package/dist/types/dwn-key-delivery.d.ts.map +1 -1
- package/dist/types/enbox-connect-protocol.d.ts +166 -1
- package/dist/types/enbox-connect-protocol.d.ts.map +1 -1
- package/dist/types/permissions-api.d.ts.map +1 -1
- package/dist/types/sync-closure-resolver.d.ts.map +1 -1
- package/dist/types/sync-engine-level.d.ts +45 -1
- package/dist/types/sync-engine-level.d.ts.map +1 -1
- package/dist/types/sync-messages.d.ts +2 -2
- package/dist/types/sync-messages.d.ts.map +1 -1
- package/dist/types/types/permissions.d.ts +9 -0
- package/dist/types/types/permissions.d.ts.map +1 -1
- package/dist/types/types/sync.d.ts +70 -2
- package/dist/types/types/sync.d.ts.map +1 -1
- package/package.json +5 -4
- package/src/dwn-api.ts +494 -38
- package/src/dwn-encryption.ts +160 -11
- package/src/dwn-key-delivery.ts +73 -61
- package/src/enbox-connect-protocol.ts +575 -6
- package/src/permissions-api.ts +13 -1
- package/src/sync-closure-resolver.ts +7 -1
- package/src/sync-engine-level.ts +368 -4
- package/src/sync-messages.ts +14 -5
- package/src/types/permissions.ts +9 -0
- package/src/types/sync.ts +86 -2
|
@@ -179,6 +179,10 @@ export class SyncEngineLevel {
|
|
|
179
179
|
}
|
|
180
180
|
return this._ledger;
|
|
181
181
|
}
|
|
182
|
+
/** LevelDB sublevel for permanently failed messages (dead letters). */
|
|
183
|
+
get _deadLetters() {
|
|
184
|
+
return this._db.sublevel('deadLetters');
|
|
185
|
+
}
|
|
182
186
|
/**
|
|
183
187
|
* Retrieves the `EnboxPlatformAgent` execution context.
|
|
184
188
|
*
|
|
@@ -475,6 +479,9 @@ export class SyncEngineLevel {
|
|
|
475
479
|
*/
|
|
476
480
|
startLiveSync(intervalMilliseconds) {
|
|
477
481
|
return __awaiter(this, void 0, void 0, function* () {
|
|
482
|
+
// Step 0: Register browser connectivity listeners for instant recovery
|
|
483
|
+
// on network switch, sleep/wake, or tab foregrounding. No-op in Node.
|
|
484
|
+
this.startBrowserConnectivityListeners();
|
|
478
485
|
// Step 1: Initial SMT catch-up.
|
|
479
486
|
try {
|
|
480
487
|
yield this.sync();
|
|
@@ -842,6 +849,11 @@ export class SyncEngineLevel {
|
|
|
842
849
|
const prevRepairConnectivity = link.connectivity;
|
|
843
850
|
link.connectivity = 'online';
|
|
844
851
|
yield this.ledger.setStatus(link, 'live');
|
|
852
|
+
// Auto-clear dead letters for this link — repair has verified
|
|
853
|
+
// convergence via SMT reconciliation so any previously recorded
|
|
854
|
+
// failures (closure, push-exhausted, pull-processing) for this
|
|
855
|
+
// specific link are no longer current.
|
|
856
|
+
void this.clearDeadLettersForLink(did, dwnUrl, protocol);
|
|
845
857
|
this.emitEvent({ type: 'repair:completed', tenantDid: did, remoteEndpoint: dwnUrl, protocol });
|
|
846
858
|
if (prevRepairConnectivity !== 'online') {
|
|
847
859
|
this.emitEvent({ type: 'link:connectivity-change', tenantDid: did, remoteEndpoint: dwnUrl, protocol, from: prevRepairConnectivity, to: 'online' });
|
|
@@ -959,8 +971,106 @@ export class SyncEngineLevel {
|
|
|
959
971
|
/**
|
|
960
972
|
* Tears down all live subscriptions and push listeners.
|
|
961
973
|
*/
|
|
974
|
+
// ---------------------------------------------------------------------------
|
|
975
|
+
// Browser connectivity: online/offline + visibilitychange
|
|
976
|
+
// ---------------------------------------------------------------------------
|
|
977
|
+
/**
|
|
978
|
+
* Registers browser `online`, `offline`, and `visibilitychange` event
|
|
979
|
+
* listeners to detect connectivity changes that WebSocket `close` events
|
|
980
|
+
* miss (NAT timeout, network switch, sleep/wake). Safe to call in Node —
|
|
981
|
+
* the guards skip registration when browser APIs are unavailable.
|
|
982
|
+
*/
|
|
983
|
+
startBrowserConnectivityListeners() {
|
|
984
|
+
this.stopBrowserConnectivityListeners();
|
|
985
|
+
// Guard: only run in browser environments with the required APIs.
|
|
986
|
+
if (typeof globalThis.addEventListener !== 'function') {
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
const generation = this._engineGeneration;
|
|
990
|
+
this._onOnline = () => {
|
|
991
|
+
if (this._engineGeneration !== generation) {
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
console.info('SyncEngineLevel: browser online — triggering immediate integrity check');
|
|
995
|
+
// Don't set _connectivityState here — individual links will transition
|
|
996
|
+
// to online as their WebSocket connections actually recover during the
|
|
997
|
+
// sync below. The public getter uses per-link aggregation.
|
|
998
|
+
// Kick off an immediate SMT reconciliation to catch up after being offline.
|
|
999
|
+
if (!this._syncLock) {
|
|
1000
|
+
this.sync().catch((err) => {
|
|
1001
|
+
console.error('SyncEngineLevel: post-online sync failed', err);
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
};
|
|
1005
|
+
this._onOffline = () => {
|
|
1006
|
+
if (this._engineGeneration !== generation) {
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
console.info('SyncEngineLevel: browser offline');
|
|
1010
|
+
this._connectivityState = 'offline';
|
|
1011
|
+
// Transition every active link to offline so the public
|
|
1012
|
+
// connectivityState getter (which aggregates per-link state)
|
|
1013
|
+
// reflects the browser's network status immediately.
|
|
1014
|
+
for (const link of this._activeLinks.values()) {
|
|
1015
|
+
const prev = link.connectivity;
|
|
1016
|
+
if (prev !== 'offline') {
|
|
1017
|
+
link.connectivity = 'offline';
|
|
1018
|
+
this.emitEvent({
|
|
1019
|
+
type: 'link:connectivity-change',
|
|
1020
|
+
tenantDid: link.tenantDid,
|
|
1021
|
+
remoteEndpoint: link.remoteEndpoint,
|
|
1022
|
+
protocol: link.protocol,
|
|
1023
|
+
from: prev,
|
|
1024
|
+
to: 'offline',
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
};
|
|
1029
|
+
this._onVisibilityChange = () => {
|
|
1030
|
+
if (this._engineGeneration !== generation) {
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
// Only act when the page becomes visible again — the user is back.
|
|
1034
|
+
if (typeof document === 'undefined' || document.visibilityState !== 'visible') {
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
console.info('SyncEngineLevel: page became visible — triggering integrity check');
|
|
1038
|
+
// The device may have slept and WebSockets may be dead. An immediate
|
|
1039
|
+
// sync via SMT reconciliation detects and repairs any divergence.
|
|
1040
|
+
if (!this._syncLock) {
|
|
1041
|
+
this.sync().catch((err) => {
|
|
1042
|
+
console.error('SyncEngineLevel: post-visibility sync failed', err);
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
};
|
|
1046
|
+
globalThis.addEventListener('online', this._onOnline);
|
|
1047
|
+
globalThis.addEventListener('offline', this._onOffline);
|
|
1048
|
+
if (typeof document !== 'undefined') {
|
|
1049
|
+
document.addEventListener('visibilitychange', this._onVisibilityChange);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
/** Removes browser connectivity listeners if they were registered. */
|
|
1053
|
+
stopBrowserConnectivityListeners() {
|
|
1054
|
+
if (this._onOnline) {
|
|
1055
|
+
globalThis.removeEventListener('online', this._onOnline);
|
|
1056
|
+
this._onOnline = undefined;
|
|
1057
|
+
}
|
|
1058
|
+
if (this._onOffline) {
|
|
1059
|
+
globalThis.removeEventListener('offline', this._onOffline);
|
|
1060
|
+
this._onOffline = undefined;
|
|
1061
|
+
}
|
|
1062
|
+
if (this._onVisibilityChange && typeof document !== 'undefined') {
|
|
1063
|
+
document.removeEventListener('visibilitychange', this._onVisibilityChange);
|
|
1064
|
+
this._onVisibilityChange = undefined;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
// ---------------------------------------------------------------------------
|
|
1068
|
+
// Teardown
|
|
1069
|
+
// ---------------------------------------------------------------------------
|
|
962
1070
|
teardownLiveSync() {
|
|
963
1071
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1072
|
+
// Remove browser connectivity listeners before tearing down.
|
|
1073
|
+
this.stopBrowserConnectivityListeners();
|
|
964
1074
|
// Increment generation to invalidate all in-flight async operations
|
|
965
1075
|
// (repairs, retry timers, degraded-poll ticks). Any async work that
|
|
966
1076
|
// captured the previous generation will bail on its next checkpoint.
|
|
@@ -1079,6 +1189,7 @@ export class SyncEngineLevel {
|
|
|
1079
1189
|
// so multiple handlers can be in-flight concurrently. The ordinal tracker
|
|
1080
1190
|
// ensures the checkpoint advances only when all earlier deliveries are committed.
|
|
1081
1191
|
const subscriptionHandler = (subMessage) => __awaiter(this, void 0, void 0, function* () {
|
|
1192
|
+
var _a;
|
|
1082
1193
|
if (this._engineGeneration !== handlerGeneration) {
|
|
1083
1194
|
return;
|
|
1084
1195
|
}
|
|
@@ -1194,8 +1305,21 @@ export class SyncEngineLevel {
|
|
|
1194
1305
|
}
|
|
1195
1306
|
const closureResult = yield evaluateClosure(event.message, messageStore, link.scope, closureCtx);
|
|
1196
1307
|
if (!closureResult.complete) {
|
|
1308
|
+
const failureCode = closureResult.failure.code;
|
|
1309
|
+
const failureDetail = closureResult.failure.detail;
|
|
1197
1310
|
console.warn(`SyncEngineLevel: Closure incomplete for ${did} -> ${dwnUrl}: ` +
|
|
1198
|
-
`${
|
|
1311
|
+
`${failureCode} — ${failureDetail}`);
|
|
1312
|
+
// Record the message that triggered the closure failure.
|
|
1313
|
+
const closureCid = yield Message.getCid(event.message);
|
|
1314
|
+
void this.recordDeadLetter({
|
|
1315
|
+
messageCid: closureCid,
|
|
1316
|
+
tenantDid: did,
|
|
1317
|
+
remoteEndpoint: dwnUrl,
|
|
1318
|
+
protocol,
|
|
1319
|
+
category: 'closure',
|
|
1320
|
+
errorCode: failureCode,
|
|
1321
|
+
errorDetail: failureDetail,
|
|
1322
|
+
});
|
|
1199
1323
|
yield this.transitionToRepairing(cursorKey, link);
|
|
1200
1324
|
return;
|
|
1201
1325
|
}
|
|
@@ -1212,6 +1336,9 @@ export class SyncEngineLevel {
|
|
|
1212
1336
|
const pulledCid = yield Message.getCid(event.message);
|
|
1213
1337
|
this._recentlyPulledCids.set(`${pulledCid}|${dwnUrl}`, Date.now() + SyncEngineLevel.ECHO_SUPPRESS_TTL_MS);
|
|
1214
1338
|
this.evictExpiredEchoEntries();
|
|
1339
|
+
// Auto-clear any dead letter for this CID — it was processed
|
|
1340
|
+
// successfully, so a previous failure has been self-healed.
|
|
1341
|
+
this.clearFailedMessage(pulledCid, dwnUrl).catch(() => { });
|
|
1215
1342
|
// Mark this ordinal as committed and drain the checkpoint.
|
|
1216
1343
|
// Guard: if the link transitioned to repairing while this handler was
|
|
1217
1344
|
// in-flight (e.g., an earlier ordinal's handler failed concurrently),
|
|
@@ -1246,6 +1373,23 @@ export class SyncEngineLevel {
|
|
|
1246
1373
|
}
|
|
1247
1374
|
catch (error) {
|
|
1248
1375
|
console.error(`SyncEngineLevel: Error processing live-pull event for ${did}`, error);
|
|
1376
|
+
// Record the failing message in the dead letter store before
|
|
1377
|
+
// transitioning to repair. The CID identifies which specific
|
|
1378
|
+
// message caused the transition.
|
|
1379
|
+
try {
|
|
1380
|
+
const failedCid = yield Message.getCid(event.message);
|
|
1381
|
+
void this.recordDeadLetter({
|
|
1382
|
+
messageCid: failedCid,
|
|
1383
|
+
tenantDid: did,
|
|
1384
|
+
remoteEndpoint: dwnUrl,
|
|
1385
|
+
protocol,
|
|
1386
|
+
category: 'pull-processing',
|
|
1387
|
+
errorDetail: (_a = error.message) !== null && _a !== void 0 ? _a : String(error),
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1390
|
+
catch (_b) {
|
|
1391
|
+
// Best effort — don't let dead letter recording block repair.
|
|
1392
|
+
}
|
|
1249
1393
|
// A failed processRawMessage means local state is incomplete.
|
|
1250
1394
|
// Transition to repairing immediately — do NOT advance the checkpoint
|
|
1251
1395
|
// past this failure or let later ordinals commit past it. SMT
|
|
@@ -1432,6 +1576,7 @@ export class SyncEngineLevel {
|
|
|
1432
1576
|
}
|
|
1433
1577
|
flushPendingPushesForLink(linkKey) {
|
|
1434
1578
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1579
|
+
var _a, _b;
|
|
1435
1580
|
const pushRuntime = this._pushRuntimes.get(linkKey);
|
|
1436
1581
|
if (!pushRuntime) {
|
|
1437
1582
|
return;
|
|
@@ -1453,6 +1598,23 @@ export class SyncEngineLevel {
|
|
|
1453
1598
|
agent: this.agent,
|
|
1454
1599
|
permissionsApi: this._permissionsApi,
|
|
1455
1600
|
});
|
|
1601
|
+
// Auto-clear dead letters for CIDs that succeeded — a previously
|
|
1602
|
+
// failed message may have been repaired by reconciliation.
|
|
1603
|
+
for (const cid of result.succeeded) {
|
|
1604
|
+
this.clearFailedMessage(cid, dwnUrl).catch(() => { });
|
|
1605
|
+
}
|
|
1606
|
+
// Record permanently failed messages in the dead letter store.
|
|
1607
|
+
for (const entry of result.permanentlyFailed) {
|
|
1608
|
+
yield this.recordDeadLetter({
|
|
1609
|
+
messageCid: entry.cid,
|
|
1610
|
+
tenantDid: did,
|
|
1611
|
+
remoteEndpoint: dwnUrl,
|
|
1612
|
+
protocol,
|
|
1613
|
+
category: 'push-permanent',
|
|
1614
|
+
errorCode: String((_a = entry.statusCode) !== null && _a !== void 0 ? _a : ''),
|
|
1615
|
+
errorDetail: (_b = entry.detail) !== null && _b !== void 0 ? _b : 'permanent push failure',
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1456
1618
|
if (result.failed.length > 0) {
|
|
1457
1619
|
const failedSet = new Set(result.failed);
|
|
1458
1620
|
const failedEntries = pushEntries.filter((entry) => failedSet.has(entry.cid));
|
|
@@ -1504,7 +1666,18 @@ export class SyncEngineLevel {
|
|
|
1504
1666
|
const maxRetries = SyncEngineLevel.PUSH_RETRY_BACKOFF_MS.length;
|
|
1505
1667
|
const pushRuntime = this.getOrCreatePushRuntime(targetKey, pending);
|
|
1506
1668
|
if (pending.retryCount >= maxRetries) {
|
|
1507
|
-
// Retry budget exhausted —
|
|
1669
|
+
// Retry budget exhausted — record each CID as a dead letter and mark
|
|
1670
|
+
// the link dirty for reconciliation.
|
|
1671
|
+
for (const entry of pending.entries) {
|
|
1672
|
+
void this.recordDeadLetter({
|
|
1673
|
+
messageCid: entry.cid,
|
|
1674
|
+
tenantDid: pending.did,
|
|
1675
|
+
remoteEndpoint: pending.dwnUrl,
|
|
1676
|
+
protocol: pending.protocol,
|
|
1677
|
+
category: 'push-exhausted',
|
|
1678
|
+
errorDetail: `push retries exhausted after ${maxRetries} attempts`,
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1508
1681
|
if (pushRuntime.timer) {
|
|
1509
1682
|
clearTimeout(pushRuntime.timer);
|
|
1510
1683
|
}
|
|
@@ -1609,6 +1782,9 @@ export class SyncEngineLevel {
|
|
|
1609
1782
|
}
|
|
1610
1783
|
if (reconcileOutcome.converged) {
|
|
1611
1784
|
yield this.ledger.clearNeedsReconcile(link);
|
|
1785
|
+
// SMT roots match — this link is converged. Clear dead letters
|
|
1786
|
+
// scoped to this specific link (tenantDid, remoteEndpoint, protocol).
|
|
1787
|
+
void this.clearDeadLettersForLink(did, dwnUrl, protocol);
|
|
1612
1788
|
this.emitEvent({ type: 'reconcile:completed', tenantDid: did, remoteEndpoint: dwnUrl, protocol });
|
|
1613
1789
|
}
|
|
1614
1790
|
else {
|
|
@@ -2035,11 +2211,22 @@ export class SyncEngineLevel {
|
|
|
2035
2211
|
*/
|
|
2036
2212
|
pullMessages(_a) {
|
|
2037
2213
|
return __awaiter(this, arguments, void 0, function* ({ did, dwnUrl, delegateDid, protocol, messageCids, prefetched }) {
|
|
2038
|
-
|
|
2214
|
+
const failedCids = yield pullMessages({
|
|
2039
2215
|
did, dwnUrl, delegateDid, protocol, messageCids, prefetched,
|
|
2040
2216
|
agent: this.agent,
|
|
2041
2217
|
permissionsApi: this._permissionsApi,
|
|
2042
2218
|
});
|
|
2219
|
+
// Record permanently failed pull entries in the dead letter store.
|
|
2220
|
+
for (const cid of failedCids) {
|
|
2221
|
+
yield this.recordDeadLetter({
|
|
2222
|
+
messageCid: cid,
|
|
2223
|
+
tenantDid: did,
|
|
2224
|
+
remoteEndpoint: dwnUrl,
|
|
2225
|
+
protocol,
|
|
2226
|
+
category: 'pull-processing',
|
|
2227
|
+
errorDetail: 'pull processing failed after retry passes exhausted',
|
|
2228
|
+
});
|
|
2229
|
+
}
|
|
2043
2230
|
});
|
|
2044
2231
|
}
|
|
2045
2232
|
// ---------------------------------------------------------------------------
|
|
@@ -2110,12 +2297,226 @@ export class SyncEngineLevel {
|
|
|
2110
2297
|
static topologicalSort(messages) {
|
|
2111
2298
|
return topologicalSort(messages);
|
|
2112
2299
|
}
|
|
2300
|
+
// ---------------------------------------------------------------------------
|
|
2301
|
+
// Dead letter tracking
|
|
2302
|
+
// ---------------------------------------------------------------------------
|
|
2303
|
+
/**
|
|
2304
|
+
* Clear dead letter entries scoped to a specific sync link. Matches on
|
|
2305
|
+
* (tenantDid, remoteEndpoint, protocol) so that repairing protocol A
|
|
2306
|
+
* does not erase still-valid failures for protocol B on the same remote.
|
|
2307
|
+
* When `protocol` is undefined (full-tenant link), clears entries that
|
|
2308
|
+
* also have no protocol.
|
|
2309
|
+
*/
|
|
2310
|
+
clearDeadLettersForLink(tenantDid, remoteEndpoint, protocol) {
|
|
2311
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
2312
|
+
var _a, e_1, _b, _c;
|
|
2313
|
+
const batch = [];
|
|
2314
|
+
try {
|
|
2315
|
+
try {
|
|
2316
|
+
for (var _d = true, _e = __asyncValues(this._deadLetters.iterator()), _f; _f = yield _e.next(), _a = _f.done, !_a; _d = true) {
|
|
2317
|
+
_c = _f.value;
|
|
2318
|
+
_d = false;
|
|
2319
|
+
const [key, value] = _c;
|
|
2320
|
+
const entry = JSON.parse(value);
|
|
2321
|
+
if (entry.tenantDid === tenantDid &&
|
|
2322
|
+
entry.remoteEndpoint === remoteEndpoint &&
|
|
2323
|
+
entry.protocol === protocol) {
|
|
2324
|
+
batch.push({ type: 'del', key });
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
2329
|
+
finally {
|
|
2330
|
+
try {
|
|
2331
|
+
if (!_d && !_a && (_b = _e.return)) yield _b.call(_e);
|
|
2332
|
+
}
|
|
2333
|
+
finally { if (e_1) throw e_1.error; }
|
|
2334
|
+
}
|
|
2335
|
+
if (batch.length > 0) {
|
|
2336
|
+
yield this._deadLetters.batch(batch);
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
catch (error) {
|
|
2340
|
+
const e = error;
|
|
2341
|
+
if (e.code !== 'LEVEL_DATABASE_NOT_OPEN') {
|
|
2342
|
+
throw error;
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
});
|
|
2346
|
+
}
|
|
2347
|
+
/**
|
|
2348
|
+
* Build a compound dead letter key. Different remotes can fail the same CID
|
|
2349
|
+
* for different reasons, so the key includes the remote endpoint.
|
|
2350
|
+
*/
|
|
2351
|
+
static deadLetterKey(messageCid, remoteEndpoint) {
|
|
2352
|
+
return remoteEndpoint ? `${messageCid}|${remoteEndpoint}` : messageCid;
|
|
2353
|
+
}
|
|
2354
|
+
recordDeadLetter(params) {
|
|
2355
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
2356
|
+
const entry = Object.assign(Object.assign({}, params), { failedAt: new Date().toISOString() });
|
|
2357
|
+
const key = SyncEngineLevel.deadLetterKey(params.messageCid, params.remoteEndpoint);
|
|
2358
|
+
try {
|
|
2359
|
+
yield this._deadLetters.put(key, JSON.stringify(entry));
|
|
2360
|
+
}
|
|
2361
|
+
catch (error) {
|
|
2362
|
+
// Suppress only the expected teardown race — any other error surfaces.
|
|
2363
|
+
const e = error;
|
|
2364
|
+
if (e.code !== 'LEVEL_DATABASE_NOT_OPEN') {
|
|
2365
|
+
throw error;
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
});
|
|
2369
|
+
}
|
|
2370
|
+
getFailedMessages(tenantDid) {
|
|
2371
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
2372
|
+
var _a, e_2, _b, _c;
|
|
2373
|
+
const entries = [];
|
|
2374
|
+
try {
|
|
2375
|
+
for (var _d = true, _e = __asyncValues(this._deadLetters.iterator()), _f; _f = yield _e.next(), _a = _f.done, !_a; _d = true) {
|
|
2376
|
+
_c = _f.value;
|
|
2377
|
+
_d = false;
|
|
2378
|
+
const [, value] = _c;
|
|
2379
|
+
const entry = JSON.parse(value);
|
|
2380
|
+
if (!tenantDid || entry.tenantDid === tenantDid) {
|
|
2381
|
+
entries.push(entry);
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
catch (e_2_1) { e_2 = { error: e_2_1 }; }
|
|
2386
|
+
finally {
|
|
2387
|
+
try {
|
|
2388
|
+
if (!_d && !_a && (_b = _e.return)) yield _b.call(_e);
|
|
2389
|
+
}
|
|
2390
|
+
finally { if (e_2) throw e_2.error; }
|
|
2391
|
+
}
|
|
2392
|
+
// Deterministic ordering: newest first so apps see the most recent failures.
|
|
2393
|
+
entries.sort((a, b) => b.failedAt.localeCompare(a.failedAt));
|
|
2394
|
+
return entries;
|
|
2395
|
+
});
|
|
2396
|
+
}
|
|
2397
|
+
clearFailedMessage(messageCid, remoteEndpoint) {
|
|
2398
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
2399
|
+
var _a, e_3, _b, _c;
|
|
2400
|
+
if (remoteEndpoint) {
|
|
2401
|
+
// Clear a specific CID + remote pair.
|
|
2402
|
+
const key = SyncEngineLevel.deadLetterKey(messageCid, remoteEndpoint);
|
|
2403
|
+
try {
|
|
2404
|
+
yield this._deadLetters.get(key);
|
|
2405
|
+
yield this._deadLetters.del(key);
|
|
2406
|
+
return true;
|
|
2407
|
+
}
|
|
2408
|
+
catch (error) {
|
|
2409
|
+
const e = error;
|
|
2410
|
+
if (e.code === 'LEVEL_NOT_FOUND') {
|
|
2411
|
+
return false;
|
|
2412
|
+
}
|
|
2413
|
+
throw error;
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
// No remote specified — clear ALL entries for this CID (any remote).
|
|
2417
|
+
let found = false;
|
|
2418
|
+
const batch = [];
|
|
2419
|
+
try {
|
|
2420
|
+
for (var _d = true, _e = __asyncValues(this._deadLetters.iterator()), _f; _f = yield _e.next(), _a = _f.done, !_a; _d = true) {
|
|
2421
|
+
_c = _f.value;
|
|
2422
|
+
_d = false;
|
|
2423
|
+
const [key, value] = _c;
|
|
2424
|
+
const entry = JSON.parse(value);
|
|
2425
|
+
if (entry.messageCid === messageCid) {
|
|
2426
|
+
batch.push({ type: 'del', key });
|
|
2427
|
+
found = true;
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
catch (e_3_1) { e_3 = { error: e_3_1 }; }
|
|
2432
|
+
finally {
|
|
2433
|
+
try {
|
|
2434
|
+
if (!_d && !_a && (_b = _e.return)) yield _b.call(_e);
|
|
2435
|
+
}
|
|
2436
|
+
finally { if (e_3) throw e_3.error; }
|
|
2437
|
+
}
|
|
2438
|
+
if (batch.length > 0) {
|
|
2439
|
+
yield this._deadLetters.batch(batch);
|
|
2440
|
+
}
|
|
2441
|
+
return found;
|
|
2442
|
+
});
|
|
2443
|
+
}
|
|
2444
|
+
clearAllFailedMessages(tenantDid) {
|
|
2445
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
2446
|
+
var _a, e_4, _b, _c;
|
|
2447
|
+
if (!tenantDid) {
|
|
2448
|
+
yield this._deadLetters.clear();
|
|
2449
|
+
return;
|
|
2450
|
+
}
|
|
2451
|
+
const batch = [];
|
|
2452
|
+
try {
|
|
2453
|
+
for (var _d = true, _e = __asyncValues(this._deadLetters.iterator()), _f; _f = yield _e.next(), _a = _f.done, !_a; _d = true) {
|
|
2454
|
+
_c = _f.value;
|
|
2455
|
+
_d = false;
|
|
2456
|
+
const [key, value] = _c;
|
|
2457
|
+
const entry = JSON.parse(value);
|
|
2458
|
+
if (entry.tenantDid === tenantDid) {
|
|
2459
|
+
batch.push({ type: 'del', key });
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
catch (e_4_1) { e_4 = { error: e_4_1 }; }
|
|
2464
|
+
finally {
|
|
2465
|
+
try {
|
|
2466
|
+
if (!_d && !_a && (_b = _e.return)) yield _b.call(_e);
|
|
2467
|
+
}
|
|
2468
|
+
finally { if (e_4) throw e_4.error; }
|
|
2469
|
+
}
|
|
2470
|
+
if (batch.length > 0) {
|
|
2471
|
+
yield this._deadLetters.batch(batch);
|
|
2472
|
+
}
|
|
2473
|
+
});
|
|
2474
|
+
}
|
|
2475
|
+
getSyncHealth() {
|
|
2476
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
2477
|
+
var _a, e_5, _b, _c;
|
|
2478
|
+
let failedMessageCount = 0;
|
|
2479
|
+
try {
|
|
2480
|
+
for (var _d = true, _e = __asyncValues(this._deadLetters.iterator()), _f; _f = yield _e.next(), _a = _f.done, !_a; _d = true) {
|
|
2481
|
+
_c = _f.value;
|
|
2482
|
+
_d = false;
|
|
2483
|
+
const _ = _c;
|
|
2484
|
+
failedMessageCount++;
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
catch (e_5_1) { e_5 = { error: e_5_1 }; }
|
|
2488
|
+
finally {
|
|
2489
|
+
try {
|
|
2490
|
+
if (!_d && !_a && (_b = _e.return)) yield _b.call(_e);
|
|
2491
|
+
}
|
|
2492
|
+
finally { if (e_5) throw e_5.error; }
|
|
2493
|
+
}
|
|
2494
|
+
// Count degraded links from the durable ledger, not just in-memory
|
|
2495
|
+
// _activeLinks. Links persist across restarts; a repairing/degraded_poll
|
|
2496
|
+
// link from a previous session must still be reported.
|
|
2497
|
+
let degradedLinkCount = 0;
|
|
2498
|
+
const allLinks = yield this.ledger.getAllLinks();
|
|
2499
|
+
for (const link of allLinks) {
|
|
2500
|
+
if (link.status === 'repairing' || link.status === 'degraded_poll') {
|
|
2501
|
+
degradedLinkCount++;
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
return {
|
|
2505
|
+
connectivity: this.connectivityState,
|
|
2506
|
+
failedMessageCount,
|
|
2507
|
+
degradedLinkCount,
|
|
2508
|
+
};
|
|
2509
|
+
});
|
|
2510
|
+
}
|
|
2511
|
+
// ---------------------------------------------------------------------------
|
|
2512
|
+
// Sync targets
|
|
2513
|
+
// ---------------------------------------------------------------------------
|
|
2113
2514
|
/**
|
|
2114
2515
|
* Returns the list of sync targets: (did, dwnUrl, delegateDid?, protocol?) tuples.
|
|
2115
2516
|
*/
|
|
2116
2517
|
getSyncTargets() {
|
|
2117
2518
|
return __awaiter(this, void 0, void 0, function* () {
|
|
2118
|
-
var _a,
|
|
2519
|
+
var _a, e_6, _b, _c;
|
|
2119
2520
|
const targets = [];
|
|
2120
2521
|
try {
|
|
2121
2522
|
for (var _d = true, _e = __asyncValues(this._db.sublevel('registeredIdentities').iterator()), _f; _f = yield _e.next(), _a = _f.done, !_a; _d = true) {
|
|
@@ -2148,12 +2549,12 @@ export class SyncEngineLevel {
|
|
|
2148
2549
|
}
|
|
2149
2550
|
}
|
|
2150
2551
|
}
|
|
2151
|
-
catch (
|
|
2552
|
+
catch (e_6_1) { e_6 = { error: e_6_1 }; }
|
|
2152
2553
|
finally {
|
|
2153
2554
|
try {
|
|
2154
2555
|
if (!_d && !_a && (_b = _e.return)) yield _b.call(_e);
|
|
2155
2556
|
}
|
|
2156
|
-
finally { if (
|
|
2557
|
+
finally { if (e_6) throw e_6.error; }
|
|
2157
2558
|
}
|
|
2158
2559
|
return targets;
|
|
2159
2560
|
});
|