@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.
@@ -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
- try {
578
- const grant = await this._permissionsApi.getPermissionForRequest({
579
- connectedDid : did,
580
- messageType : DwnInterface.MessagesSubscribe,
581
- delegateDid,
582
- protocol,
583
- cached : true
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.MessagesRead,
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
- for (const [, pending] of entries) {
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
- continue;
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
- const permissionGrantIdForLeaves = await this.getSyncPermissionGrantId(did, delegateDid, protocol);
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
  // ---------------------------------------------------------------------------