@streamr/trackerless-network 103.3.1 → 103.7.0-rc.2

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/exports.cjs CHANGED
@@ -29,7 +29,7 @@ class ExternalNetworkRpc {
29
29
  }
30
30
  }
31
31
 
32
- var version = "103.3.1";
32
+ var version = "103.7.0-rc.2";
33
33
 
34
34
  // @generated message type with reflection information, may provide speed optimized methods
35
35
  class Any$Type extends runtime.MessageType {
@@ -1676,6 +1676,18 @@ class PauseNeighborRequest$Type extends runtime.MessageType {
1676
1676
  */
1677
1677
  const PauseNeighborRequest = new PauseNeighborRequest$Type();
1678
1678
  // @generated message type with reflection information, may provide speed optimized methods
1679
+ class PauseNeighborResponse$Type extends runtime.MessageType {
1680
+ constructor() {
1681
+ super("PauseNeighborResponse", [
1682
+ { no: 1, name: "accepted", kind: "scalar", T: 8 /*ScalarType.BOOL*/ }
1683
+ ]);
1684
+ }
1685
+ }
1686
+ /**
1687
+ * @generated MessageType for protobuf message PauseNeighborResponse
1688
+ */
1689
+ const PauseNeighborResponse = new PauseNeighborResponse$Type();
1690
+ // @generated message type with reflection information, may provide speed optimized methods
1679
1691
  class ResumeNeighborRequest$Type extends runtime.MessageType {
1680
1692
  constructor() {
1681
1693
  super("ResumeNeighborRequest", [
@@ -1731,7 +1743,7 @@ const NodeInfoRpc = new runtimeRpc.ServiceType("NodeInfoRpc", [
1731
1743
  * @generated ServiceType for protobuf service PlumtreeRpc
1732
1744
  */
1733
1745
  const PlumtreeRpc = new runtimeRpc.ServiceType("PlumtreeRpc", [
1734
- { name: "pauseNeighbor", options: {}, I: PauseNeighborRequest, O: Empty },
1746
+ { name: "pauseNeighbor", options: {}, I: PauseNeighborRequest, O: PauseNeighborResponse },
1735
1747
  { name: "resumeNeighbor", options: {}, I: ResumeNeighborRequest, O: Empty },
1736
1748
  { name: "sendMetadata", options: {}, I: MessageID, O: Empty }
1737
1749
  ]);
@@ -2392,12 +2404,72 @@ class NeighborUpdateManager {
2392
2404
  }
2393
2405
  }
2394
2406
 
2407
+ let enabled = false;
2408
+ function setGapDiagnosticsEnabled(val) {
2409
+ enabled = val;
2410
+ globalThis.__trackerlessGapDiagEnabled = val;
2411
+ }
2412
+ const SUMMARY_INTERVAL_MS = 2000;
2413
+ const accumulators = new Map();
2414
+ function logGapDiagnosticSampled(layer, opts = {}) {
2415
+ if (!enabled)
2416
+ return;
2417
+ const now = performance.now();
2418
+ const threshold = opts.outlierThresholdMs ?? 30;
2419
+ let acc = accumulators.get(layer);
2420
+ if (acc === undefined) {
2421
+ acc = { count: 0, sumDeltaMs: 0, maxDeltaMs: 0, outlierCount: 0, lastReportMs: now, lastEventMs: now };
2422
+ accumulators.set(layer, acc);
2423
+ return;
2424
+ }
2425
+ const deltaMs = now - acc.lastEventMs;
2426
+ acc.lastEventMs = now;
2427
+ acc.count++;
2428
+ if (deltaMs > acc.maxDeltaMs)
2429
+ acc.maxDeltaMs = deltaMs;
2430
+ acc.sumDeltaMs += deltaMs;
2431
+ if (deltaMs > threshold)
2432
+ acc.outlierCount++;
2433
+ if (deltaMs > threshold) {
2434
+ const payload = {
2435
+ layer,
2436
+ timestampMs: now,
2437
+ deltaMs: +deltaMs.toFixed(2),
2438
+ detail: opts.detail,
2439
+ };
2440
+ // eslint-disable-next-line no-console
2441
+ console.log('[gap-diagnostics]', JSON.stringify(payload));
2442
+ }
2443
+ if (now - acc.lastReportMs >= SUMMARY_INTERVAL_MS) {
2444
+ const summaryDetail = {
2445
+ count: acc.count,
2446
+ meanDeltaMs: acc.count > 0 ? +(acc.sumDeltaMs / acc.count).toFixed(2) : 0,
2447
+ maxDeltaMs: +acc.maxDeltaMs.toFixed(2),
2448
+ outlierCount: acc.outlierCount,
2449
+ periodMs: +(now - acc.lastReportMs).toFixed(1),
2450
+ };
2451
+ const summary = {
2452
+ layer: `${layer}.summary`,
2453
+ timestampMs: now,
2454
+ detail: summaryDetail,
2455
+ };
2456
+ // eslint-disable-next-line no-console
2457
+ console.log('[gap-diagnostics]', JSON.stringify(summary));
2458
+ acc.count = 0;
2459
+ acc.sumDeltaMs = 0;
2460
+ acc.maxDeltaMs = 0;
2461
+ acc.outlierCount = 0;
2462
+ acc.lastReportMs = now;
2463
+ }
2464
+ }
2465
+
2395
2466
  class ContentDeliveryRpcLocal {
2396
2467
  options;
2397
2468
  constructor(options) {
2398
2469
  this.options = options;
2399
2470
  }
2400
2471
  async sendStreamMessage(message, context) {
2472
+ logGapDiagnosticSampled('trackerless.rpcLocal.sendStreamMessage');
2401
2473
  const previousNode = context.incomingSourceDescriptor;
2402
2474
  const previousNodeId = dht.toNodeId(previousNode);
2403
2475
  this.options.markForInspection(previousNodeId, message.messageId);
@@ -2758,11 +2830,16 @@ class ContentDeliveryLayerNode extends eventemitter3.EventEmitter {
2758
2830
  this.options.neighborFinder.stop();
2759
2831
  this.options.neighborUpdateManager.stop();
2760
2832
  this.options.inspector.stop();
2833
+ this.duplicateDetectors.clear();
2761
2834
  }
2762
2835
  broadcast(msg, previousNode) {
2763
2836
  if (!previousNode) {
2837
+ logGapDiagnosticSampled('trackerless.cdNode.broadcastOut');
2764
2838
  markAndCheckDuplicate(this.duplicateDetectors, msg.messageId, msg.previousMessageRef);
2765
2839
  }
2840
+ else {
2841
+ logGapDiagnosticSampled('trackerless.cdNode.broadcastIn');
2842
+ }
2766
2843
  this.emit('message', msg);
2767
2844
  const skipBackPropagation = previousNode !== undefined && !this.options.temporaryConnectionRpcLocal.hasNode(previousNode);
2768
2845
  this.options.propagation.feedUnseenMessage(msg, this.getPropagationTargets(msg), skipBackPropagation ? previousNode : null);
@@ -3060,6 +3137,7 @@ class Propagation {
3060
3137
  }
3061
3138
  sendAndAwaitThenMark({ message, source, handledNeighbors }, neighborId) {
3062
3139
  if (!handledNeighbors.has(neighborId) && neighborId !== source) {
3140
+ logGapDiagnosticSampled('trackerless.propagation.sendToNeighbor');
3063
3141
  (async () => {
3064
3142
  try {
3065
3143
  await this.sendToNeighbor(neighborId, message);
@@ -3333,14 +3411,17 @@ class PlumtreeRpcLocal {
3333
3411
  async pauseNeighbor(request, context) {
3334
3412
  const sender = dht.toNodeId(context.incomingSourceDescriptor);
3335
3413
  if (this.neighbors.has(sender)) {
3336
- this.pausedNodes.add(sender, request.messageChainId);
3414
+ const accepted = this.pausedNodes.add(sender, request.messageChainId);
3415
+ return { accepted };
3337
3416
  }
3338
- return Empty;
3417
+ return { accepted: false };
3339
3418
  }
3340
3419
  async resumeNeighbor(request, context) {
3341
3420
  const sender = context.incomingSourceDescriptor;
3342
- this.pausedNodes.delete(dht.toNodeId(sender), request.messageChainId);
3343
- await this.sendBuffer(request.fromTimestamp, request.messageChainId, sender);
3421
+ if (this.neighbors.has(dht.toNodeId(sender))) {
3422
+ this.pausedNodes.delete(dht.toNodeId(sender), request.messageChainId);
3423
+ await this.sendBuffer(request.fromTimestamp, request.messageChainId, sender);
3424
+ }
3344
3425
  return Empty;
3345
3426
  }
3346
3427
  }
@@ -3353,10 +3434,9 @@ class PlumtreeRpcRemote extends dht.RpcRemote {
3353
3434
  await this.getClient().sendMetadata(msg, options);
3354
3435
  }
3355
3436
  async pauseNeighbor(messageChainId) {
3356
- const options = this.formDhtRpcOptions({
3357
- notification: true
3358
- });
3359
- await this.getClient().pauseNeighbor({ messageChainId }, options);
3437
+ const options = this.formDhtRpcOptions();
3438
+ const response = await this.getClient().pauseNeighbor({ messageChainId }, options);
3439
+ return response.accepted;
3360
3440
  }
3361
3441
  async resumeNeighbor(fromTimestamp, messageChainId) {
3362
3442
  const options = this.formDhtRpcOptions({
@@ -3378,9 +3458,10 @@ class PausedNeighbors {
3378
3458
  this.pausedNeighbors.set(msgChainId, new Set());
3379
3459
  }
3380
3460
  if (this.pausedNeighbors.get(msgChainId).size >= this.limit) {
3381
- return;
3461
+ return false;
3382
3462
  }
3383
3463
  this.pausedNeighbors.get(msgChainId).add(node);
3464
+ return true;
3384
3465
  }
3385
3466
  delete(node, msgChainId) {
3386
3467
  this.pausedNeighbors.get(msgChainId)?.delete(node);
@@ -3413,19 +3494,24 @@ class PausedNeighbors {
3413
3494
  }
3414
3495
 
3415
3496
  const MAX_PAUSED_NEIGHBORS_DEFAULT = 3;
3497
+ const DEFAULT_RECOVERY_TIMEOUT = 500;
3498
+ const DEFAULT_RECOVERY_CHECK_INTERVAL = 200;
3499
+ const DEFAULT_RECOVERY_COOLDOWN = 2500;
3416
3500
  const logger$4 = new utils.Logger('PlumtreeManager');
3417
3501
  class PlumtreeManager extends eventemitter3.EventEmitter {
3418
3502
  neighbors;
3419
3503
  localPeerDescriptor;
3420
- // We have paused sending real data to these neighbrs and only send metadata
3421
3504
  localPausedNeighbors;
3422
- // We have asked these nodes to pause sending real data to us, used to limit sending of pausing and resuming requests
3423
3505
  remotePausedNeighbors;
3424
3506
  rpcLocal;
3425
3507
  latestMessages = new Map();
3426
3508
  rpcCommunicator;
3427
- metadataTimestampsAheadOfRealData = new Map();
3428
3509
  maxPausedNeighbors;
3510
+ recoveryState = new Map();
3511
+ recoveryCooldownUntil = new Map();
3512
+ recoveryTimeout;
3513
+ recoveryCooldown;
3514
+ abortController = new AbortController();
3429
3515
  constructor(options) {
3430
3516
  super();
3431
3517
  this.neighbors = options.neighbors;
@@ -3433,12 +3519,22 @@ class PlumtreeManager extends eventemitter3.EventEmitter {
3433
3519
  this.localPeerDescriptor = options.localPeerDescriptor;
3434
3520
  this.localPausedNeighbors = new PausedNeighbors(options.maxPausedNeighbors ?? MAX_PAUSED_NEIGHBORS_DEFAULT);
3435
3521
  this.remotePausedNeighbors = new PausedNeighbors(options.maxPausedNeighbors ?? MAX_PAUSED_NEIGHBORS_DEFAULT);
3522
+ this.recoveryTimeout = options.recoveryTimeout ?? DEFAULT_RECOVERY_TIMEOUT;
3523
+ this.recoveryCooldown = options.recoveryCooldown ?? DEFAULT_RECOVERY_COOLDOWN;
3436
3524
  this.rpcLocal = new PlumtreeRpcLocal(this.neighbors, this.localPausedNeighbors, (metadata, previousNode) => this.onMetadata(metadata, previousNode), (fromTimestamp, msgChainId, remotePeerDescriptor) => this.sendBuffer(fromTimestamp, msgChainId, remotePeerDescriptor));
3437
- this.neighbors.on('nodeRemoved', (nodeId) => this.onNeighborRemoved(nodeId));
3525
+ this.neighbors.on('nodeRemoved', this.onNeighborRemoved);
3438
3526
  this.rpcCommunicator = options.rpcCommunicator;
3439
3527
  this.rpcCommunicator.registerRpcNotification(MessageID, 'sendMetadata', (msg, context) => this.rpcLocal.sendMetadata(msg, context));
3440
- this.rpcCommunicator.registerRpcNotification(PauseNeighborRequest, 'pauseNeighbor', (msg, context) => this.rpcLocal.pauseNeighbor(msg, context));
3528
+ this.rpcCommunicator.registerRpcMethod(PauseNeighborRequest, PauseNeighborResponse, 'pauseNeighbor', (msg, context) => this.rpcLocal.pauseNeighbor(msg, context));
3441
3529
  this.rpcCommunicator.registerRpcNotification(ResumeNeighborRequest, 'resumeNeighbor', (msg, context) => this.rpcLocal.resumeNeighbor(msg, context));
3530
+ utils.setAbortableInterval(() => {
3531
+ const now = performance.now();
3532
+ for (const [chainId, state] of this.recoveryState) {
3533
+ if (now - state.metadataAheadSince >= this.recoveryTimeout && !state.resumeInProgress) {
3534
+ this.attemptRecovery(chainId, state, this.getLatestMessageTimestamp(chainId));
3535
+ }
3536
+ }
3537
+ }, options.recoveryCheckInterval ?? DEFAULT_RECOVERY_CHECK_INTERVAL, this.abortController.signal);
3442
3538
  }
3443
3539
  async pauseNeighbor(node, msgChainId) {
3444
3540
  if (this.neighbors.has(dht.toNodeId(node))
@@ -3446,8 +3542,16 @@ class PlumtreeManager extends eventemitter3.EventEmitter {
3446
3542
  && this.remotePausedNeighbors.size(msgChainId) < this.maxPausedNeighbors) {
3447
3543
  logger$4.debug(`Pausing neighbor ${dht.toNodeId(node)}`);
3448
3544
  this.remotePausedNeighbors.add(dht.toNodeId(node), msgChainId);
3449
- const remote = this.createRemote(node);
3450
- await remote.pauseNeighbor(msgChainId);
3545
+ try {
3546
+ const remote = this.createRemote(node);
3547
+ const accepted = await remote.pauseNeighbor(msgChainId);
3548
+ if (!accepted) {
3549
+ this.remotePausedNeighbors.delete(dht.toNodeId(node), msgChainId);
3550
+ }
3551
+ }
3552
+ catch (_e) {
3553
+ this.remotePausedNeighbors.delete(dht.toNodeId(node), msgChainId);
3554
+ }
3451
3555
  }
3452
3556
  }
3453
3557
  async resumeNeighbor(node, msgChainId, fromTimestamp) {
@@ -3458,9 +3562,15 @@ class PlumtreeManager extends eventemitter3.EventEmitter {
3458
3562
  await remote.resumeNeighbor(fromTimestamp, msgChainId);
3459
3563
  }
3460
3564
  }
3461
- onNeighborRemoved(nodeId) {
3565
+ onNeighborRemoved = (nodeId) => {
3462
3566
  this.localPausedNeighbors.deleteAll(nodeId);
3463
3567
  this.remotePausedNeighbors.deleteAll(nodeId);
3568
+ for (const [_chainId, state] of this.recoveryState) {
3569
+ state.candidates = state.candidates.filter((c) => dht.toNodeId(c) !== nodeId);
3570
+ if (state.lastAttemptedNode !== null && dht.toNodeId(state.lastAttemptedNode) === nodeId) {
3571
+ state.lastAttemptedNode = null;
3572
+ }
3573
+ }
3464
3574
  if (this.neighbors.size() > 0) {
3465
3575
  this.remotePausedNeighbors.forEach((pausedNeighbors, msgChainId) => {
3466
3576
  if (pausedNeighbors.size >= this.neighbors.size()) {
@@ -3470,7 +3580,7 @@ class PlumtreeManager extends eventemitter3.EventEmitter {
3470
3580
  }
3471
3581
  });
3472
3582
  }
3473
- }
3583
+ };
3474
3584
  getLatestMessageTimestamp(msgChainId) {
3475
3585
  if (!this.latestMessages.has(msgChainId) || this.latestMessages.get(msgChainId).length === 0) {
3476
3586
  return 0;
@@ -3480,22 +3590,61 @@ class PlumtreeManager extends eventemitter3.EventEmitter {
3480
3590
  async sendBuffer(fromTimestamp, msgChainId, neighbor) {
3481
3591
  const remote = new ContentDeliveryRpcRemote(this.localPeerDescriptor, neighbor, this.rpcCommunicator, ContentDeliveryRpcClient);
3482
3592
  const messages = this.latestMessages.get(msgChainId)?.filter((msg) => msg.messageId.timestamp > fromTimestamp) ?? [];
3483
- await Promise.all(messages.map((msg) => remote.sendStreamMessage(msg)));
3593
+ for (const msg of messages) {
3594
+ await remote.sendStreamMessage(msg);
3595
+ }
3484
3596
  }
3485
3597
  async onMetadata(msg, previousNode) {
3486
- // If we receive newer metadata than messages in the buffer, resume the sending neighbor
3487
- const latestMessageTimestamp = this.getLatestMessageTimestamp(msg.messageChainId);
3488
- if (latestMessageTimestamp < msg.timestamp) {
3489
- if (!this.metadataTimestampsAheadOfRealData.has(msg.messageChainId)) {
3490
- this.metadataTimestampsAheadOfRealData.set(msg.messageChainId, new Set());
3491
- }
3492
- this.metadataTimestampsAheadOfRealData.get(msg.messageChainId).add(msg.timestamp);
3493
- if (this.metadataTimestampsAheadOfRealData.get(msg.messageChainId).size > 1) {
3494
- await this.resumeNeighbor(previousNode, msg.messageChainId, this.getLatestMessageTimestamp(msg.messageChainId));
3495
- this.metadataTimestampsAheadOfRealData.get(msg.messageChainId).forEach((timestamp) => {
3496
- this.metadataTimestampsAheadOfRealData.get(msg.messageChainId).delete(timestamp);
3497
- });
3498
- }
3598
+ const latestTs = this.getLatestMessageTimestamp(msg.messageChainId);
3599
+ if (latestTs >= msg.timestamp) {
3600
+ return;
3601
+ }
3602
+ const chainId = msg.messageChainId;
3603
+ const cooldownUntil = this.recoveryCooldownUntil.get(chainId);
3604
+ if (cooldownUntil !== undefined && performance.now() < cooldownUntil) {
3605
+ return;
3606
+ }
3607
+ let state = this.recoveryState.get(chainId);
3608
+ if (!state) {
3609
+ state = {
3610
+ timestampsAhead: new Set(),
3611
+ metadataAheadSince: performance.now(),
3612
+ candidates: [],
3613
+ lastAttemptedNode: null,
3614
+ resumeInProgress: false
3615
+ };
3616
+ this.recoveryState.set(chainId, state);
3617
+ }
3618
+ state.timestampsAhead.add(msg.timestamp);
3619
+ const nodeId = dht.toNodeId(previousNode);
3620
+ const isLastAttempted = state.lastAttemptedNode !== null && dht.toNodeId(state.lastAttemptedNode) === nodeId;
3621
+ if (!isLastAttempted && !state.candidates.some((c) => dht.toNodeId(c) === nodeId)) {
3622
+ state.candidates.push(previousNode);
3623
+ }
3624
+ if (state.timestampsAhead.size > 1 && !state.resumeInProgress) {
3625
+ await this.attemptRecovery(chainId, state, latestTs);
3626
+ }
3627
+ }
3628
+ async attemptRecovery(chainId, state, latestTs) {
3629
+ const candidate = state.candidates.shift();
3630
+ if (!candidate) {
3631
+ state.metadataAheadSince = performance.now();
3632
+ return;
3633
+ }
3634
+ state.resumeInProgress = true;
3635
+ state.lastAttemptedNode = candidate;
3636
+ state.candidates = [];
3637
+ state.timestampsAhead.clear();
3638
+ state.metadataAheadSince = performance.now();
3639
+ try {
3640
+ const remote = this.createRemote(candidate);
3641
+ await remote.resumeNeighbor(latestTs, chainId);
3642
+ }
3643
+ catch (_e) {
3644
+ logger$4.debug('Recovery resume failed, will retry with next candidate');
3645
+ }
3646
+ finally {
3647
+ state.resumeInProgress = false;
3499
3648
  }
3500
3649
  }
3501
3650
  createRemote(neighbor) {
@@ -3513,8 +3662,13 @@ class PlumtreeManager extends eventemitter3.EventEmitter {
3513
3662
  this.latestMessages.get(messageChainId).shift();
3514
3663
  this.latestMessages.get(messageChainId).push(msg);
3515
3664
  }
3516
- if (this.metadataTimestampsAheadOfRealData.has(msg.messageId.messageChainId)) {
3517
- this.metadataTimestampsAheadOfRealData.get(msg.messageId.messageChainId).delete(msg.messageId.timestamp);
3665
+ const state = this.recoveryState.get(messageChainId);
3666
+ if (state) {
3667
+ if (state.lastAttemptedNode) {
3668
+ this.remotePausedNeighbors.delete(dht.toNodeId(state.lastAttemptedNode), messageChainId);
3669
+ }
3670
+ this.recoveryState.delete(messageChainId);
3671
+ this.recoveryCooldownUntil.set(messageChainId, performance.now() + this.recoveryCooldown);
3518
3672
  }
3519
3673
  this.emit('message', msg);
3520
3674
  const neighbors = this.neighbors.getAll().filter((neighbor) => dht.toNodeId(neighbor.getPeerDescriptor()) !== previousNode);
@@ -3532,8 +3686,18 @@ class PlumtreeManager extends eventemitter3.EventEmitter {
3532
3686
  return this.localPausedNeighbors.isPaused(dht.toNodeId(node), msgChainId)
3533
3687
  || this.remotePausedNeighbors.isPaused(dht.toNodeId(node), msgChainId);
3534
3688
  }
3689
+ getLocalPausedNeighbors() {
3690
+ return this.localPausedNeighbors;
3691
+ }
3692
+ getRemotePausedNeighbors() {
3693
+ return this.remotePausedNeighbors;
3694
+ }
3535
3695
  stop() {
3696
+ this.abortController.abort();
3536
3697
  this.neighbors.off('nodeRemoved', this.onNeighborRemoved);
3698
+ this.latestMessages.clear();
3699
+ this.recoveryState.clear();
3700
+ this.recoveryCooldownUntil.clear();
3537
3701
  }
3538
3702
  }
3539
3703
 
@@ -4485,5 +4649,7 @@ exports.NetworkNode = NetworkNode;
4485
4649
  exports.NetworkStack = NetworkStack;
4486
4650
  exports.StreamMessage = StreamMessage;
4487
4651
  exports.createNetworkNode = createNetworkNode;
4652
+ exports.logGapDiagnosticSampled = logGapDiagnosticSampled;
4653
+ exports.setTrackerlessGapDiagnosticsEnabled = setGapDiagnosticsEnabled;
4488
4654
  exports.streamPartIdToDataKey = streamPartIdToDataKey;
4489
4655
  //# sourceMappingURL=exports.cjs.map