@naylence/runtime 0.3.5-test.961 → 0.3.5-test.962

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.
@@ -49,6 +49,26 @@ class BroadcastChannelConnector extends base_async_connector_js_1.BaseAsyncConne
49
49
  }
50
50
  return null;
51
51
  }
52
+ static normalizeNodeId(value) {
53
+ if (typeof value !== 'string') {
54
+ return null;
55
+ }
56
+ const trimmed = value.trim();
57
+ return trimmed.length > 0 ? trimmed : null;
58
+ }
59
+ static normalizeTargetNodeId(value) {
60
+ if (typeof value !== 'string') {
61
+ return undefined;
62
+ }
63
+ const trimmed = value.trim();
64
+ if (trimmed.length === 0) {
65
+ return undefined;
66
+ }
67
+ if (trimmed === '*') {
68
+ return '*';
69
+ }
70
+ return trimmed;
71
+ }
52
72
  constructor(config, baseConfig = {}) {
53
73
  ensureBroadcastEnvironment();
54
74
  super(baseConfig);
@@ -71,10 +91,18 @@ class BroadcastChannelConnector extends base_async_connector_js_1.BaseAsyncConne
71
91
  this.inbox = new bounded_async_queue_js_1.BoundedAsyncQueue(preferredCapacity);
72
92
  this.inboxCapacity = preferredCapacity;
73
93
  this.connectorId = BroadcastChannelConnector.generateConnectorId();
94
+ const normalizedLocalNodeId = BroadcastChannelConnector.normalizeNodeId(config.localNodeId);
95
+ if (!normalizedLocalNodeId) {
96
+ throw new Error('BroadcastChannelConnector requires a non-empty localNodeId');
97
+ }
98
+ this.localNodeId = normalizedLocalNodeId;
99
+ this.targetNodeId = BroadcastChannelConnector.normalizeTargetNodeId(config.initialTargetNodeId);
74
100
  this.channel = new BroadcastChannel(this.channelName);
75
101
  logger.debug('broadcast_channel_connector_created', {
76
102
  channel: this.channelName,
77
103
  connector_id: this.connectorId,
104
+ local_node_id: this.localNodeId,
105
+ target_node_id: this.targetNodeId ?? null,
78
106
  inbox_capacity: preferredCapacity,
79
107
  timestamp: new Date().toISOString(),
80
108
  });
@@ -96,15 +124,32 @@ class BroadcastChannelConnector extends base_async_connector_js_1.BaseAsyncConne
96
124
  ? message.constructor?.name ?? typeof message
97
125
  : typeof message,
98
126
  has_sender_id: Boolean(message?.senderId),
127
+ has_sender_node_id: Boolean(message?.senderNodeId),
99
128
  });
100
129
  if (!message || typeof message !== 'object') {
101
130
  return;
102
131
  }
103
132
  const busMessage = message;
104
- if (typeof busMessage.senderId !== 'string' || busMessage.senderId.length === 0) {
133
+ const senderNodeId = BroadcastChannelConnector.normalizeNodeId(busMessage.senderNodeId);
134
+ if (!senderNodeId) {
135
+ logger.debug('broadcast_channel_message_rejected', {
136
+ channel: this.channelName,
137
+ connector_id: this.connectorId,
138
+ reason: 'missing_sender_node_id',
139
+ });
140
+ return;
141
+ }
142
+ if (senderNodeId === this.localNodeId) {
143
+ logger.debug('broadcast_channel_message_rejected', {
144
+ channel: this.channelName,
145
+ connector_id: this.connectorId,
146
+ reason: 'self_echo',
147
+ sender_node_id: senderNodeId,
148
+ });
105
149
  return;
106
150
  }
107
- if (busMessage.senderId === this.connectorId) {
151
+ const incomingTargetNodeId = BroadcastChannelConnector.normalizeTargetNodeId(busMessage.targetNodeId);
152
+ if (!this._shouldAcceptMessageFromBus(senderNodeId, incomingTargetNodeId)) {
108
153
  return;
109
154
  }
110
155
  const payload = BroadcastChannelConnector.coercePayload(busMessage.payload);
@@ -118,11 +163,13 @@ class BroadcastChannelConnector extends base_async_connector_js_1.BaseAsyncConne
118
163
  }
119
164
  logger.debug('broadcast_channel_message_received', {
120
165
  channel: this.channelName,
121
- sender_id: busMessage.senderId,
166
+ sender_id: message?.senderId,
167
+ sender_node_id: senderNodeId,
168
+ target_node_id: incomingTargetNodeId ?? null,
122
169
  connector_id: this.connectorId,
123
170
  payload_length: payload.byteLength,
124
171
  });
125
- if (this._shouldSkipDuplicateAck(busMessage.senderId, payload)) {
172
+ if (this._shouldSkipDuplicateAck(senderNodeId, payload)) {
126
173
  return;
127
174
  }
128
175
  try {
@@ -266,12 +313,17 @@ class BroadcastChannelConnector extends base_async_connector_js_1.BaseAsyncConne
266
313
  }
267
314
  async _transportSendBytes(data) {
268
315
  ensureBroadcastEnvironment();
316
+ const targetNodeId = this.targetNodeId ?? '*';
269
317
  logger.debug('broadcast_channel_message_sending', {
270
318
  channel: this.channelName,
271
319
  sender_id: this.connectorId,
320
+ sender_node_id: this.localNodeId,
321
+ target_node_id: targetNodeId,
272
322
  });
273
323
  this.channel.postMessage({
274
324
  senderId: this.connectorId,
325
+ senderNodeId: this.localNodeId,
326
+ targetNodeId,
275
327
  payload: data,
276
328
  });
277
329
  }
@@ -334,6 +386,51 @@ class BroadcastChannelConnector extends base_async_connector_js_1.BaseAsyncConne
334
386
  }
335
387
  return rawOrEnvelope;
336
388
  }
389
+ _isWildcardTarget() {
390
+ return this.targetNodeId === '*' || typeof this.targetNodeId === 'undefined';
391
+ }
392
+ _shouldAcceptMessageFromBus(senderNodeId, targetNodeId) {
393
+ if (this._isWildcardTarget()) {
394
+ if (targetNodeId && targetNodeId !== '*') {
395
+ logger.debug('broadcast_channel_message_rejected', {
396
+ channel: this.channelName,
397
+ connector_id: this.connectorId,
398
+ reason: 'wildcard_target_mismatch',
399
+ sender_node_id: senderNodeId,
400
+ target_node_id: targetNodeId,
401
+ local_node_id: this.localNodeId,
402
+ });
403
+ return false;
404
+ }
405
+ return true;
406
+ }
407
+ const expectedSender = this.targetNodeId;
408
+ if (expectedSender && expectedSender !== '*' && senderNodeId !== expectedSender) {
409
+ logger.debug('broadcast_channel_message_rejected', {
410
+ channel: this.channelName,
411
+ connector_id: this.connectorId,
412
+ reason: 'unexpected_sender',
413
+ expected_sender_node_id: expectedSender,
414
+ sender_node_id: senderNodeId,
415
+ local_node_id: this.localNodeId,
416
+ });
417
+ return false;
418
+ }
419
+ if (targetNodeId &&
420
+ targetNodeId !== '*' &&
421
+ targetNodeId !== this.localNodeId) {
422
+ logger.debug('broadcast_channel_message_rejected', {
423
+ channel: this.channelName,
424
+ connector_id: this.connectorId,
425
+ reason: 'unexpected_target',
426
+ sender_node_id: senderNodeId,
427
+ target_node_id: targetNodeId,
428
+ local_node_id: this.localNodeId,
429
+ });
430
+ return false;
431
+ }
432
+ return true;
433
+ }
337
434
  _describeInboxItem(item) {
338
435
  if (item instanceof Uint8Array) {
339
436
  return 'bytes';
@@ -364,7 +461,7 @@ class BroadcastChannelConnector extends base_async_connector_js_1.BaseAsyncConne
364
461
  const normalizedSenderId = typeof senderId === 'string' && senderId.length > 0
365
462
  ? senderId
366
463
  : undefined;
367
- if (normalizedSenderId && normalizedSenderId !== this.connectorId) {
464
+ if (normalizedSenderId && normalizedSenderId !== this.localNodeId) {
368
465
  logger.debug('broadcast_channel_duplicate_ack_bypass_non_self', {
369
466
  channel: this.channelName,
370
467
  connector_id: this.connectorId,
@@ -404,7 +501,7 @@ class BroadcastChannelConnector extends base_async_connector_js_1.BaseAsyncConne
404
501
  return false;
405
502
  }
406
503
  const senderId = this._extractSenderIdFromInboxItem(item);
407
- if (senderId && senderId !== this.connectorId) {
504
+ if (senderId && senderId !== this.localNodeId) {
408
505
  logger.debug('broadcast_channel_duplicate_ack_bypass_non_self', {
409
506
  channel: this.channelName,
410
507
  connector_id: this.connectorId,
@@ -500,6 +597,34 @@ class BroadcastChannelConnector extends base_async_connector_js_1.BaseAsyncConne
500
597
  });
501
598
  }
502
599
  }
600
+ setTargetNodeId(nodeId) {
601
+ const normalized = BroadcastChannelConnector.normalizeNodeId(nodeId);
602
+ if (!normalized) {
603
+ throw new Error('BroadcastChannelConnector target node id must be a non-empty string');
604
+ }
605
+ if (normalized === '*') {
606
+ this.setWildcardTarget();
607
+ return;
608
+ }
609
+ this.targetNodeId = normalized;
610
+ logger.debug('broadcast_channel_target_updated', {
611
+ channel: this.channelName,
612
+ connector_id: this.connectorId,
613
+ local_node_id: this.localNodeId,
614
+ target_node_id: this.targetNodeId,
615
+ target_mode: 'direct',
616
+ });
617
+ }
618
+ setWildcardTarget() {
619
+ this.targetNodeId = '*';
620
+ logger.debug('broadcast_channel_target_updated', {
621
+ channel: this.channelName,
622
+ connector_id: this.connectorId,
623
+ local_node_id: this.localNodeId,
624
+ target_node_id: this.targetNodeId,
625
+ target_mode: 'wildcard',
626
+ });
627
+ }
503
628
  _trimSeenAcks(now) {
504
629
  while (this.seenAckOrder.length > 0) {
505
630
  const candidate = this.seenAckOrder[0];
@@ -328,7 +328,7 @@ class BroadcastChannelListener extends transport_listener_js_1.TransportListener
328
328
  node: routingNode,
329
329
  });
330
330
  const selection = grant_selection_policy_js_1.defaultGrantSelectionPolicy.selectCallbackGrant(selectionContext);
331
- connectorConfig = this._grantToConnectorConfig(selection.grant);
331
+ connectorConfig = this._grantToConnectorConfig(selection.grant, systemId);
332
332
  }
333
333
  catch (error) {
334
334
  logger.debug('broadcast_channel_listener_grant_selection_failed', {
@@ -337,13 +337,20 @@ class BroadcastChannelListener extends transport_listener_js_1.TransportListener
337
337
  error: error instanceof Error ? error.message : String(error),
338
338
  });
339
339
  connectorConfig =
340
- this._extractBroadcastConnectorConfig(frame) ??
341
- {
340
+ this._extractBroadcastConnectorConfig(frame, systemId) ??
341
+ this._buildConnectorConfigForSystem(systemId, {
342
342
  type: broadcast_channel_connector_js_1.BROADCAST_CHANNEL_CONNECTOR_TYPE,
343
343
  channelName: this._channelName,
344
344
  inboxCapacity: this._inboxCapacity,
345
345
  passive: true,
346
- };
346
+ });
347
+ }
348
+ if (!connectorConfig) {
349
+ logger.error('broadcast_channel_listener_missing_connector_config', {
350
+ sender_id: params.senderId,
351
+ system_id: systemId,
352
+ });
353
+ return null;
347
354
  }
348
355
  try {
349
356
  const connector = await routingNode.createOriginConnector({
@@ -369,7 +376,7 @@ class BroadcastChannelListener extends transport_listener_js_1.TransportListener
369
376
  return null;
370
377
  }
371
378
  }
372
- _extractBroadcastConnectorConfig(frame) {
379
+ _extractBroadcastConnectorConfig(frame, systemId) {
373
380
  const rawGrants = frame.callbackGrants;
374
381
  if (!Array.isArray(rawGrants)) {
375
382
  return null;
@@ -380,7 +387,10 @@ class BroadcastChannelListener extends transport_listener_js_1.TransportListener
380
387
  (grant.type === broadcast_channel_connection_grant_js_1.BROADCAST_CHANNEL_CONNECTION_GRANT_TYPE ||
381
388
  grant.type === broadcast_channel_connector_js_1.BROADCAST_CHANNEL_CONNECTOR_TYPE)) {
382
389
  try {
383
- return this._grantToConnectorConfig(grant);
390
+ if (grant.type === broadcast_channel_connector_js_1.BROADCAST_CHANNEL_CONNECTOR_TYPE) {
391
+ return this._buildConnectorConfigForSystem(systemId, grant);
392
+ }
393
+ return this._buildConnectorConfigForSystem(systemId, (0, broadcast_channel_connection_grant_js_1.broadcastChannelGrantToConnectorConfig)(grant));
384
394
  }
385
395
  catch (error) {
386
396
  logger.debug('broadcast_channel_listener_grant_normalization_failed', {
@@ -391,31 +401,87 @@ class BroadcastChannelListener extends transport_listener_js_1.TransportListener
391
401
  }
392
402
  return null;
393
403
  }
394
- _grantToConnectorConfig(grant) {
395
- if (grant.type !== broadcast_channel_connector_js_1.BROADCAST_CHANNEL_CONNECTOR_TYPE) {
396
- if (grant.type === broadcast_channel_connection_grant_js_1.BROADCAST_CHANNEL_CONNECTION_GRANT_TYPE) {
397
- return (0, broadcast_channel_connection_grant_js_1.broadcastChannelGrantToConnectorConfig)(grant);
404
+ _grantToConnectorConfig(grant, systemId) {
405
+ if (grant.type === broadcast_channel_connector_js_1.BROADCAST_CHANNEL_CONNECTOR_TYPE) {
406
+ return this._buildConnectorConfigForSystem(systemId, grant);
407
+ }
408
+ if (grant.type === broadcast_channel_connection_grant_js_1.BROADCAST_CHANNEL_CONNECTION_GRANT_TYPE) {
409
+ return this._buildConnectorConfigForSystem(systemId, (0, broadcast_channel_connection_grant_js_1.broadcastChannelGrantToConnectorConfig)(grant));
410
+ }
411
+ if ('toConnectorConfig' in grant &&
412
+ typeof grant.toConnectorConfig ===
413
+ 'function') {
414
+ const normalized = grant.toConnectorConfig();
415
+ if (normalized.type !== broadcast_channel_connector_js_1.BROADCAST_CHANNEL_CONNECTOR_TYPE) {
416
+ throw new Error(`Unsupported grant connector type: ${normalized.type}`);
398
417
  }
399
- throw new Error(`Unsupported grant type: ${grant.type}`);
418
+ return this._buildConnectorConfigForSystem(systemId, normalized);
400
419
  }
401
- const candidate = grant;
402
- const config = {
420
+ throw new Error(`Unsupported grant type: ${grant.type}`);
421
+ }
422
+ _buildConnectorConfigForSystem(systemId, baseConfig) {
423
+ const localNodeId = this._requireLocalNodeId();
424
+ const targetSystemId = this._normalizeNodeId(systemId);
425
+ if (!targetSystemId) {
426
+ throw new Error('BroadcastChannelListener requires a valid system id');
427
+ }
428
+ const candidate = baseConfig ?? null;
429
+ const channelCandidate = candidate && 'channelName' in candidate
430
+ ? candidate.channelName
431
+ : undefined;
432
+ const inboxCandidate = candidate && 'inboxCapacity' in candidate
433
+ ? candidate.inboxCapacity
434
+ : undefined;
435
+ const initialWindowCandidate = candidate && 'initialWindow' in candidate
436
+ ? candidate.initialWindow
437
+ : undefined;
438
+ const passiveCandidate = candidate && 'passive' in candidate
439
+ ? candidate.passive
440
+ : undefined;
441
+ const targetCandidate = candidate && 'initialTargetNodeId' in candidate
442
+ ? candidate.initialTargetNodeId
443
+ : undefined;
444
+ const channelName = typeof channelCandidate === 'string' && channelCandidate.trim().length > 0
445
+ ? channelCandidate.trim()
446
+ : this._channelName;
447
+ const inboxCapacity = typeof inboxCandidate === 'number' &&
448
+ Number.isFinite(inboxCandidate) &&
449
+ inboxCandidate > 0
450
+ ? Math.floor(inboxCandidate)
451
+ : this._inboxCapacity;
452
+ const initialWindow = typeof initialWindowCandidate === 'number' &&
453
+ Number.isFinite(initialWindowCandidate) &&
454
+ initialWindowCandidate > 0
455
+ ? Math.floor(initialWindowCandidate)
456
+ : undefined;
457
+ const initialTargetNodeId = this._normalizeNodeId(targetCandidate) ?? targetSystemId;
458
+ return {
403
459
  type: broadcast_channel_connector_js_1.BROADCAST_CHANNEL_CONNECTOR_TYPE,
404
- channelName: this._channelName,
405
- inboxCapacity: this._inboxCapacity,
406
- passive: true,
460
+ channelName,
461
+ inboxCapacity,
462
+ passive: typeof passiveCandidate === 'boolean' ? passiveCandidate : true,
463
+ initialWindow,
464
+ localNodeId,
465
+ initialTargetNodeId,
407
466
  };
408
- const channelCandidate = candidate.channelName ?? candidate['channel_name'];
409
- if (typeof channelCandidate === 'string' && channelCandidate.trim().length > 0) {
410
- config.channelName = channelCandidate.trim();
467
+ }
468
+ _requireLocalNodeId() {
469
+ if (!this._routingNode) {
470
+ throw new Error('BroadcastChannelListener requires routing node context');
411
471
  }
412
- const inboxCandidate = candidate.inboxCapacity ?? candidate['inbox_capacity'];
413
- if (typeof inboxCandidate === 'number' &&
414
- Number.isFinite(inboxCandidate) &&
415
- inboxCandidate > 0) {
416
- config.inboxCapacity = Math.floor(inboxCandidate);
472
+ const normalized = this._normalizeNodeId(this._routingNode.sid) ??
473
+ this._normalizeNodeId(this._routingNode.id);
474
+ if (!normalized) {
475
+ throw new Error('BroadcastChannelListener requires routing node with a stable identifier');
476
+ }
477
+ return normalized;
478
+ }
479
+ _normalizeNodeId(value) {
480
+ if (typeof value !== 'string') {
481
+ return null;
417
482
  }
418
- return config;
483
+ const trimmed = value.trim();
484
+ return trimmed.length > 0 ? trimmed : null;
419
485
  }
420
486
  _monitorConnectorLifecycle(senderId, systemId, connector) {
421
487
  const maybeClosable = connector;
@@ -1,10 +1,10 @@
1
1
  "use strict";
2
2
  // This file is auto-generated during build - do not edit manually
3
- // Generated from package.json version: 0.3.5-test.961
3
+ // Generated from package.json version: 0.3.5-test.962
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
5
  exports.VERSION = void 0;
6
6
  /**
7
7
  * The package version, injected at build time.
8
8
  * @internal
9
9
  */
10
- exports.VERSION = '0.3.5-test.961';
10
+ exports.VERSION = '0.3.5-test.962';
@@ -72,8 +72,13 @@ export class BroadcastChannelConnectorFactory extends ConnectorFactory {
72
72
  }
73
73
  const normalized = this._normalizeConfig(config);
74
74
  const options = (factoryArgs[0] ?? {});
75
+ const localNodeId = this._normalizeNodeId(options.localNodeId);
76
+ if (!localNodeId) {
77
+ throw new Error('BroadcastChannelConnectorFactory requires a localNodeId in create() options');
78
+ }
75
79
  const channelName = normalized.channelName ?? DEFAULT_CHANNEL;
76
80
  const inboxCapacity = normalized.inboxCapacity ?? DEFAULT_INBOX_CAPACITY;
81
+ const resolvedTarget = this._normalizeTargetNodeId(options.initialTargetNodeId ?? normalized.initialTargetNodeId);
77
82
  const baseConfig = {
78
83
  drainTimeout: normalized.drainTimeout,
79
84
  flowControl: normalized.flowControl,
@@ -88,6 +93,8 @@ export class BroadcastChannelConnectorFactory extends ConnectorFactory {
88
93
  type: BROADCAST_CHANNEL_CONNECTOR_TYPE,
89
94
  channelName,
90
95
  inboxCapacity,
96
+ localNodeId,
97
+ initialTargetNodeId: resolvedTarget,
91
98
  };
92
99
  const connector = new BroadcastChannelConnector(connectorConfig, baseConfig);
93
100
  if (options.authorization) {
@@ -111,6 +118,13 @@ export class BroadcastChannelConnectorFactory extends ConnectorFactory {
111
118
  normalized.channelName = channel.trim();
112
119
  }
113
120
  const capacity = candidate.inboxCapacity ?? candidate['inbox_capacity'];
121
+ const initialTargetNodeId = candidate.initialTargetNodeId ?? candidate['initial_target_node_id'];
122
+ if (typeof initialTargetNodeId === 'string' && initialTargetNodeId.trim().length > 0) {
123
+ normalized.initialTargetNodeId = initialTargetNodeId.trim();
124
+ }
125
+ else if (initialTargetNodeId === '*') {
126
+ normalized.initialTargetNodeId = '*';
127
+ }
114
128
  if (typeof capacity === 'number' &&
115
129
  Number.isFinite(capacity) &&
116
130
  capacity > 0) {
@@ -154,5 +168,18 @@ export class BroadcastChannelConnectorFactory extends ConnectorFactory {
154
168
  normalized.inboxCapacity ?? DEFAULT_INBOX_CAPACITY;
155
169
  return normalized;
156
170
  }
171
+ _normalizeNodeId(value) {
172
+ if (typeof value !== 'string') {
173
+ return null;
174
+ }
175
+ const trimmed = value.trim();
176
+ return trimmed.length > 0 ? trimmed : null;
177
+ }
178
+ _normalizeTargetNodeId(value) {
179
+ if (value === '*') {
180
+ return '*';
181
+ }
182
+ return this._normalizeNodeId(value) ?? '*';
183
+ }
157
184
  }
158
185
  export default BroadcastChannelConnectorFactory;
@@ -46,6 +46,26 @@ export class BroadcastChannelConnector extends BaseAsyncConnector {
46
46
  }
47
47
  return null;
48
48
  }
49
+ static normalizeNodeId(value) {
50
+ if (typeof value !== 'string') {
51
+ return null;
52
+ }
53
+ const trimmed = value.trim();
54
+ return trimmed.length > 0 ? trimmed : null;
55
+ }
56
+ static normalizeTargetNodeId(value) {
57
+ if (typeof value !== 'string') {
58
+ return undefined;
59
+ }
60
+ const trimmed = value.trim();
61
+ if (trimmed.length === 0) {
62
+ return undefined;
63
+ }
64
+ if (trimmed === '*') {
65
+ return '*';
66
+ }
67
+ return trimmed;
68
+ }
49
69
  constructor(config, baseConfig = {}) {
50
70
  ensureBroadcastEnvironment();
51
71
  super(baseConfig);
@@ -68,10 +88,18 @@ export class BroadcastChannelConnector extends BaseAsyncConnector {
68
88
  this.inbox = new BoundedAsyncQueue(preferredCapacity);
69
89
  this.inboxCapacity = preferredCapacity;
70
90
  this.connectorId = BroadcastChannelConnector.generateConnectorId();
91
+ const normalizedLocalNodeId = BroadcastChannelConnector.normalizeNodeId(config.localNodeId);
92
+ if (!normalizedLocalNodeId) {
93
+ throw new Error('BroadcastChannelConnector requires a non-empty localNodeId');
94
+ }
95
+ this.localNodeId = normalizedLocalNodeId;
96
+ this.targetNodeId = BroadcastChannelConnector.normalizeTargetNodeId(config.initialTargetNodeId);
71
97
  this.channel = new BroadcastChannel(this.channelName);
72
98
  logger.debug('broadcast_channel_connector_created', {
73
99
  channel: this.channelName,
74
100
  connector_id: this.connectorId,
101
+ local_node_id: this.localNodeId,
102
+ target_node_id: this.targetNodeId ?? null,
75
103
  inbox_capacity: preferredCapacity,
76
104
  timestamp: new Date().toISOString(),
77
105
  });
@@ -93,15 +121,32 @@ export class BroadcastChannelConnector extends BaseAsyncConnector {
93
121
  ? message.constructor?.name ?? typeof message
94
122
  : typeof message,
95
123
  has_sender_id: Boolean(message?.senderId),
124
+ has_sender_node_id: Boolean(message?.senderNodeId),
96
125
  });
97
126
  if (!message || typeof message !== 'object') {
98
127
  return;
99
128
  }
100
129
  const busMessage = message;
101
- if (typeof busMessage.senderId !== 'string' || busMessage.senderId.length === 0) {
130
+ const senderNodeId = BroadcastChannelConnector.normalizeNodeId(busMessage.senderNodeId);
131
+ if (!senderNodeId) {
132
+ logger.debug('broadcast_channel_message_rejected', {
133
+ channel: this.channelName,
134
+ connector_id: this.connectorId,
135
+ reason: 'missing_sender_node_id',
136
+ });
137
+ return;
138
+ }
139
+ if (senderNodeId === this.localNodeId) {
140
+ logger.debug('broadcast_channel_message_rejected', {
141
+ channel: this.channelName,
142
+ connector_id: this.connectorId,
143
+ reason: 'self_echo',
144
+ sender_node_id: senderNodeId,
145
+ });
102
146
  return;
103
147
  }
104
- if (busMessage.senderId === this.connectorId) {
148
+ const incomingTargetNodeId = BroadcastChannelConnector.normalizeTargetNodeId(busMessage.targetNodeId);
149
+ if (!this._shouldAcceptMessageFromBus(senderNodeId, incomingTargetNodeId)) {
105
150
  return;
106
151
  }
107
152
  const payload = BroadcastChannelConnector.coercePayload(busMessage.payload);
@@ -115,11 +160,13 @@ export class BroadcastChannelConnector extends BaseAsyncConnector {
115
160
  }
116
161
  logger.debug('broadcast_channel_message_received', {
117
162
  channel: this.channelName,
118
- sender_id: busMessage.senderId,
163
+ sender_id: message?.senderId,
164
+ sender_node_id: senderNodeId,
165
+ target_node_id: incomingTargetNodeId ?? null,
119
166
  connector_id: this.connectorId,
120
167
  payload_length: payload.byteLength,
121
168
  });
122
- if (this._shouldSkipDuplicateAck(busMessage.senderId, payload)) {
169
+ if (this._shouldSkipDuplicateAck(senderNodeId, payload)) {
123
170
  return;
124
171
  }
125
172
  try {
@@ -263,12 +310,17 @@ export class BroadcastChannelConnector extends BaseAsyncConnector {
263
310
  }
264
311
  async _transportSendBytes(data) {
265
312
  ensureBroadcastEnvironment();
313
+ const targetNodeId = this.targetNodeId ?? '*';
266
314
  logger.debug('broadcast_channel_message_sending', {
267
315
  channel: this.channelName,
268
316
  sender_id: this.connectorId,
317
+ sender_node_id: this.localNodeId,
318
+ target_node_id: targetNodeId,
269
319
  });
270
320
  this.channel.postMessage({
271
321
  senderId: this.connectorId,
322
+ senderNodeId: this.localNodeId,
323
+ targetNodeId,
272
324
  payload: data,
273
325
  });
274
326
  }
@@ -331,6 +383,51 @@ export class BroadcastChannelConnector extends BaseAsyncConnector {
331
383
  }
332
384
  return rawOrEnvelope;
333
385
  }
386
+ _isWildcardTarget() {
387
+ return this.targetNodeId === '*' || typeof this.targetNodeId === 'undefined';
388
+ }
389
+ _shouldAcceptMessageFromBus(senderNodeId, targetNodeId) {
390
+ if (this._isWildcardTarget()) {
391
+ if (targetNodeId && targetNodeId !== '*') {
392
+ logger.debug('broadcast_channel_message_rejected', {
393
+ channel: this.channelName,
394
+ connector_id: this.connectorId,
395
+ reason: 'wildcard_target_mismatch',
396
+ sender_node_id: senderNodeId,
397
+ target_node_id: targetNodeId,
398
+ local_node_id: this.localNodeId,
399
+ });
400
+ return false;
401
+ }
402
+ return true;
403
+ }
404
+ const expectedSender = this.targetNodeId;
405
+ if (expectedSender && expectedSender !== '*' && senderNodeId !== expectedSender) {
406
+ logger.debug('broadcast_channel_message_rejected', {
407
+ channel: this.channelName,
408
+ connector_id: this.connectorId,
409
+ reason: 'unexpected_sender',
410
+ expected_sender_node_id: expectedSender,
411
+ sender_node_id: senderNodeId,
412
+ local_node_id: this.localNodeId,
413
+ });
414
+ return false;
415
+ }
416
+ if (targetNodeId &&
417
+ targetNodeId !== '*' &&
418
+ targetNodeId !== this.localNodeId) {
419
+ logger.debug('broadcast_channel_message_rejected', {
420
+ channel: this.channelName,
421
+ connector_id: this.connectorId,
422
+ reason: 'unexpected_target',
423
+ sender_node_id: senderNodeId,
424
+ target_node_id: targetNodeId,
425
+ local_node_id: this.localNodeId,
426
+ });
427
+ return false;
428
+ }
429
+ return true;
430
+ }
334
431
  _describeInboxItem(item) {
335
432
  if (item instanceof Uint8Array) {
336
433
  return 'bytes';
@@ -361,7 +458,7 @@ export class BroadcastChannelConnector extends BaseAsyncConnector {
361
458
  const normalizedSenderId = typeof senderId === 'string' && senderId.length > 0
362
459
  ? senderId
363
460
  : undefined;
364
- if (normalizedSenderId && normalizedSenderId !== this.connectorId) {
461
+ if (normalizedSenderId && normalizedSenderId !== this.localNodeId) {
365
462
  logger.debug('broadcast_channel_duplicate_ack_bypass_non_self', {
366
463
  channel: this.channelName,
367
464
  connector_id: this.connectorId,
@@ -401,7 +498,7 @@ export class BroadcastChannelConnector extends BaseAsyncConnector {
401
498
  return false;
402
499
  }
403
500
  const senderId = this._extractSenderIdFromInboxItem(item);
404
- if (senderId && senderId !== this.connectorId) {
501
+ if (senderId && senderId !== this.localNodeId) {
405
502
  logger.debug('broadcast_channel_duplicate_ack_bypass_non_self', {
406
503
  channel: this.channelName,
407
504
  connector_id: this.connectorId,
@@ -497,6 +594,34 @@ export class BroadcastChannelConnector extends BaseAsyncConnector {
497
594
  });
498
595
  }
499
596
  }
597
+ setTargetNodeId(nodeId) {
598
+ const normalized = BroadcastChannelConnector.normalizeNodeId(nodeId);
599
+ if (!normalized) {
600
+ throw new Error('BroadcastChannelConnector target node id must be a non-empty string');
601
+ }
602
+ if (normalized === '*') {
603
+ this.setWildcardTarget();
604
+ return;
605
+ }
606
+ this.targetNodeId = normalized;
607
+ logger.debug('broadcast_channel_target_updated', {
608
+ channel: this.channelName,
609
+ connector_id: this.connectorId,
610
+ local_node_id: this.localNodeId,
611
+ target_node_id: this.targetNodeId,
612
+ target_mode: 'direct',
613
+ });
614
+ }
615
+ setWildcardTarget() {
616
+ this.targetNodeId = '*';
617
+ logger.debug('broadcast_channel_target_updated', {
618
+ channel: this.channelName,
619
+ connector_id: this.connectorId,
620
+ local_node_id: this.localNodeId,
621
+ target_node_id: this.targetNodeId,
622
+ target_mode: 'wildcard',
623
+ });
624
+ }
500
625
  _trimSeenAcks(now) {
501
626
  while (this.seenAckOrder.length > 0) {
502
627
  const candidate = this.seenAckOrder[0];