@enbox/agent 0.5.9 → 0.5.10
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 +8 -8
- package/dist/browser.mjs.map +3 -3
- package/dist/esm/sync-engine-level.js +37 -199
- package/dist/esm/sync-engine-level.js.map +1 -1
- package/dist/types/sync-engine-level.d.ts +0 -9
- package/dist/types/sync-engine-level.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/sync-engine-level.ts +40 -223
package/src/sync-engine-level.ts
CHANGED
|
@@ -40,56 +40,6 @@ const MAX_DIFF_DEPTH = 16;
|
|
|
40
40
|
const BATCHED_DIFF_DEPTH = 8;
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
|
-
* Maximum number of concurrent remote HTTP requests during a tree diff.
|
|
44
|
-
* The binary tree walk fans out in parallel — without a limit, depth N
|
|
45
|
-
* produces 2^N concurrent requests, which can exhaust server rate limits.
|
|
46
|
-
*/
|
|
47
|
-
const REMOTE_CONCURRENCY = 4;
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Counting semaphore for bounding concurrent async operations.
|
|
51
|
-
* Used by the tree walk to limit in-flight remote HTTP requests.
|
|
52
|
-
*/
|
|
53
|
-
class Semaphore {
|
|
54
|
-
private _permits: number;
|
|
55
|
-
private readonly _waiting: (() => void)[] = [];
|
|
56
|
-
|
|
57
|
-
constructor(permits: number) {
|
|
58
|
-
this._permits = permits;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Wait until a permit is available, then consume one. */
|
|
62
|
-
async acquire(): Promise<void> {
|
|
63
|
-
if (this._permits > 0) {
|
|
64
|
-
this._permits--;
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
return new Promise<void>((resolve) => {
|
|
68
|
-
this._waiting.push(resolve);
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** Release a permit, waking the next waiter if any. */
|
|
73
|
-
release(): void {
|
|
74
|
-
const next = this._waiting.shift();
|
|
75
|
-
if (next) {
|
|
76
|
-
next();
|
|
77
|
-
} else {
|
|
78
|
-
this._permits++;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** Acquire a permit, run the task, then release regardless of outcome. */
|
|
83
|
-
async run<T>(fn: () => Promise<T>): Promise<T> {
|
|
84
|
-
await this.acquire();
|
|
85
|
-
try {
|
|
86
|
-
return await fn();
|
|
87
|
-
} finally {
|
|
88
|
-
this.release();
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
43
|
/**
|
|
94
44
|
* Key for the subscription cursor sublevel. Cursors are keyed by
|
|
95
45
|
* `{did}^{dwnUrl}[^{protocol}]` and store an opaque EventLog cursor string.
|
|
@@ -572,28 +522,19 @@ export class SyncEngineLevel implements SyncEngine {
|
|
|
572
522
|
const filters = protocol ? [{ protocol }] : [];
|
|
573
523
|
|
|
574
524
|
// Look up permission grant for MessagesSubscribe if using a delegate.
|
|
525
|
+
// The unified scope matching in AgentPermissionsApi accepts a
|
|
526
|
+
// Messages.Read grant for MessagesSubscribe requests, so a single
|
|
527
|
+
// lookup is sufficient.
|
|
575
528
|
let permissionGrantId: string | undefined;
|
|
576
529
|
if (delegateDid) {
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
permissionGrantId = grant.grant.id;
|
|
586
|
-
} catch {
|
|
587
|
-
// Fall back to trying MessagesRead which is a unified scope.
|
|
588
|
-
const grant = await this._permissionsApi.getPermissionForRequest({
|
|
589
|
-
connectedDid : did,
|
|
590
|
-
messageType : DwnInterface.MessagesRead,
|
|
591
|
-
delegateDid,
|
|
592
|
-
protocol,
|
|
593
|
-
cached : true
|
|
594
|
-
});
|
|
595
|
-
permissionGrantId = grant.grant.id;
|
|
596
|
-
}
|
|
530
|
+
const grant = await this._permissionsApi.getPermissionForRequest({
|
|
531
|
+
connectedDid : did,
|
|
532
|
+
messageType : DwnInterface.MessagesSubscribe,
|
|
533
|
+
delegateDid,
|
|
534
|
+
protocol,
|
|
535
|
+
cached : true
|
|
536
|
+
});
|
|
537
|
+
permissionGrantId = grant.grant.id;
|
|
597
538
|
}
|
|
598
539
|
|
|
599
540
|
// Define the subscription handler that processes incoming events.
|
|
@@ -627,12 +568,14 @@ export class SyncEngineLevel implements SyncEngine {
|
|
|
627
568
|
}
|
|
628
569
|
|
|
629
570
|
await this.agent.dwn.processRawMessage(did, event.message, { dataStream });
|
|
571
|
+
|
|
572
|
+
// Only advance the cursor after successful processing.
|
|
573
|
+
// If processing fails, the event will be re-delivered on
|
|
574
|
+
// reconnection (cursor-based resume from the last good point).
|
|
575
|
+
await this.setCursor(cursorKey, subMessage.cursor);
|
|
630
576
|
} catch (error: any) {
|
|
631
577
|
console.error(`SyncEngineLevel: Error processing live-pull event for ${did}`, error);
|
|
632
578
|
}
|
|
633
|
-
|
|
634
|
-
// Persist cursor for resume on reconnect.
|
|
635
|
-
await this.setCursor(cursorKey, subMessage.cursor);
|
|
636
579
|
}
|
|
637
580
|
};
|
|
638
581
|
|
|
@@ -719,7 +662,7 @@ export class SyncEngineLevel implements SyncEngine {
|
|
|
719
662
|
if (delegateDid) {
|
|
720
663
|
const grant = await this._permissionsApi.getPermissionForRequest({
|
|
721
664
|
connectedDid : did,
|
|
722
|
-
messageType : DwnInterface.
|
|
665
|
+
messageType : DwnInterface.MessagesSubscribe,
|
|
723
666
|
delegateDid,
|
|
724
667
|
protocol,
|
|
725
668
|
cached : true,
|
|
@@ -790,10 +733,11 @@ export class SyncEngineLevel implements SyncEngine {
|
|
|
790
733
|
const entries = [...this._pendingPushCids.entries()];
|
|
791
734
|
this._pendingPushCids.clear();
|
|
792
735
|
|
|
793
|
-
|
|
736
|
+
// Push to all endpoints in parallel — each target is independent.
|
|
737
|
+
await Promise.all(entries.map(async ([, pending]) => {
|
|
794
738
|
const { did, dwnUrl, delegateDid, protocol, cids } = pending;
|
|
795
739
|
if (cids.length === 0) {
|
|
796
|
-
|
|
740
|
+
return;
|
|
797
741
|
}
|
|
798
742
|
|
|
799
743
|
try {
|
|
@@ -805,8 +749,25 @@ export class SyncEngineLevel implements SyncEngine {
|
|
|
805
749
|
});
|
|
806
750
|
} catch (error: any) {
|
|
807
751
|
console.error(`SyncEngineLevel: Push-on-write failed for ${did} -> ${dwnUrl}`, error);
|
|
752
|
+
|
|
753
|
+
// Re-queue the failed CIDs so they are retried on the next
|
|
754
|
+
// debounce cycle (or picked up by the SMT integrity check).
|
|
755
|
+
const targetKey = this.buildCursorKey(did, dwnUrl, protocol);
|
|
756
|
+
let requeued = this._pendingPushCids.get(targetKey);
|
|
757
|
+
if (!requeued) {
|
|
758
|
+
requeued = { did, dwnUrl, delegateDid, protocol, cids: [] };
|
|
759
|
+
this._pendingPushCids.set(targetKey, requeued);
|
|
760
|
+
}
|
|
761
|
+
requeued.cids.push(...cids);
|
|
762
|
+
|
|
763
|
+
// Schedule a retry after a short delay.
|
|
764
|
+
if (!this._pushDebounceTimer) {
|
|
765
|
+
this._pushDebounceTimer = setTimeout((): void => {
|
|
766
|
+
void this.flushPendingPushes();
|
|
767
|
+
}, PUSH_DEBOUNCE_MS * 4); // Back off: 1 second instead of 250ms.
|
|
768
|
+
}
|
|
808
769
|
}
|
|
809
|
-
}
|
|
770
|
+
}));
|
|
810
771
|
}
|
|
811
772
|
|
|
812
773
|
// ---------------------------------------------------------------------------
|
|
@@ -870,8 +831,6 @@ export class SyncEngineLevel implements SyncEngine {
|
|
|
870
831
|
return undefined;
|
|
871
832
|
}
|
|
872
833
|
|
|
873
|
-
|
|
874
|
-
|
|
875
834
|
// ---------------------------------------------------------------------------
|
|
876
835
|
// Default Hash Cache
|
|
877
836
|
// ---------------------------------------------------------------------------
|
|
@@ -987,97 +946,6 @@ export class SyncEngineLevel implements SyncEngine {
|
|
|
987
946
|
}
|
|
988
947
|
|
|
989
948
|
// ---------------------------------------------------------------------------
|
|
990
|
-
// Tree Diff — walk the SMT to find divergent leaf sets
|
|
991
|
-
// ---------------------------------------------------------------------------
|
|
992
|
-
|
|
993
|
-
/**
|
|
994
|
-
* Walks the local and remote SMTs in parallel, recursing into subtrees whose
|
|
995
|
-
* hashes differ, until reaching `MAX_DIFF_DEPTH` where leaves are enumerated.
|
|
996
|
-
*
|
|
997
|
-
* Returns the sets of messageCids that exist only locally or only remotely.
|
|
998
|
-
*/
|
|
999
|
-
private async walkTreeDiff({ did, dwnUrl, delegateDid, protocol }: {
|
|
1000
|
-
did: string;
|
|
1001
|
-
dwnUrl: string;
|
|
1002
|
-
delegateDid?: string;
|
|
1003
|
-
protocol?: string;
|
|
1004
|
-
}): Promise<{ onlyLocal: string[]; onlyRemote: string[] }> {
|
|
1005
|
-
const onlyLocal: string[] = [];
|
|
1006
|
-
const onlyRemote: string[] = [];
|
|
1007
|
-
|
|
1008
|
-
// Hoist permission grant lookup — resolved once and reused for all subtree/leaf requests.
|
|
1009
|
-
const permissionGrantId = await this.getSyncPermissionGrantId(did, delegateDid, protocol);
|
|
1010
|
-
|
|
1011
|
-
// Gate remote HTTP requests through a semaphore so the binary tree walk
|
|
1012
|
-
// doesn't produce an exponential burst of concurrent requests. Local
|
|
1013
|
-
// DWN requests (in-process) are not gated.
|
|
1014
|
-
const remoteSemaphore = new Semaphore(REMOTE_CONCURRENCY);
|
|
1015
|
-
|
|
1016
|
-
const walk = async (prefix: string): Promise<void> => {
|
|
1017
|
-
// Get subtree hashes for this prefix from local and remote.
|
|
1018
|
-
// Only the remote request is gated by the semaphore.
|
|
1019
|
-
const [localHash, remoteHash] = await Promise.all([
|
|
1020
|
-
this.getLocalSubtreeHash(did, prefix, delegateDid, protocol, permissionGrantId),
|
|
1021
|
-
remoteSemaphore.run(() => this.getRemoteSubtreeHash(did, dwnUrl, prefix, delegateDid, protocol, permissionGrantId)),
|
|
1022
|
-
]);
|
|
1023
|
-
|
|
1024
|
-
// If hashes match, this subtree is identical — skip.
|
|
1025
|
-
if (localHash === remoteHash) {
|
|
1026
|
-
return;
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
// Short-circuit: if one side is the default (empty-subtree) hash, all entries
|
|
1030
|
-
// on the other side are unique. Enumerate leaves directly instead of recursing
|
|
1031
|
-
// further into the tree — this avoids an exponential walk when one DWN has
|
|
1032
|
-
// entries that the other lacks entirely in this subtree.
|
|
1033
|
-
const emptyHash = await this.getDefaultHashHex(prefix.length);
|
|
1034
|
-
if (remoteHash === emptyHash && localHash !== emptyHash) {
|
|
1035
|
-
const localLeaves = await this.getLocalLeaves(did, prefix, delegateDid, protocol, permissionGrantId);
|
|
1036
|
-
onlyLocal.push(...localLeaves);
|
|
1037
|
-
return;
|
|
1038
|
-
}
|
|
1039
|
-
if (localHash === emptyHash && remoteHash !== emptyHash) {
|
|
1040
|
-
const remoteLeaves = await remoteSemaphore.run(
|
|
1041
|
-
() => this.getRemoteLeaves(did, dwnUrl, prefix, delegateDid, protocol, permissionGrantId),
|
|
1042
|
-
);
|
|
1043
|
-
onlyRemote.push(...remoteLeaves);
|
|
1044
|
-
return;
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
// If we've reached the maximum diff depth, enumerate leaves.
|
|
1048
|
-
if (prefix.length >= MAX_DIFF_DEPTH) {
|
|
1049
|
-
const [localLeaves, remoteLeaves] = await Promise.all([
|
|
1050
|
-
this.getLocalLeaves(did, prefix, delegateDid, protocol, permissionGrantId),
|
|
1051
|
-
remoteSemaphore.run(() => this.getRemoteLeaves(did, dwnUrl, prefix, delegateDid, protocol, permissionGrantId)),
|
|
1052
|
-
]);
|
|
1053
|
-
|
|
1054
|
-
const localSet = new Set(localLeaves);
|
|
1055
|
-
const remoteSet = new Set(remoteLeaves);
|
|
1056
|
-
|
|
1057
|
-
for (const cid of localLeaves) {
|
|
1058
|
-
if (!remoteSet.has(cid)) {
|
|
1059
|
-
onlyLocal.push(cid);
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
for (const cid of remoteLeaves) {
|
|
1063
|
-
if (!localSet.has(cid)) {
|
|
1064
|
-
onlyRemote.push(cid);
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
return;
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
// Recurse into left (0) and right (1) children in parallel.
|
|
1071
|
-
await Promise.all([
|
|
1072
|
-
walk(prefix + '0'),
|
|
1073
|
-
walk(prefix + '1'),
|
|
1074
|
-
]);
|
|
1075
|
-
};
|
|
1076
|
-
|
|
1077
|
-
await walk('');
|
|
1078
|
-
return { onlyLocal, onlyRemote };
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
949
|
// ---------------------------------------------------------------------------
|
|
1082
950
|
// Batched Diff — single round-trip set reconciliation
|
|
1083
951
|
// ---------------------------------------------------------------------------
|
|
@@ -1133,7 +1001,8 @@ export class SyncEngineLevel implements SyncEngine {
|
|
|
1133
1001
|
}
|
|
1134
1002
|
|
|
1135
1003
|
// Step 3: Enumerate local leaves for prefixes the remote reported as onlyLocal.
|
|
1136
|
-
|
|
1004
|
+
// Reuse the same grant ID from step 2 (avoids redundant lookup).
|
|
1005
|
+
const permissionGrantIdForLeaves = permissionGrantId;
|
|
1137
1006
|
const onlyLocalCids: string[] = [];
|
|
1138
1007
|
for (const prefix of reply.onlyLocal ?? []) {
|
|
1139
1008
|
const leaves = await this.getLocalLeaves(did, prefix, delegateDid, protocol, permissionGrantIdForLeaves);
|
|
@@ -1234,32 +1103,6 @@ export class SyncEngineLevel implements SyncEngine {
|
|
|
1234
1103
|
return reply.hash ?? '';
|
|
1235
1104
|
}
|
|
1236
1105
|
|
|
1237
|
-
private async getRemoteSubtreeHash(
|
|
1238
|
-
did: string, dwnUrl: string, prefix: string, delegateDid?: string, protocol?: string, permissionGrantId?: string
|
|
1239
|
-
): Promise<string> {
|
|
1240
|
-
const syncMessage = await this.agent.dwn.processRequest({
|
|
1241
|
-
store : false,
|
|
1242
|
-
author : did,
|
|
1243
|
-
target : did,
|
|
1244
|
-
messageType : DwnInterface.MessagesSync,
|
|
1245
|
-
granteeDid : delegateDid,
|
|
1246
|
-
messageParams : {
|
|
1247
|
-
action: 'subtree',
|
|
1248
|
-
prefix,
|
|
1249
|
-
protocol,
|
|
1250
|
-
permissionGrantId
|
|
1251
|
-
}
|
|
1252
|
-
});
|
|
1253
|
-
|
|
1254
|
-
const reply = await this.agent.rpc.sendDwnRequest({
|
|
1255
|
-
dwnUrl,
|
|
1256
|
-
targetDid : did,
|
|
1257
|
-
message : syncMessage.message,
|
|
1258
|
-
}) as MessagesSyncReply;
|
|
1259
|
-
|
|
1260
|
-
return reply.hash ?? '';
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
1106
|
/**
|
|
1264
1107
|
* Get all leaf messageCids under a given prefix from the local DWN.
|
|
1265
1108
|
*
|
|
@@ -1294,32 +1137,6 @@ export class SyncEngineLevel implements SyncEngine {
|
|
|
1294
1137
|
return reply.entries ?? [];
|
|
1295
1138
|
}
|
|
1296
1139
|
|
|
1297
|
-
private async getRemoteLeaves(
|
|
1298
|
-
did: string, dwnUrl: string, prefix: string, delegateDid?: string, protocol?: string, permissionGrantId?: string
|
|
1299
|
-
): Promise<string[]> {
|
|
1300
|
-
const syncMessage = await this.agent.dwn.processRequest({
|
|
1301
|
-
store : false,
|
|
1302
|
-
author : did,
|
|
1303
|
-
target : did,
|
|
1304
|
-
messageType : DwnInterface.MessagesSync,
|
|
1305
|
-
granteeDid : delegateDid,
|
|
1306
|
-
messageParams : {
|
|
1307
|
-
action: 'leaves',
|
|
1308
|
-
prefix,
|
|
1309
|
-
protocol,
|
|
1310
|
-
permissionGrantId
|
|
1311
|
-
}
|
|
1312
|
-
});
|
|
1313
|
-
|
|
1314
|
-
const reply = await this.agent.rpc.sendDwnRequest({
|
|
1315
|
-
dwnUrl,
|
|
1316
|
-
targetDid : did,
|
|
1317
|
-
message : syncMessage.message,
|
|
1318
|
-
}) as MessagesSyncReply;
|
|
1319
|
-
|
|
1320
|
-
return reply.entries ?? [];
|
|
1321
|
-
}
|
|
1322
|
-
|
|
1323
1140
|
// ---------------------------------------------------------------------------
|
|
1324
1141
|
// Pull / Push — delegates to standalone functions in sync-messages.ts
|
|
1325
1142
|
// ---------------------------------------------------------------------------
|