@streamr/sdk 103.7.0-rc.2 → 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'));
80310
+ }
80311
+ if (this.outgoingMessageListener) {
80312
+ return this.outgoingMessageListener(rpcMessage, rpcMessage.requestId, callContext);
80368
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,8 +86058,56 @@
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();
86113
86063
  function logGapDiagnosticSampled(layer, opts = {}) {
86114
- return;
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
+ }
86115
86111
  }
86116
86112
 
86117
86113
  // @generated message type with reflection information, may provide speed optimized methods
@@ -88141,6 +88137,7 @@
88141
88137
  if ((this.state === ConnectionManagerState.STOPPED || this.state === ConnectionManagerState.STOPPING) && !opts.sendIfStopped) {
88142
88138
  return;
88143
88139
  }
88140
+ logGapDiagnosticSampled('dht.connMgr.send');
88144
88141
  const peerDescriptor = message.targetDescriptor;
88145
88142
  if (this.isConnectionToSelf(peerDescriptor)) {
88146
88143
  throw new CannotConnectToSelf('Cannot send to self');
@@ -88216,6 +88213,7 @@
88216
88213
  this.rpcCommunicator?.handleMessageFromPeer(message);
88217
88214
  }
88218
88215
  else {
88216
+ logGapDiagnosticSampled('dht.connMgr.emitMessage');
88219
88217
  logger$A.trace('emit "message" ' + toNodeId(message.sourceDescriptor)
88220
88218
  + ' ' + message.serviceId + ' ' + message.messageId);
88221
88219
  this.emit('message', message);
@@ -88225,6 +88223,7 @@
88225
88223
  if (this.state === ConnectionManagerState.STOPPED) {
88226
88224
  return;
88227
88225
  }
88226
+ logGapDiagnosticSampled('dht.connMgr.onData');
88228
88227
  this.metrics.receiveBytesPerSecond.record(data.byteLength);
88229
88228
  this.metrics.receiveMessagesPerSecond.record(1);
88230
88229
  let message;
@@ -88504,7 +88503,7 @@
88504
88503
  }
88505
88504
  };
88506
88505
 
88507
- var version$2 = "103.7.0-rc.2";
88506
+ var version$2 = "103.8.0-rc.3";
88508
88507
 
88509
88508
  const logger$y = new Logger('Handshaker');
88510
88509
  // Optimally the Outgoing and Incoming Handshakers could be their own separate classes
@@ -88926,6 +88925,7 @@
88926
88925
  };
88927
88926
  dataChannel.onmessage = (msg) => {
88928
88927
  logger$v.trace('dc.onmessage');
88928
+ logGapDiagnosticSampled('dht.dc.onmessage');
88929
88929
  this.emit('data', new Uint8Array(msg.data));
88930
88930
  };
88931
88931
  dataChannel.onbufferedamountlow = () => {
@@ -88996,6 +88996,66 @@
88996
88996
  * onbufferedamountlow) fire in the worker's event loop. The main thread
88997
88997
  * is never involved in the data path.
88998
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
+ }
88999
89059
  // ── Module-level bridge client (initialized once per worker) ────────
89000
89060
  let resolveBridgeProxy;
89001
89061
  const bridgeProxyPromise = new Promise((resolve) => {
@@ -89038,6 +89098,19 @@
89038
89098
  earlyTimeout;
89039
89099
  messageQueue = [];
89040
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;
89041
89114
  constructor(params) {
89042
89115
  super();
89043
89116
  this.connectionId = createRandomConnectionId();
@@ -89153,6 +89226,59 @@
89153
89226
  }
89154
89227
  }
89155
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
+ }
89156
89282
  setupDataChannel(dataChannel) {
89157
89283
  this.dataChannel = dataChannel;
89158
89284
  this.dataChannel.binaryType = 'arraybuffer';
@@ -89170,7 +89296,11 @@
89170
89296
  };
89171
89297
  dataChannel.onmessage = (msg) => {
89172
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();
89173
89302
  this.emit('data', new Uint8Array(msg.data));
89303
+ recordDcProcessing(performance.now() - t0);
89174
89304
  };
89175
89305
  dataChannel.onbufferedamountlow = () => {
89176
89306
  logger$u.trace('dc.onBufferedAmountLow (worker)');
@@ -93732,7 +93862,7 @@
93732
93862
  }
93733
93863
  }
93734
93864
 
93735
- var version$1 = "103.7.0-rc.2";
93865
+ var version$1 = "103.8.0-rc.3";
93736
93866
 
93737
93867
  // @generated message type with reflection information, may provide speed optimized methods
93738
93868
  let Any$Type$1 = class Any$Type extends MessageType {
@@ -96479,7 +96609,13 @@
96479
96609
  if (!previousNode) {
96480
96610
  markAndCheckDuplicate(this.duplicateDetectors, msg.messageId, msg.previousMessageRef);
96481
96611
  }
96482
- 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
+ }
96483
96619
  const skipBackPropagation = previousNode !== undefined && !this.options.temporaryConnectionRpcLocal.hasNode(previousNode);
96484
96620
  this.options.propagation.feedUnseenMessage(msg, this.getPropagationTargets(msg), skipBackPropagation ? previousNode : null);
96485
96621
  this.messagesPropagated += 1;
@@ -97858,6 +97994,7 @@
97858
97994
  neighborUpdateInterval: this.options.neighborUpdateInterval,
97859
97995
  isLocalNodeEntryPoint,
97860
97996
  bufferWhileConnecting: this.options.bufferWhileConnecting,
97997
+ suppressOwnMessageLoopback: this.options.suppressOwnMessageLoopback,
97861
97998
  plumtreeOptimization: streamPartDeliveryOptions?.plumtreeOptimization?.enabled,
97862
97999
  plumtreeMaxPausedNeighbors: streamPartDeliveryOptions?.plumtreeOptimization?.enabled === true ?
97863
98000
  streamPartDeliveryOptions?.plumtreeOptimization?.maxPausedNeighbors : undefined