@enbox/agent 0.5.11 → 0.5.13
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 +6 -6
- package/dist/browser.mjs.map +3 -3
- package/dist/esm/sync-engine-level.js +42 -11
- package/dist/esm/sync-engine-level.js.map +1 -1
- package/dist/esm/sync-messages.js +24 -1
- package/dist/esm/sync-messages.js.map +1 -1
- package/dist/types/sync-engine-level.d.ts.map +1 -1
- package/dist/types/sync-messages.d.ts +9 -0
- package/dist/types/sync-messages.d.ts.map +1 -1
- package/dist/types/types/sync.d.ts +3 -1
- package/dist/types/types/sync.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/sync-engine-level.ts +46 -11
- package/src/sync-messages.ts +24 -1
- package/src/types/sync.ts +3 -1
package/src/sync-engine-level.ts
CHANGED
|
@@ -1173,7 +1173,19 @@ export class SyncEngineLevel implements SyncEngine {
|
|
|
1173
1173
|
// Resolve the cursor from the link's pull checkpoint (preferred) or legacy storage.
|
|
1174
1174
|
const cursorKey = this.buildCursorKey(did, dwnUrl, protocol);
|
|
1175
1175
|
const link = this._activeLinks.get(cursorKey);
|
|
1176
|
-
|
|
1176
|
+
let cursor = link?.pull.contiguousAppliedToken ?? await this.getCursor(cursorKey);
|
|
1177
|
+
|
|
1178
|
+
// Guard against corrupted tokens with empty fields — these would fail
|
|
1179
|
+
// MessagesSubscribe JSON schema validation (minLength: 1). Discard and
|
|
1180
|
+
// start from the beginning rather than crash the subscription.
|
|
1181
|
+
if (cursor && (!cursor.streamId || !cursor.messageCid || !cursor.epoch || !cursor.position)) {
|
|
1182
|
+
console.warn(`SyncEngineLevel: Discarding stored cursor with empty field(s) for ${did} -> ${dwnUrl}`);
|
|
1183
|
+
cursor = undefined;
|
|
1184
|
+
if (link) {
|
|
1185
|
+
ReplicationLedger.resetCheckpoint(link.pull);
|
|
1186
|
+
await this.ledger.saveLink(link);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1177
1189
|
|
|
1178
1190
|
// Build the MessagesSubscribe filters.
|
|
1179
1191
|
// When the link has protocolPathPrefixes, include them in the filter so the
|
|
@@ -1426,7 +1438,11 @@ export class SyncEngineLevel implements SyncEngine {
|
|
|
1426
1438
|
// a fresh cursor-stamped message after reconnection.
|
|
1427
1439
|
const resubscribeFactory: ResubscribeFactory = async (resumeCursor?: ProgressToken) => {
|
|
1428
1440
|
// On reconnect, use the latest durable checkpoint position if available.
|
|
1429
|
-
|
|
1441
|
+
// Discard tokens with empty fields to avoid schema validation failures.
|
|
1442
|
+
let effectiveCursor = resumeCursor ?? link?.pull.contiguousAppliedToken ?? cursor;
|
|
1443
|
+
if (effectiveCursor && (!effectiveCursor.streamId || !effectiveCursor.messageCid || !effectiveCursor.epoch || !effectiveCursor.position)) {
|
|
1444
|
+
effectiveCursor = undefined;
|
|
1445
|
+
}
|
|
1430
1446
|
const resumeRequest = {
|
|
1431
1447
|
...subscribeRequest,
|
|
1432
1448
|
messageParams: { ...subscribeRequest.messageParams, cursor: effectiveCursor },
|
|
@@ -1496,6 +1512,19 @@ export class SyncEngineLevel implements SyncEngine {
|
|
|
1496
1512
|
}): Promise<void> {
|
|
1497
1513
|
const { did, delegateDid, dwnUrl, protocol } = target;
|
|
1498
1514
|
|
|
1515
|
+
// Guard against corrupted push cursors — same validation as the pull side.
|
|
1516
|
+
let pushCursor = target.pushCursor;
|
|
1517
|
+
if (pushCursor && (!pushCursor.streamId || !pushCursor.messageCid || !pushCursor.epoch || !pushCursor.position)) {
|
|
1518
|
+
console.warn(`SyncEngineLevel: Discarding stored push cursor with empty field(s) for ${did} -> ${dwnUrl}`);
|
|
1519
|
+
pushCursor = undefined;
|
|
1520
|
+
const cursorKey = this.buildCursorKey(did, dwnUrl, protocol);
|
|
1521
|
+
const link = this._activeLinks.get(cursorKey);
|
|
1522
|
+
if (link) {
|
|
1523
|
+
ReplicationLedger.resetCheckpoint(link.push);
|
|
1524
|
+
await this.ledger.saveLink(link);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1499
1528
|
// Build filters scoped to the protocol (if any).
|
|
1500
1529
|
const filters = protocol ? [{ protocol }] : [];
|
|
1501
1530
|
|
|
@@ -1584,7 +1613,7 @@ export class SyncEngineLevel implements SyncEngine {
|
|
|
1584
1613
|
target : did,
|
|
1585
1614
|
messageType : DwnInterface.MessagesSubscribe,
|
|
1586
1615
|
granteeDid : delegateDid,
|
|
1587
|
-
messageParams : { filters, permissionGrantId, cursor:
|
|
1616
|
+
messageParams : { filters, permissionGrantId, cursor: pushCursor },
|
|
1588
1617
|
subscriptionHandler : subscriptionHandler as any,
|
|
1589
1618
|
});
|
|
1590
1619
|
|
|
@@ -1655,10 +1684,13 @@ export class SyncEngineLevel implements SyncEngine {
|
|
|
1655
1684
|
await this.ledger.saveLink(link);
|
|
1656
1685
|
}
|
|
1657
1686
|
|
|
1658
|
-
// Re-queue
|
|
1659
|
-
//
|
|
1687
|
+
// Re-queue only TRANSIENT failures for retry. Permanent failures (400/401/403)
|
|
1688
|
+
// are dropped — they will never succeed regardless of retry.
|
|
1660
1689
|
if (result.failed.length > 0) {
|
|
1661
|
-
console.error(
|
|
1690
|
+
console.error(
|
|
1691
|
+
`SyncEngineLevel: Push-on-write failed for ${did} -> ${dwnUrl}: ` +
|
|
1692
|
+
`${result.failed.length} transient failures of ${cids.length} messages`
|
|
1693
|
+
);
|
|
1662
1694
|
const failedSet = new Set(result.failed);
|
|
1663
1695
|
const failedEntries = pushEntries.filter(e => failedSet.has(e.cid));
|
|
1664
1696
|
let requeued = this._pendingPushCids.get(targetKey);
|
|
@@ -1672,9 +1704,12 @@ export class SyncEngineLevel implements SyncEngine {
|
|
|
1672
1704
|
if (!this._pushDebounceTimer) {
|
|
1673
1705
|
this._pushDebounceTimer = setTimeout((): void => {
|
|
1674
1706
|
void this.flushPendingPushes();
|
|
1675
|
-
}, PUSH_DEBOUNCE_MS * 4);
|
|
1707
|
+
}, PUSH_DEBOUNCE_MS * 4);
|
|
1676
1708
|
}
|
|
1677
1709
|
}
|
|
1710
|
+
// Permanent failures are logged by pushMessages but NOT re-queued.
|
|
1711
|
+
// They will be rediscovered by the next SMT integrity check if the
|
|
1712
|
+
// local/remote state has changed, but won't spin in a retry loop.
|
|
1678
1713
|
} catch (error: any) {
|
|
1679
1714
|
// Truly unexpected error (not per-message failure). Re-queue entire
|
|
1680
1715
|
// batch so entries aren't silently dropped from the debounce queue.
|
|
@@ -1717,10 +1752,10 @@ export class SyncEngineLevel implements SyncEngine {
|
|
|
1717
1752
|
try {
|
|
1718
1753
|
const parsed = JSON.parse(raw);
|
|
1719
1754
|
if (parsed && typeof parsed === 'object' &&
|
|
1720
|
-
typeof parsed.streamId === 'string' &&
|
|
1721
|
-
typeof parsed.epoch === 'string' &&
|
|
1722
|
-
typeof parsed.position === 'string' &&
|
|
1723
|
-
typeof parsed.messageCid === 'string') {
|
|
1755
|
+
typeof parsed.streamId === 'string' && parsed.streamId.length > 0 &&
|
|
1756
|
+
typeof parsed.epoch === 'string' && parsed.epoch.length > 0 &&
|
|
1757
|
+
typeof parsed.position === 'string' && parsed.position.length > 0 &&
|
|
1758
|
+
typeof parsed.messageCid === 'string' && parsed.messageCid.length > 0) {
|
|
1724
1759
|
return parsed as ProgressToken;
|
|
1725
1760
|
}
|
|
1726
1761
|
} catch {
|
package/src/sync-messages.ts
CHANGED
|
@@ -37,6 +37,20 @@ export function syncMessageReplyIsSuccessful(reply: UnionMessageReply): boolean
|
|
|
37
37
|
);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Determines whether a failed push reply represents a permanent failure that
|
|
42
|
+
* should NOT be retried. Permanent failures include protocol violations (400),
|
|
43
|
+
* authorization errors (401/403), and schema validation errors that will never
|
|
44
|
+
* succeed regardless of retry.
|
|
45
|
+
*
|
|
46
|
+
* Transient failures (5xx, network errors) are worth retrying.
|
|
47
|
+
*/
|
|
48
|
+
export function isPermanentPushFailure(reply: UnionMessageReply): boolean {
|
|
49
|
+
return reply.status.code === 400 ||
|
|
50
|
+
reply.status.code === 401 ||
|
|
51
|
+
reply.status.code === 403;
|
|
52
|
+
}
|
|
53
|
+
|
|
40
54
|
/**
|
|
41
55
|
* Helper to get the CID of a message for logging purposes.
|
|
42
56
|
*/
|
|
@@ -313,6 +327,7 @@ export async function pushMessages({ did, dwnUrl, delegateDid, protocol, message
|
|
|
313
327
|
}): Promise<PushResult> {
|
|
314
328
|
const succeeded: string[] = [];
|
|
315
329
|
const failed: string[] = [];
|
|
330
|
+
const permanentlyFailed: string[] = [];
|
|
316
331
|
|
|
317
332
|
// Step 1: Fetch all local messages (streams are pull-based, not yet consumed).
|
|
318
333
|
const fetched: SyncMessageEntry[] = [];
|
|
@@ -342,17 +357,25 @@ export async function pushMessages({ did, dwnUrl, delegateDid, protocol, message
|
|
|
342
357
|
|
|
343
358
|
if (syncMessageReplyIsSuccessful(reply)) {
|
|
344
359
|
succeeded.push(cid);
|
|
360
|
+
} else if (isPermanentPushFailure(reply)) {
|
|
361
|
+
// Permanent failures (400/401/403) will never succeed — do NOT retry.
|
|
362
|
+
// These include protocol violations (RecordLimitExceeded), auth errors,
|
|
363
|
+
// and schema validation failures.
|
|
364
|
+
console.warn(`SyncEngineLevel: push permanently failed for ${cid}: ${reply.status.code} ${reply.status.detail}`);
|
|
365
|
+
permanentlyFailed.push(cid);
|
|
345
366
|
} else {
|
|
367
|
+
// Transient failures (5xx, etc.) — worth retrying.
|
|
346
368
|
console.error(`SyncEngineLevel: push failed for ${cid}: ${reply.status.code} ${reply.status.detail}`);
|
|
347
369
|
failed.push(cid);
|
|
348
370
|
}
|
|
349
371
|
} catch (error: any) {
|
|
372
|
+
// Network errors — transient, worth retrying.
|
|
350
373
|
console.error(`SyncEngineLevel: push error for ${cid}: ${error.message ?? error}`);
|
|
351
374
|
failed.push(cid);
|
|
352
375
|
}
|
|
353
376
|
}
|
|
354
377
|
|
|
355
|
-
return { succeeded, failed };
|
|
378
|
+
return { succeeded, failed, permanentlyFailed };
|
|
356
379
|
}
|
|
357
380
|
|
|
358
381
|
/**
|
package/src/types/sync.ts
CHANGED
|
@@ -194,8 +194,10 @@ export type ReplicationLinkState = {
|
|
|
194
194
|
export type PushResult = {
|
|
195
195
|
/** messageCids that were accepted (202/204/409 — idempotent success). */
|
|
196
196
|
succeeded: string[];
|
|
197
|
-
/** messageCids that failed
|
|
197
|
+
/** messageCids that failed with a transient error (5xx, network) — worth retrying. */
|
|
198
198
|
failed: string[];
|
|
199
|
+
/** messageCids that failed permanently (400/401/403) — will never succeed, do NOT retry. */
|
|
200
|
+
permanentlyFailed: string[];
|
|
199
201
|
};
|
|
200
202
|
|
|
201
203
|
/**
|