@streamr/sdk 103.6.0-rc.0 → 103.8.0-rc.3

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.
@@ -1762,7 +1762,7 @@
1762
1762
  d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
1763
1763
  }
1764
1764
 
1765
- function __awaiter$1(thisArg, _arguments, P, generator) {
1765
+ function __awaiter(thisArg, _arguments, P, generator) {
1766
1766
  function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
1767
1767
  return new (P || (P = Promise))(function (resolve, reject) {
1768
1768
  function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
@@ -2350,7 +2350,7 @@
2350
2350
  });
2351
2351
  };
2352
2352
  InternalDependencyContainer.prototype.dispose = function () {
2353
- return __awaiter$1(this, void 0, void 0, function () {
2353
+ return __awaiter(this, void 0, void 0, function () {
2354
2354
  var promises;
2355
2355
  return __generator$1(this, function (_a) {
2356
2356
  switch (_a.label) {
@@ -79595,52 +79595,6 @@
79595
79595
  }
79596
79596
  }
79597
79597
 
79598
- var __awaiter = (window && window.__awaiter) || function (thisArg, _arguments, P, generator) {
79599
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
79600
- return new (P || (P = Promise))(function (resolve, reject) {
79601
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
79602
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
79603
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
79604
- step((generator = generator.apply(thisArg, _arguments || [])).next());
79605
- });
79606
- };
79607
- /**
79608
- * A unary RPC call. Unary means there is exactly one input message and
79609
- * exactly one output message unless an error occurred.
79610
- */
79611
- class UnaryCall {
79612
- constructor(method, requestHeaders, request, headers, response, status, trailers) {
79613
- this.method = method;
79614
- this.requestHeaders = requestHeaders;
79615
- this.request = request;
79616
- this.headers = headers;
79617
- this.response = response;
79618
- this.status = status;
79619
- this.trailers = trailers;
79620
- }
79621
- /**
79622
- * If you are only interested in the final outcome of this call,
79623
- * you can await it to receive a `FinishedUnaryCall`.
79624
- */
79625
- then(onfulfilled, onrejected) {
79626
- return this.promiseFinished().then(value => onfulfilled ? Promise.resolve(onfulfilled(value)) : value, reason => onrejected ? Promise.resolve(onrejected(reason)) : Promise.reject(reason));
79627
- }
79628
- promiseFinished() {
79629
- return __awaiter(this, void 0, void 0, function* () {
79630
- let [headers, response, status, trailers] = yield Promise.all([this.headers, this.response, this.status, this.trailers]);
79631
- return {
79632
- method: this.method,
79633
- requestHeaders: this.requestHeaders,
79634
- request: this.request,
79635
- headers,
79636
- response,
79637
- status,
79638
- trailers
79639
- };
79640
- });
79641
- }
79642
- }
79643
-
79644
79598
  /**
79645
79599
  * Creates a "stack" of of all interceptors specified in the given `RpcOptions`.
79646
79600
  * Used by generated client implementations.
@@ -79952,33 +79906,42 @@
79952
79906
  notification: notification ? 'notification' : undefined
79953
79907
  };
79954
79908
  }
79955
- unary(method, input, options) {
79956
- if (!options?.isProtoRpc) {
79957
- // eslint-disable-next-line max-len
79958
- throw new Error('ProtoRpc ClientTransport can only be used with ProtoRpcClients. Please convert your protobuf-ts generated client to a ProtoRpcClient by calling toProtoRpcclient(yourClient).');
79959
- }
79960
- const requestBody = Any$3.pack(input, method.I);
79909
+ // ProtoRpc clients drive `request()` / `notification()` directly (via
79910
+ // toProtoRpcClient); this RpcTransport entry point is only reached by a raw
79911
+ // protobuf-ts client that was never wrapped — hence the guard.
79912
+ // eslint-disable-next-line class-methods-use-this
79913
+ unary(_method, _input, _options) {
79914
+ // eslint-disable-next-line max-len
79915
+ throw new Error('ProtoRpc ClientTransport can only be used with ProtoRpcClients. Please convert your protobuf-ts generated client to a ProtoRpcClient by calling toProtoRpcclient(yourClient).');
79916
+ }
79917
+ // Direct request path: one response deferred, no UnaryCall / header /
79918
+ // status / trailer. Returns the response message promise.
79919
+ request(method, input, options) {
79961
79920
  const request = {
79962
- header: ClientTransport.createRequestHeaders(method, options.notification),
79963
- body: requestBody,
79921
+ header: ClientTransport.createRequestHeaders(method, false),
79922
+ body: Any$3.pack(input, method.I),
79964
79923
  requestId: v4()
79965
79924
  };
79966
- const defHeader = new Deferred();
79967
- const defMessage = new Deferred();
79968
- const defStatus = new Deferred();
79969
- const defTrailer = new Deferred();
79970
- const unary = new UnaryCall(method, {}, input, defHeader.promise, defMessage.promise, defStatus.promise, defTrailer.promise);
79971
- const deferredParser = (bytes) => method.O.fromBinary(bytes);
79972
- const deferred = {
79973
- message: defMessage,
79974
- header: defHeader,
79975
- trailer: defTrailer,
79976
- status: defStatus,
79977
- messageParser: deferredParser
79925
+ const message = new Deferred();
79926
+ logger$2$3.trace(`New rpc request, ${request.requestId}`);
79927
+ this.emit('rpcRequest', request, options, {
79928
+ message: message,
79929
+ messageParser: (bytes) => method.O.fromBinary(bytes)
79930
+ });
79931
+ return message.promise;
79932
+ }
79933
+ // Direct notification path: fire-and-forget, no deferreds / OngoingRequest.
79934
+ // Returns the send promise so the caller still sees send success/failure.
79935
+ notification(method, input, options) {
79936
+ const request = {
79937
+ header: ClientTransport.createRequestHeaders(method, true),
79938
+ body: Any$3.pack(input, method.I),
79939
+ requestId: v4()
79978
79940
  };
79979
- logger$2$3.trace(`New rpc ${options.notification ? 'notification' : 'request'}, ${request.requestId}`);
79980
- this.emit('rpcRequest', request, options, deferred);
79981
- return unary;
79941
+ logger$2$3.trace(`New rpc notification, ${request.requestId}`);
79942
+ let sendResult = Promise.resolve();
79943
+ this.emit('rpcNotification', request, options, (result) => { sendResult = result; });
79944
+ return sendResult;
79982
79945
  }
79983
79946
  // eslint-disable-next-line class-methods-use-this
79984
79947
  clientStreaming(method) {
@@ -80178,8 +80141,13 @@
80178
80141
  async handleNotification(rpcMessage, callContext) {
80179
80142
  logger$1$4.trace(`Server processing RPC notification ${rpcMessage.requestId}`);
80180
80143
  const implementation = this.getImplementation(rpcMessage, this.notifications);
80181
- const timeout = implementation.options.timeout;
80182
- await promiseTimeout(timeout, implementation.fn(rpcMessage.body, callContext ?? new ProtoCallContext()));
80144
+ // Notifications have no response and their rejection is swallowed by
80145
+ // the caller (RpcCommunicator.handleNotification logs and drops it),
80146
+ // so a per-message timeout — which armed/cleared a setTimeout on every
80147
+ // inbound data-plane message — bought nothing but timer/promise churn.
80148
+ // Requests keep their timeout in handleRequest (they have a caller
80149
+ // awaiting a result).
80150
+ await implementation.fn(rpcMessage.body, callContext ?? new ProtoCallContext());
80183
80151
  }
80184
80152
  registerRpcMethod(requestClass, returnClass, name, fn, opts = {}) {
80185
80153
  const options = parseOptions(opts);
@@ -80206,7 +80174,6 @@
80206
80174
  }
80207
80175
  }
80208
80176
 
80209
- /* eslint-disable promise/catch-or-return */
80210
80177
  var StatusCode;
80211
80178
  (function (StatusCode) {
80212
80179
  StatusCode["OK"] = "OK";
@@ -80235,17 +80202,6 @@
80235
80202
  }
80236
80203
  this.resolveDeferredPromises(response);
80237
80204
  }
80238
- resolveNotification() {
80239
- if (this.timeoutRef) {
80240
- clearTimeout(this.timeoutRef);
80241
- }
80242
- if (this.deferredPromises.message.state === DeferredState.PENDING) {
80243
- this.deferredPromises.message.resolve({});
80244
- this.deferredPromises.header.resolve({});
80245
- this.deferredPromises.status.resolve({ code: StatusCode.OK, detail: '' });
80246
- this.deferredPromises.trailer.resolve({});
80247
- }
80248
- }
80249
80205
  rejectRequest(error, code) {
80250
80206
  if (this.timeoutRef) {
80251
80207
  clearTimeout(this.timeoutRef);
@@ -80257,9 +80213,6 @@
80257
80213
  try {
80258
80214
  const parsedResponse = this.deferredPromises.messageParser(response.body.value);
80259
80215
  this.deferredPromises.message.resolve(parsedResponse);
80260
- this.deferredPromises.header.resolve({});
80261
- this.deferredPromises.status.resolve({ code: StatusCode.OK, detail: '' });
80262
- this.deferredPromises.trailer.resolve({});
80263
80216
  }
80264
80217
  catch (err) {
80265
80218
  logger$z.debug(`Could not parse response, received message is likely `);
@@ -80268,12 +80221,9 @@
80268
80221
  }
80269
80222
  }
80270
80223
  }
80271
- rejectDeferredPromises(error, code) {
80224
+ rejectDeferredPromises(error, _code) {
80272
80225
  if (this.deferredPromises.message.state === DeferredState.PENDING) {
80273
80226
  this.deferredPromises.message.reject(error);
80274
- this.deferredPromises.header.reject(error);
80275
- this.deferredPromises.status.reject({ code, detail: error.message });
80276
- this.deferredPromises.trailer.reject(error);
80277
80227
  }
80278
80228
  }
80279
80229
  fulfillsPredicate(predicate) {
@@ -80300,6 +80250,10 @@
80300
80250
  this.rpcClientTransport.on('rpcRequest', (rpcMessage, options, deferredPromises) => {
80301
80251
  this.onOutgoingMessage(rpcMessage, options, deferredPromises);
80302
80252
  });
80253
+ // Client side listener for outgoing notification (fire-and-forget).
80254
+ this.rpcClientTransport.on('rpcNotification', (rpcMessage, options, setSendResult) => {
80255
+ setSendResult(this.onOutgoingNotification(rpcMessage, options));
80256
+ });
80303
80257
  }
80304
80258
  async handleIncomingMessage(message, callContext) {
80305
80259
  if (this.stopped) {
@@ -80324,6 +80278,8 @@
80324
80278
  this.ongoingRequests.clear();
80325
80279
  this.rpcClientTransport.stop();
80326
80280
  }
80281
+ // Sends a client request (deferredPromises present) or a server response
80282
+ // (deferredPromises undefined). Notifications use onOutgoingNotification.
80327
80283
  onOutgoingMessage(rpcMessage, callContext, deferredPromises) {
80328
80284
  if (this.stopped) {
80329
80285
  if (deferredPromises) {
@@ -80332,40 +80288,30 @@
80332
80288
  }
80333
80289
  return;
80334
80290
  }
80335
- const requestOptions = this.rpcClientTransport.mergeOptions(callContext);
80336
- // do not register a notification
80337
- if (deferredPromises && (!callContext || !callContext.notification)) {
80338
- this.registerRequest(rpcMessage.requestId, deferredPromises, callContext, requestOptions.timeout);
80291
+ if (deferredPromises) {
80292
+ const timeout = callContext.timeout ?? this.rpcRequestTimeout;
80293
+ this.registerRequest(rpcMessage.requestId, deferredPromises, callContext, timeout);
80339
80294
  }
80340
80295
  logger$z.trace(`onOutGoingMessage, messageId: ${rpcMessage.requestId}`);
80341
80296
  if (this.outgoingMessageListener) {
80342
80297
  this.outgoingMessageListener(rpcMessage, rpcMessage.requestId, callContext)
80343
80298
  .catch((clientSideException) => {
80344
80299
  if (deferredPromises) {
80345
- if (this.ongoingRequests.has(rpcMessage.requestId)) {
80346
- this.handleClientError(rpcMessage.requestId, clientSideException);
80347
- }
80348
- else {
80349
- const ongoingRequest = new OngoingRequest(deferredPromises, callContext);
80350
- ongoingRequest.rejectRequest(clientSideException, StatusCode.SERVER_ERROR);
80351
- }
80352
- }
80353
- })
80354
- .then(() => {
80355
- if (deferredPromises) {
80356
- if (!this.ongoingRequests.has(rpcMessage.requestId)) {
80357
- const ongoingRequest = new OngoingRequest(deferredPromises, callContext);
80358
- ongoingRequest.resolveNotification();
80359
- }
80300
+ this.handleClientError(rpcMessage.requestId, clientSideException);
80360
80301
  }
80361
80302
  });
80362
80303
  }
80363
- else if (deferredPromises) {
80364
- if (!this.ongoingRequests.has(rpcMessage.requestId)) {
80365
- const ongoingRequest = new OngoingRequest(deferredPromises, callContext);
80366
- ongoingRequest.resolveNotification();
80367
- }
80304
+ }
80305
+ // Fire-and-forget outgoing notification: no deferreds, no OngoingRequest.
80306
+ // Returns the send promise so the caller observes send success/failure.
80307
+ onOutgoingNotification(rpcMessage, callContext) {
80308
+ if (this.stopped) {
80309
+ return Promise.reject(new Error('stopped'));
80368
80310
  }
80311
+ if (this.outgoingMessageListener) {
80312
+ return this.outgoingMessageListener(rpcMessage, rpcMessage.requestId, callContext);
80313
+ }
80314
+ return Promise.resolve();
80369
80315
  }
80370
80316
  async onIncomingMessage(rpcMessage, callContext) {
80371
80317
  logger$z.trace(`onIncomingMessage, requestId: ${rpcMessage.requestId}`);
@@ -80505,41 +80451,43 @@
80505
80451
  }
80506
80452
  }
80507
80453
 
80508
- /* eslint-disable prefer-spread, @typescript-eslint/consistent-indexed-object-style */
80454
+ /* eslint-disable @typescript-eslint/consistent-indexed-object-style */
80509
80455
  function toProtoRpcClient(orig) {
80510
80456
  const ret = {};
80511
80457
  Object.assign(ret, orig);
80512
- const notify = async (methodName, obj, args) => {
80513
- if (args.length < 2) {
80514
- args.push({});
80515
- }
80516
- else {
80517
- args[1] ??= {};
80518
- }
80519
- args[1].isProtoRpc = true;
80520
- args[1].notification = true;
80521
- await obj[methodName].apply(obj, args);
80458
+ // The generated protobuf-ts client only exists to forward to its transport;
80459
+ // we drive that transport directly (building the RpcMessage ourselves),
80460
+ // bypassing UnaryCall / mergeOptions / stackIntercept. `_transport` is the
80461
+ // protobuf-ts generated client's parameter property.
80462
+ // eslint-disable-next-line no-underscore-dangle
80463
+ const transport = orig._transport;
80464
+ const buildOptions = (args, notification) => {
80465
+ const options = (args.length >= 2 && args[1] != undefined) ? args[1] : {};
80466
+ options.isProtoRpc = true;
80467
+ if (notification) {
80468
+ options.notification = true;
80469
+ }
80470
+ return options;
80522
80471
  };
80523
- const callRpc = (methodName, obj, args) => {
80524
- if (args.length < 2) {
80525
- args.push({});
80526
- }
80527
- else {
80528
- args[1] ??= {};
80529
- }
80530
- args[1].isProtoRpc = true;
80531
- return obj[methodName].apply(obj, args);
80472
+ // Legacy fallback (only if the client is backed by a non-ClientTransport
80473
+ // RpcTransport never happens in this codebase): keep the generated path.
80474
+ const legacyNotify = async (methodName, obj, args) => {
80475
+ await obj[methodName].apply(obj, [args[0], buildOptions(args, true)]);
80476
+ };
80477
+ const legacyCall = (methodName, obj, args) => {
80478
+ return obj[methodName].apply(obj, [args[0], buildOptions(args, false)]);
80532
80479
  };
80533
80480
  orig.methods.forEach((method) => {
80534
- if (method.O.typeName === Empty$3.typeName) {
80535
- ret[method.name] = (...args) => {
80536
- return notify(method.name, orig, args);
80537
- };
80481
+ const isNotification = method.O.typeName === Empty$3.typeName;
80482
+ if (transport instanceof ClientTransport) {
80483
+ ret[method.name] = isNotification
80484
+ ? (...args) => transport.notification(method, args[0], buildOptions(args, true))
80485
+ : (...args) => transport.request(method, args[0], buildOptions(args, false));
80538
80486
  }
80539
80487
  else {
80540
- ret[method.name] = (...args) => {
80541
- return callRpc(method.name, orig, args).response;
80542
- };
80488
+ ret[method.name] = isNotification
80489
+ ? (...args) => legacyNotify(method.name, orig, args)
80490
+ : (...args) => legacyCall(method.name, orig, args).response;
80543
80491
  }
80544
80492
  });
80545
80493
  return ret;
@@ -86110,6 +86058,57 @@
86110
86058
  class SendFailed extends Err {
86111
86059
  constructor(message, originalError) { super(ErrorCode.SEND_FAILED, message, originalError); }
86112
86060
  }
86061
+ const SUMMARY_INTERVAL_MS = 2000;
86062
+ const accumulators = new Map();
86063
+ function logGapDiagnosticSampled(layer, opts = {}) {
86064
+ const now = performance.now();
86065
+ const threshold = opts.outlierThresholdMs ?? 30;
86066
+ let acc = accumulators.get(layer);
86067
+ if (acc === undefined) {
86068
+ acc = { count: 0, sumDeltaMs: 0, maxDeltaMs: 0, outlierCount: 0, lastReportMs: now, lastEventMs: now };
86069
+ accumulators.set(layer, acc);
86070
+ return;
86071
+ }
86072
+ const deltaMs = now - acc.lastEventMs;
86073
+ acc.lastEventMs = now;
86074
+ acc.count++;
86075
+ if (deltaMs > acc.maxDeltaMs)
86076
+ acc.maxDeltaMs = deltaMs;
86077
+ acc.sumDeltaMs += deltaMs;
86078
+ if (deltaMs > threshold)
86079
+ acc.outlierCount++;
86080
+ if (deltaMs > threshold) {
86081
+ const payload = {
86082
+ layer,
86083
+ timestampMs: now,
86084
+ deltaMs: +deltaMs.toFixed(2),
86085
+ detail: opts.detail,
86086
+ };
86087
+ // eslint-disable-next-line no-console
86088
+ console.log('[gap-diagnostics]', JSON.stringify(payload));
86089
+ }
86090
+ if (now - acc.lastReportMs >= SUMMARY_INTERVAL_MS) {
86091
+ const summaryDetail = {
86092
+ count: acc.count,
86093
+ meanDeltaMs: acc.count > 0 ? +(acc.sumDeltaMs / acc.count).toFixed(2) : 0,
86094
+ maxDeltaMs: +acc.maxDeltaMs.toFixed(2),
86095
+ outlierCount: acc.outlierCount,
86096
+ periodMs: +(now - acc.lastReportMs).toFixed(1),
86097
+ };
86098
+ const summary = {
86099
+ layer: `${layer}.summary`,
86100
+ timestampMs: now,
86101
+ detail: summaryDetail,
86102
+ };
86103
+ // eslint-disable-next-line no-console
86104
+ console.log('[gap-diagnostics]', JSON.stringify(summary));
86105
+ acc.count = 0;
86106
+ acc.sumDeltaMs = 0;
86107
+ acc.maxDeltaMs = 0;
86108
+ acc.outlierCount = 0;
86109
+ acc.lastReportMs = now;
86110
+ }
86111
+ }
86113
86112
 
86114
86113
  // @generated message type with reflection information, may provide speed optimized methods
86115
86114
  let Empty$Type$2 = class Empty$Type extends MessageType {
@@ -88138,6 +88137,7 @@
88138
88137
  if ((this.state === ConnectionManagerState.STOPPED || this.state === ConnectionManagerState.STOPPING) && !opts.sendIfStopped) {
88139
88138
  return;
88140
88139
  }
88140
+ logGapDiagnosticSampled('dht.connMgr.send');
88141
88141
  const peerDescriptor = message.targetDescriptor;
88142
88142
  if (this.isConnectionToSelf(peerDescriptor)) {
88143
88143
  throw new CannotConnectToSelf('Cannot send to self');
@@ -88213,6 +88213,7 @@
88213
88213
  this.rpcCommunicator?.handleMessageFromPeer(message);
88214
88214
  }
88215
88215
  else {
88216
+ logGapDiagnosticSampled('dht.connMgr.emitMessage');
88216
88217
  logger$A.trace('emit "message" ' + toNodeId(message.sourceDescriptor)
88217
88218
  + ' ' + message.serviceId + ' ' + message.messageId);
88218
88219
  this.emit('message', message);
@@ -88222,6 +88223,7 @@
88222
88223
  if (this.state === ConnectionManagerState.STOPPED) {
88223
88224
  return;
88224
88225
  }
88226
+ logGapDiagnosticSampled('dht.connMgr.onData');
88225
88227
  this.metrics.receiveBytesPerSecond.record(data.byteLength);
88226
88228
  this.metrics.receiveMessagesPerSecond.record(1);
88227
88229
  let message;
@@ -88501,7 +88503,7 @@
88501
88503
  }
88502
88504
  };
88503
88505
 
88504
- var version$2 = "103.6.0-rc.0";
88506
+ var version$2 = "103.8.0-rc.3";
88505
88507
 
88506
88508
  const logger$y = new Logger('Handshaker');
88507
88509
  // Optimally the Outgoing and Incoming Handshakers could be their own separate classes
@@ -88892,6 +88894,9 @@
88892
88894
  }
88893
88895
  send(data) {
88894
88896
  if (this.lastState === 'connected') {
88897
+ logGapDiagnosticSampled('dht.dc.send', {
88898
+ detail: { bufferedAmount: this.dataChannel.bufferedAmount, queueLen: this.messageQueue.length }
88899
+ });
88895
88900
  if (this.dataChannel.bufferedAmount > this.bufferThresholdHigh) {
88896
88901
  this.messageQueue.push(data);
88897
88902
  }
@@ -88920,6 +88925,7 @@
88920
88925
  };
88921
88926
  dataChannel.onmessage = (msg) => {
88922
88927
  logger$v.trace('dc.onmessage');
88928
+ logGapDiagnosticSampled('dht.dc.onmessage');
88923
88929
  this.emit('data', new Uint8Array(msg.data));
88924
88930
  };
88925
88931
  dataChannel.onbufferedamountlow = () => {
@@ -88990,6 +88996,66 @@
88990
88996
  * onbufferedamountlow) fire in the worker's event loop. The main thread
88991
88997
  * is never involved in the data path.
88992
88998
  */
88999
+ // ── agent log: layer0-vs-layer2 contention probe ────────────────────
89000
+ // Every DHT connection's datachannel `onmessage` runs in THIS one worker, and
89001
+ // `emit('data')` synchronously drives the downstream routing / RPC dispatch.
89002
+ // So timing each emit captures the per-message synchronous processing cost —
89003
+ // and aggregating across ALL connections tells us how much of the worker's
89004
+ // event loop is consumed servicing GLOBAL-DHT layer0 traffic (routing for the
89005
+ // hundreds of nodes we forward for) vs our handful of media messages. If a
89006
+ // media arrival gap coincides with a window of high non-media processing, that
89007
+ // is layer0 starving layer2. Emitted in the same `[gap-diagnostics]` line
89008
+ // shape the analyzer already parses.
89009
+ let dcWinStart = performance.now();
89010
+ let dcWinCount = 0;
89011
+ let dcWinBusyMs = 0;
89012
+ let dcWinMaxMs = 0;
89013
+ function recordDcProcessing(procMs) {
89014
+ dcWinCount++;
89015
+ dcWinBusyMs += procMs;
89016
+ if (procMs > dcWinMaxMs)
89017
+ dcWinMaxMs = procMs;
89018
+ // Individual event for a single message whose inline processing blocked the
89019
+ // loop unusually long (one expensive routing/RPC dispatch).
89020
+ if (procMs > 15) {
89021
+ try {
89022
+ console.log('[gap-diagnostics]', JSON.stringify({
89023
+ layer: 'dht.dc.processing.slow',
89024
+ timestampMs: performance.now(),
89025
+ deltaMs: +procMs.toFixed(2),
89026
+ }));
89027
+ }
89028
+ catch {
89029
+ /* ignore */
89030
+ }
89031
+ }
89032
+ const now = performance.now();
89033
+ const winElapsed = now - dcWinStart;
89034
+ if (winElapsed >= 1000) {
89035
+ try {
89036
+ console.log('[gap-diagnostics]', JSON.stringify({
89037
+ layer: 'dht.dc.processing.window',
89038
+ timestampMs: now,
89039
+ detail: {
89040
+ windowMs: +winElapsed.toFixed(0),
89041
+ count: dcWinCount,
89042
+ msgPerSec: +((dcWinCount / winElapsed) * 1000).toFixed(0),
89043
+ busyMs: +dcWinBusyMs.toFixed(1),
89044
+ occupancyPct: +((dcWinBusyMs / winElapsed) *
89045
+ 100).toFixed(1),
89046
+ maxMs: +dcWinMaxMs.toFixed(1),
89047
+ },
89048
+ }));
89049
+ }
89050
+ catch {
89051
+ /* ignore */
89052
+ }
89053
+ dcWinStart = now;
89054
+ dcWinCount = 0;
89055
+ dcWinBusyMs = 0;
89056
+ dcWinMaxMs = 0;
89057
+ }
89058
+ }
88993
89059
  // ── Module-level bridge client (initialized once per worker) ────────
88994
89060
  let resolveBridgeProxy;
88995
89061
  const bridgeProxyPromise = new Promise((resolve) => {
@@ -89032,6 +89098,19 @@
89032
89098
  earlyTimeout;
89033
89099
  messageQueue = [];
89034
89100
  startPromise;
89101
+ // agent log: PER-CONNECTION datachannel receive cadence. The global
89102
+ // `dht.dc.onmessage` accumulator can't isolate one stream; this tracks
89103
+ // inter-message arrival on THIS connection so we can pick the connection
89104
+ // carrying the composite media (highest count/bytes) and compare its
89105
+ // datachannel-level gaps against messageArrival (post-routing) and
89106
+ // videoFrameArrival (post-decrypt) — i.e. localize WHERE the gap is born.
89107
+ lastRecvMs;
89108
+ recvWinStart = 0;
89109
+ recvCount = 0;
89110
+ recvSumDelta = 0;
89111
+ recvMaxDelta = 0;
89112
+ recvBytes = 0;
89113
+ recvMaxBytes = 0;
89035
89114
  constructor(params) {
89036
89115
  super();
89037
89116
  this.connectionId = createRandomConnectionId();
@@ -89072,7 +89151,7 @@
89072
89151
  if (state === DisconnectedState.CLOSED ||
89073
89152
  state === DisconnectedState.DISCONNECTED ||
89074
89153
  state === DisconnectedState.FAILED) {
89075
- this.doClose(false);
89154
+ this.doClose(false, `pcState=${state}`);
89076
89155
  }
89077
89156
  },
89078
89157
  onDataChannel: (channel) => {
@@ -89125,6 +89204,9 @@
89125
89204
  }
89126
89205
  send(data) {
89127
89206
  if (this.connected && this.dataChannel) {
89207
+ logGapDiagnosticSampled('dht.dc.send', {
89208
+ detail: { bufferedAmount: this.dataChannel.bufferedAmount, queueLen: this.messageQueue.length }
89209
+ });
89128
89210
  if (this.dataChannel.bufferedAmount > this.bufferThresholdHigh) {
89129
89211
  this.messageQueue.push(data);
89130
89212
  }
@@ -89144,6 +89226,59 @@
89144
89226
  }
89145
89227
  }
89146
89228
  // ── DataChannel handling (runs entirely in the worker) ──────
89229
+ recordRecv(bytes) {
89230
+ const now = performance.now();
89231
+ const conn = String(this.connectionId).slice(0, 8);
89232
+ if (this.lastRecvMs !== undefined) {
89233
+ const delta = now - this.lastRecvMs;
89234
+ this.recvSumDelta += delta;
89235
+ if (delta > this.recvMaxDelta)
89236
+ this.recvMaxDelta = delta;
89237
+ if (delta > 60) {
89238
+ try {
89239
+ console.log('[gap-diagnostics]', JSON.stringify({
89240
+ layer: 'dht.dc.recvGap',
89241
+ timestampMs: now,
89242
+ deltaMs: +delta.toFixed(1),
89243
+ detail: { conn, bytes },
89244
+ }));
89245
+ }
89246
+ catch (_e) { /* ignore */ }
89247
+ }
89248
+ }
89249
+ this.lastRecvMs = now;
89250
+ this.recvCount++;
89251
+ this.recvBytes += bytes;
89252
+ if (bytes > this.recvMaxBytes)
89253
+ this.recvMaxBytes = bytes;
89254
+ if (this.recvWinStart === 0)
89255
+ this.recvWinStart = now;
89256
+ const elapsed = now - this.recvWinStart;
89257
+ if (elapsed >= 1000) {
89258
+ try {
89259
+ console.log('[gap-diagnostics]', JSON.stringify({
89260
+ layer: 'dht.dc.recv',
89261
+ timestampMs: now,
89262
+ detail: {
89263
+ conn,
89264
+ count: this.recvCount,
89265
+ perSec: Math.round((this.recvCount / elapsed) * 1000),
89266
+ meanMs: +(this.recvSumDelta / Math.max(1, this.recvCount)).toFixed(1),
89267
+ maxMs: +this.recvMaxDelta.toFixed(1),
89268
+ bytesPerSec: Math.round((this.recvBytes / elapsed) * 1000),
89269
+ maxBytes: this.recvMaxBytes,
89270
+ },
89271
+ }));
89272
+ }
89273
+ catch (_e) { /* ignore */ }
89274
+ this.recvWinStart = now;
89275
+ this.recvCount = 0;
89276
+ this.recvSumDelta = 0;
89277
+ this.recvMaxDelta = 0;
89278
+ this.recvBytes = 0;
89279
+ this.recvMaxBytes = 0;
89280
+ }
89281
+ }
89147
89282
  setupDataChannel(dataChannel) {
89148
89283
  this.dataChannel = dataChannel;
89149
89284
  this.dataChannel.binaryType = 'arraybuffer';
@@ -89154,14 +89289,18 @@
89154
89289
  };
89155
89290
  dataChannel.onclose = () => {
89156
89291
  logger$u.trace('dc.onClosed (worker)');
89157
- this.doClose(false);
89292
+ this.doClose(false, 'dataChannel.onclose');
89158
89293
  };
89159
89294
  dataChannel.onerror = (err) => {
89160
89295
  logger$u.warn('Data channel error (worker)', { err });
89161
89296
  };
89162
89297
  dataChannel.onmessage = (msg) => {
89163
89298
  logger$u.trace('dc.onmessage (worker)');
89299
+ logGapDiagnosticSampled('dht.dc.onmessage');
89300
+ this.recordRecv(msg.data instanceof ArrayBuffer ? msg.data.byteLength : 0);
89301
+ const t0 = performance.now();
89164
89302
  this.emit('data', new Uint8Array(msg.data));
89303
+ recordDcProcessing(performance.now() - t0);
89165
89304
  };
89166
89305
  dataChannel.onbufferedamountlow = () => {
89167
89306
  logger$u.trace('dc.onBufferedAmountLow (worker)');
@@ -91407,7 +91546,7 @@
91407
91546
  logger$d$1.debug(`Ring join on ${this.options.serviceId} timed out`);
91408
91547
  }
91409
91548
  finally {
91410
- sessions.forEach((session) => this.ongoingDiscoverySessions.delete(session.id));
91549
+ sessions.forEach((session) => this.ongoingRingDiscoverySessions.delete(session.id));
91411
91550
  }
91412
91551
  }
91413
91552
  async rejoinDht(entryPoint, contactedPeers = new Set(), distantJoinContactPeers = new Set()) {
@@ -93723,7 +93862,7 @@
93723
93862
  }
93724
93863
  }
93725
93864
 
93726
- var version$1 = "103.6.0-rc.0";
93865
+ var version$1 = "103.8.0-rc.3";
93727
93866
 
93728
93867
  // @generated message type with reflection information, may provide speed optimized methods
93729
93868
  let Any$Type$1 = class Any$Type extends MessageType {
@@ -96464,12 +96603,19 @@
96464
96603
  this.options.neighborFinder.stop();
96465
96604
  this.options.neighborUpdateManager.stop();
96466
96605
  this.options.inspector.stop();
96606
+ this.duplicateDetectors.clear();
96467
96607
  }
96468
96608
  broadcast(msg, previousNode) {
96469
96609
  if (!previousNode) {
96470
96610
  markAndCheckDuplicate(this.duplicateDetectors, msg.messageId, msg.previousMessageRef);
96471
96611
  }
96472
- this.emit('message', msg);
96612
+ // Deliver to local listeners — except own publishes (no previousNode)
96613
+ // when loopback is suppressed: nothing local consumes them and they
96614
+ // would otherwise be re-serialized across a worker boundary just to be
96615
+ // discarded. Propagation + duplicate detection below are unaffected.
96616
+ if (previousNode !== undefined || !this.options.suppressOwnMessageLoopback) {
96617
+ this.emit('message', msg);
96618
+ }
96473
96619
  const skipBackPropagation = previousNode !== undefined && !this.options.temporaryConnectionRpcLocal.hasNode(previousNode);
96474
96620
  this.options.propagation.feedUnseenMessage(msg, this.getPropagationTargets(msg), skipBackPropagation ? previousNode : null);
96475
96621
  this.messagesPropagated += 1;
@@ -97323,6 +97469,9 @@
97323
97469
  stop() {
97324
97470
  this.abortController.abort();
97325
97471
  this.neighbors.off('nodeRemoved', this.onNeighborRemoved);
97472
+ this.latestMessages.clear();
97473
+ this.recoveryState.clear();
97474
+ this.recoveryCooldownUntil.clear();
97326
97475
  }
97327
97476
  }
97328
97477
 
@@ -97845,6 +97994,7 @@
97845
97994
  neighborUpdateInterval: this.options.neighborUpdateInterval,
97846
97995
  isLocalNodeEntryPoint,
97847
97996
  bufferWhileConnecting: this.options.bufferWhileConnecting,
97997
+ suppressOwnMessageLoopback: this.options.suppressOwnMessageLoopback,
97848
97998
  plumtreeOptimization: streamPartDeliveryOptions?.plumtreeOptimization?.enabled,
97849
97999
  plumtreeMaxPausedNeighbors: streamPartDeliveryOptions?.plumtreeOptimization?.enabled === true ?
97850
98000
  streamPartDeliveryOptions?.plumtreeOptimization?.maxPausedNeighbors : undefined