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

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.
Files changed (24) hide show
  1. package/dist/browser/index.cjs +290 -33
  2. package/dist/browser/index.mjs +290 -33
  3. package/dist/cjs/naylence/fame/connector/broadcast-channel-connector-factory.js +36 -0
  4. package/dist/cjs/naylence/fame/connector/broadcast-channel-connector.browser.js +131 -6
  5. package/dist/cjs/naylence/fame/connector/broadcast-channel-listener.js +91 -25
  6. package/dist/cjs/naylence/fame/node/upstream-session-manager.js +30 -0
  7. package/dist/cjs/version.js +2 -2
  8. package/dist/esm/naylence/fame/connector/broadcast-channel-connector-factory.js +36 -0
  9. package/dist/esm/naylence/fame/connector/broadcast-channel-connector.browser.js +131 -6
  10. package/dist/esm/naylence/fame/connector/broadcast-channel-listener.js +91 -25
  11. package/dist/esm/naylence/fame/node/upstream-session-manager.js +30 -0
  12. package/dist/esm/version.js +2 -2
  13. package/dist/node/index.cjs +290 -33
  14. package/dist/node/index.mjs +290 -33
  15. package/dist/node/node.cjs +290 -33
  16. package/dist/node/node.mjs +290 -33
  17. package/dist/types/naylence/fame/connector/broadcast-channel-connector-factory.d.ts +6 -0
  18. package/dist/types/naylence/fame/connector/broadcast-channel-connector.browser.d.ts +10 -0
  19. package/dist/types/naylence/fame/connector/broadcast-channel-connector.node.d.ts +1 -6
  20. package/dist/types/naylence/fame/connector/broadcast-channel-listener.d.ts +3 -0
  21. package/dist/types/naylence/fame/grants/broadcast-channel-connection-grant.d.ts +1 -1
  22. package/dist/types/naylence/fame/node/upstream-session-manager.d.ts +2 -0
  23. package/dist/types/version.d.ts +1 -1
  24. package/package.json +1 -1
@@ -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];
@@ -324,7 +324,7 @@ export class BroadcastChannelListener extends TransportListener {
324
324
  node: routingNode,
325
325
  });
326
326
  const selection = defaultGrantSelectionPolicy.selectCallbackGrant(selectionContext);
327
- connectorConfig = this._grantToConnectorConfig(selection.grant);
327
+ connectorConfig = this._grantToConnectorConfig(selection.grant, systemId);
328
328
  }
329
329
  catch (error) {
330
330
  logger.debug('broadcast_channel_listener_grant_selection_failed', {
@@ -333,13 +333,20 @@ export class BroadcastChannelListener extends TransportListener {
333
333
  error: error instanceof Error ? error.message : String(error),
334
334
  });
335
335
  connectorConfig =
336
- this._extractBroadcastConnectorConfig(frame) ??
337
- {
336
+ this._extractBroadcastConnectorConfig(frame, systemId) ??
337
+ this._buildConnectorConfigForSystem(systemId, {
338
338
  type: BROADCAST_CHANNEL_CONNECTOR_TYPE,
339
339
  channelName: this._channelName,
340
340
  inboxCapacity: this._inboxCapacity,
341
341
  passive: true,
342
- };
342
+ });
343
+ }
344
+ if (!connectorConfig) {
345
+ logger.error('broadcast_channel_listener_missing_connector_config', {
346
+ sender_id: params.senderId,
347
+ system_id: systemId,
348
+ });
349
+ return null;
343
350
  }
344
351
  try {
345
352
  const connector = await routingNode.createOriginConnector({
@@ -365,7 +372,7 @@ export class BroadcastChannelListener extends TransportListener {
365
372
  return null;
366
373
  }
367
374
  }
368
- _extractBroadcastConnectorConfig(frame) {
375
+ _extractBroadcastConnectorConfig(frame, systemId) {
369
376
  const rawGrants = frame.callbackGrants;
370
377
  if (!Array.isArray(rawGrants)) {
371
378
  return null;
@@ -376,7 +383,10 @@ export class BroadcastChannelListener extends TransportListener {
376
383
  (grant.type === BROADCAST_CHANNEL_CONNECTION_GRANT_TYPE ||
377
384
  grant.type === BROADCAST_CHANNEL_CONNECTOR_TYPE)) {
378
385
  try {
379
- return this._grantToConnectorConfig(grant);
386
+ if (grant.type === BROADCAST_CHANNEL_CONNECTOR_TYPE) {
387
+ return this._buildConnectorConfigForSystem(systemId, grant);
388
+ }
389
+ return this._buildConnectorConfigForSystem(systemId, broadcastChannelGrantToConnectorConfig(grant));
380
390
  }
381
391
  catch (error) {
382
392
  logger.debug('broadcast_channel_listener_grant_normalization_failed', {
@@ -387,31 +397,87 @@ export class BroadcastChannelListener extends TransportListener {
387
397
  }
388
398
  return null;
389
399
  }
390
- _grantToConnectorConfig(grant) {
391
- if (grant.type !== BROADCAST_CHANNEL_CONNECTOR_TYPE) {
392
- if (grant.type === BROADCAST_CHANNEL_CONNECTION_GRANT_TYPE) {
393
- return broadcastChannelGrantToConnectorConfig(grant);
400
+ _grantToConnectorConfig(grant, systemId) {
401
+ if (grant.type === BROADCAST_CHANNEL_CONNECTOR_TYPE) {
402
+ return this._buildConnectorConfigForSystem(systemId, grant);
403
+ }
404
+ if (grant.type === BROADCAST_CHANNEL_CONNECTION_GRANT_TYPE) {
405
+ return this._buildConnectorConfigForSystem(systemId, broadcastChannelGrantToConnectorConfig(grant));
406
+ }
407
+ if ('toConnectorConfig' in grant &&
408
+ typeof grant.toConnectorConfig ===
409
+ 'function') {
410
+ const normalized = grant.toConnectorConfig();
411
+ if (normalized.type !== BROADCAST_CHANNEL_CONNECTOR_TYPE) {
412
+ throw new Error(`Unsupported grant connector type: ${normalized.type}`);
394
413
  }
395
- throw new Error(`Unsupported grant type: ${grant.type}`);
414
+ return this._buildConnectorConfigForSystem(systemId, normalized);
396
415
  }
397
- const candidate = grant;
398
- const config = {
416
+ throw new Error(`Unsupported grant type: ${grant.type}`);
417
+ }
418
+ _buildConnectorConfigForSystem(systemId, baseConfig) {
419
+ const localNodeId = this._requireLocalNodeId();
420
+ const targetSystemId = this._normalizeNodeId(systemId);
421
+ if (!targetSystemId) {
422
+ throw new Error('BroadcastChannelListener requires a valid system id');
423
+ }
424
+ const candidate = baseConfig ?? null;
425
+ const channelCandidate = candidate && 'channelName' in candidate
426
+ ? candidate.channelName
427
+ : undefined;
428
+ const inboxCandidate = candidate && 'inboxCapacity' in candidate
429
+ ? candidate.inboxCapacity
430
+ : undefined;
431
+ const initialWindowCandidate = candidate && 'initialWindow' in candidate
432
+ ? candidate.initialWindow
433
+ : undefined;
434
+ const passiveCandidate = candidate && 'passive' in candidate
435
+ ? candidate.passive
436
+ : undefined;
437
+ const targetCandidate = candidate && 'initialTargetNodeId' in candidate
438
+ ? candidate.initialTargetNodeId
439
+ : undefined;
440
+ const channelName = typeof channelCandidate === 'string' && channelCandidate.trim().length > 0
441
+ ? channelCandidate.trim()
442
+ : this._channelName;
443
+ const inboxCapacity = typeof inboxCandidate === 'number' &&
444
+ Number.isFinite(inboxCandidate) &&
445
+ inboxCandidate > 0
446
+ ? Math.floor(inboxCandidate)
447
+ : this._inboxCapacity;
448
+ const initialWindow = typeof initialWindowCandidate === 'number' &&
449
+ Number.isFinite(initialWindowCandidate) &&
450
+ initialWindowCandidate > 0
451
+ ? Math.floor(initialWindowCandidate)
452
+ : undefined;
453
+ const initialTargetNodeId = this._normalizeNodeId(targetCandidate) ?? targetSystemId;
454
+ return {
399
455
  type: BROADCAST_CHANNEL_CONNECTOR_TYPE,
400
- channelName: this._channelName,
401
- inboxCapacity: this._inboxCapacity,
402
- passive: true,
456
+ channelName,
457
+ inboxCapacity,
458
+ passive: typeof passiveCandidate === 'boolean' ? passiveCandidate : true,
459
+ initialWindow,
460
+ localNodeId,
461
+ initialTargetNodeId,
403
462
  };
404
- const channelCandidate = candidate.channelName ?? candidate['channel_name'];
405
- if (typeof channelCandidate === 'string' && channelCandidate.trim().length > 0) {
406
- config.channelName = channelCandidate.trim();
463
+ }
464
+ _requireLocalNodeId() {
465
+ if (!this._routingNode) {
466
+ throw new Error('BroadcastChannelListener requires routing node context');
407
467
  }
408
- const inboxCandidate = candidate.inboxCapacity ?? candidate['inbox_capacity'];
409
- if (typeof inboxCandidate === 'number' &&
410
- Number.isFinite(inboxCandidate) &&
411
- inboxCandidate > 0) {
412
- config.inboxCapacity = Math.floor(inboxCandidate);
468
+ const normalized = this._normalizeNodeId(this._routingNode.sid) ??
469
+ this._normalizeNodeId(this._routingNode.id);
470
+ if (!normalized) {
471
+ throw new Error('BroadcastChannelListener requires routing node with a stable identifier');
472
+ }
473
+ return normalized;
474
+ }
475
+ _normalizeNodeId(value) {
476
+ if (typeof value !== 'string') {
477
+ return null;
413
478
  }
414
- return config;
479
+ const trimmed = value.trim();
480
+ return trimmed.length > 0 ? trimmed : null;
415
481
  }
416
482
  _monitorConnectorLifecycle(senderId, systemId, connector) {
417
483
  const maybeClosable = connector;
@@ -277,6 +277,20 @@ export class UpstreamSessionManager extends TaskSpawner {
277
277
  waitEvent(event, signal) {
278
278
  return signal ? event.wait({ signal }) : event.wait();
279
279
  }
280
+ _getLocalNodeId() {
281
+ const normalized = this._normalizeNodeId(this.node.id);
282
+ if (!normalized) {
283
+ throw new Error('UpstreamSessionManager requires node with a stable identifier');
284
+ }
285
+ return normalized;
286
+ }
287
+ _normalizeNodeId(value) {
288
+ if (typeof value !== 'string') {
289
+ return null;
290
+ }
291
+ const trimmed = value.trim();
292
+ return trimmed.length > 0 ? trimmed : null;
293
+ }
280
294
  async connectCycle() {
281
295
  if (!this.admissionClient) {
282
296
  throw new FameConnectError('Admission client is required to attach upstream');
@@ -298,6 +312,8 @@ export class UpstreamSessionManager extends TaskSpawner {
298
312
  await this.onWelcome(welcome.frame);
299
313
  const connector = await ConnectorFactory.createConnector(grant, {
300
314
  systemId: welcome.frame.systemId,
315
+ localNodeId: this._getLocalNodeId(),
316
+ initialTargetNodeId: '*',
301
317
  });
302
318
  await connector.start(this.wrappedHandler);
303
319
  this.connector = connector;
@@ -323,6 +339,20 @@ export class UpstreamSessionManager extends TaskSpawner {
323
339
  }
324
340
  const attachInfo = await this.attachClient.attach(this.node, this.outboundOriginType, connector, welcome.frame, this.wrappedHandler, this.getKeys() ?? undefined, callbackGrants);
325
341
  this.targetSystemId = attachInfo.targetSystemId ?? null;
342
+ if (this.targetSystemId) {
343
+ const targetAware = connector;
344
+ if (typeof targetAware.setTargetNodeId === 'function') {
345
+ try {
346
+ targetAware.setTargetNodeId(this.targetSystemId);
347
+ }
348
+ catch (error) {
349
+ logger.warning('broadcast_channel_target_apply_failed', {
350
+ error: error instanceof Error ? error.message : String(error),
351
+ target_node_id: this.targetSystemId,
352
+ });
353
+ }
354
+ }
355
+ }
326
356
  await this.onAttach(attachInfo, connector);
327
357
  // Close the admission client immediately after attach completes
328
358
  // This releases HTTP keep-alive connections (Node.js fetch/undici requires explicit cleanup)
@@ -1,7 +1,7 @@
1
1
  // This file is auto-generated during build - do not edit manually
2
- // Generated from package.json version: 0.3.5-test.961
2
+ // Generated from package.json version: 0.3.5-test.963
3
3
  /**
4
4
  * The package version, injected at build time.
5
5
  * @internal
6
6
  */
7
- export const VERSION = '0.3.5-test.961';
7
+ export const VERSION = '0.3.5-test.963';