@naylence/runtime 0.3.5-test.942 → 0.3.5-test.943

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 (31) hide show
  1. package/dist/browser/index.cjs +296 -6
  2. package/dist/browser/index.mjs +296 -6
  3. package/dist/cjs/naylence/fame/connector/broadcast-channel-connector-factory.js +12 -0
  4. package/dist/cjs/naylence/fame/connector/broadcast-channel-connector.browser.js +69 -1
  5. package/dist/cjs/naylence/fame/connector/inpage-connector-factory.js +12 -0
  6. package/dist/cjs/naylence/fame/connector/inpage-connector.js +66 -1
  7. package/dist/cjs/naylence/fame/connector/transport-frame.js +101 -0
  8. package/dist/cjs/naylence/fame/grants/broadcast-channel-connection-grant.js +28 -0
  9. package/dist/cjs/naylence/fame/grants/inpage-connection-grant.js +28 -0
  10. package/dist/cjs/version.js +2 -2
  11. package/dist/esm/naylence/fame/connector/broadcast-channel-connector-factory.js +12 -0
  12. package/dist/esm/naylence/fame/connector/broadcast-channel-connector.browser.js +69 -1
  13. package/dist/esm/naylence/fame/connector/inpage-connector-factory.js +12 -0
  14. package/dist/esm/naylence/fame/connector/inpage-connector.js +66 -1
  15. package/dist/esm/naylence/fame/connector/transport-frame.js +94 -0
  16. package/dist/esm/naylence/fame/grants/broadcast-channel-connection-grant.js +28 -0
  17. package/dist/esm/naylence/fame/grants/inpage-connection-grant.js +28 -0
  18. package/dist/esm/version.js +2 -2
  19. package/dist/node/index.cjs +296 -6
  20. package/dist/node/index.mjs +296 -6
  21. package/dist/node/node.cjs +312 -6
  22. package/dist/node/node.mjs +312 -6
  23. package/dist/types/naylence/fame/connector/broadcast-channel-connector-factory.d.ts +2 -0
  24. package/dist/types/naylence/fame/connector/broadcast-channel-connector.browser.d.ts +4 -0
  25. package/dist/types/naylence/fame/connector/inpage-connector-factory.d.ts +2 -0
  26. package/dist/types/naylence/fame/connector/inpage-connector.d.ts +4 -0
  27. package/dist/types/naylence/fame/connector/transport-frame.d.ts +58 -0
  28. package/dist/types/naylence/fame/grants/broadcast-channel-connection-grant.d.ts +6 -0
  29. package/dist/types/naylence/fame/grants/inpage-connection-grant.d.ts +8 -0
  30. package/dist/types/version.d.ts +1 -1
  31. package/package.json +1 -1
@@ -10,6 +10,7 @@ const errors_js_1 = require("../errors/errors.js");
10
10
  const logging_js_1 = require("../util/logging.js");
11
11
  const bounded_async_queue_js_1 = require("../util/bounded-async-queue.js");
12
12
  const core_1 = require("@naylence/core");
13
+ const transport_frame_js_1 = require("./transport-frame.js");
13
14
  const logger = (0, logging_js_1.getLogger)('naylence.fame.connector.inpage_connector');
14
15
  exports.INPAGE_CONNECTOR_TYPE = 'inpage-connector';
15
16
  const DEFAULT_CHANNEL = 'naylence-fabric';
@@ -80,9 +81,20 @@ class InPageConnector extends base_async_connector_js_1.BaseAsyncConnector {
80
81
  : DEFAULT_INBOX_CAPACITY;
81
82
  this.inbox = new bounded_async_queue_js_1.BoundedAsyncQueue(preferredCapacity);
82
83
  this.connectorId = InPageConnector.generateConnectorId();
84
+ // Set local and remote node IDs (defaults to connector ID for backwards compatibility)
85
+ this.localNodeId =
86
+ typeof config.localNodeId === 'string' && config.localNodeId.trim().length > 0
87
+ ? config.localNodeId.trim()
88
+ : this.connectorId;
89
+ this.remoteNodeId =
90
+ typeof config.remoteNodeId === 'string' && config.remoteNodeId.trim().length > 0
91
+ ? config.remoteNodeId.trim()
92
+ : '*'; // Accept from any remote if not specified
83
93
  logger.debug('inpage_connector_initialized', {
84
94
  channel: this.channelName,
85
95
  connector_id: this.connectorId,
96
+ local_node_id: this.localNodeId,
97
+ remote_node_id: this.remoteNodeId,
86
98
  });
87
99
  this.onMsg = (event) => {
88
100
  const messageEvent = event;
@@ -116,6 +128,43 @@ class InPageConnector extends base_async_connector_js_1.BaseAsyncConnector {
116
128
  if (busMessage.senderId === this.connectorId) {
117
129
  return;
118
130
  }
131
+ // Try to unwrap as transport frame
132
+ const unwrapped = (0, transport_frame_js_1.unwrapTransportFrame)(busMessage.payload, this.localNodeId, this.remoteNodeId === '*' ? busMessage.senderId : this.remoteNodeId);
133
+ if (unwrapped) {
134
+ // Successfully unwrapped transport frame
135
+ logger.debug('inpage_transport_frame_received', {
136
+ channel: this.channelName,
137
+ sender_id: busMessage.senderId,
138
+ connector_id: this.connectorId,
139
+ local_node_id: this.localNodeId,
140
+ remote_node_id: this.remoteNodeId,
141
+ payload_length: unwrapped.byteLength,
142
+ });
143
+ try {
144
+ if (typeof this.inbox.tryEnqueue === 'function') {
145
+ const accepted = this.inbox.tryEnqueue(unwrapped);
146
+ if (accepted) {
147
+ return;
148
+ }
149
+ }
150
+ this.inbox.enqueue(unwrapped);
151
+ }
152
+ catch (error) {
153
+ if (error instanceof bounded_async_queue_js_1.QueueFullError) {
154
+ logger.warning('inpage_receive_queue_full', {
155
+ channel: this.channelName,
156
+ });
157
+ }
158
+ else {
159
+ logger.error('inpage_receive_error', {
160
+ channel: this.channelName,
161
+ error: error instanceof Error ? error.message : String(error),
162
+ });
163
+ }
164
+ }
165
+ return;
166
+ }
167
+ // Fall back to legacy format (no transport frame)
119
168
  const payload = InPageConnector.coercePayload(busMessage.payload);
120
169
  if (!payload) {
121
170
  logger.debug('inpage_payload_rejected', {
@@ -274,11 +323,27 @@ class InPageConnector extends base_async_connector_js_1.BaseAsyncConnector {
274
323
  logger.debug('inpage_message_sending', {
275
324
  channel: this.channelName,
276
325
  sender_id: this.connectorId,
326
+ local_node_id: this.localNodeId,
327
+ remote_node_id: this.remoteNodeId,
277
328
  });
329
+ // Only use transport framing if both localNodeId and remoteNodeId are explicitly set
330
+ // (not using default values). This ensures backwards compatibility.
331
+ const useTransportFrame = this.localNodeId !== this.connectorId ||
332
+ this.remoteNodeId !== '*';
333
+ let payload;
334
+ if (useTransportFrame) {
335
+ // Wrap payload in transport frame
336
+ const frame = (0, transport_frame_js_1.wrapTransportFrame)(data, this.localNodeId, this.remoteNodeId);
337
+ payload = (0, transport_frame_js_1.serializeTransportFrame)(frame);
338
+ }
339
+ else {
340
+ // Legacy format: send raw payload
341
+ payload = data;
342
+ }
278
343
  const event = new MessageEvent(this.channelName, {
279
344
  data: {
280
345
  senderId: this.connectorId,
281
- payload: data,
346
+ payload,
282
347
  },
283
348
  });
284
349
  getSharedBus().dispatchEvent(event);
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ /**
3
+ * Transport frame layer for multiplexing logical links on physical channels.
4
+ *
5
+ * This lightweight framing layer wraps raw FAME payloads to enable multiple
6
+ * logical connections over a single physical channel (BroadcastChannel or InPage bus).
7
+ *
8
+ * The transport frame does NOT modify FAME envelopes - it only wraps the raw
9
+ * Uint8Array payload at the connector level.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.TRANSPORT_FRAME_VERSION = void 0;
13
+ exports.wrapTransportFrame = wrapTransportFrame;
14
+ exports.serializeTransportFrame = serializeTransportFrame;
15
+ exports.unwrapTransportFrame = unwrapTransportFrame;
16
+ exports.isTransportFrame = isTransportFrame;
17
+ /**
18
+ * Transport frame version for future compatibility
19
+ */
20
+ exports.TRANSPORT_FRAME_VERSION = 1;
21
+ /**
22
+ * Wrap a raw payload in a transport frame
23
+ *
24
+ * @param payload - Raw FAME envelope bytes
25
+ * @param srcNodeId - Local node ID (this connector)
26
+ * @param dstNodeId - Remote node ID (target connector)
27
+ * @returns Transport frame ready for transmission
28
+ */
29
+ function wrapTransportFrame(payload, srcNodeId, dstNodeId) {
30
+ return {
31
+ v: exports.TRANSPORT_FRAME_VERSION,
32
+ src: srcNodeId,
33
+ dst: dstNodeId,
34
+ payload,
35
+ };
36
+ }
37
+ /**
38
+ * Serialize a transport frame for transmission over the bus
39
+ *
40
+ * @param frame - Transport frame to serialize
41
+ * @returns Serialized frame data ready for postMessage/dispatchEvent
42
+ */
43
+ function serializeTransportFrame(frame) {
44
+ // Convert Uint8Array to regular array for JSON serialization
45
+ const serializable = {
46
+ v: frame.v,
47
+ src: frame.src,
48
+ dst: frame.dst,
49
+ payload: Array.from(frame.payload),
50
+ };
51
+ return serializable;
52
+ }
53
+ /**
54
+ * Unwrap a transport frame, validating source and destination
55
+ *
56
+ * @param raw - Raw data from the bus
57
+ * @param localNodeId - This connector's node ID
58
+ * @param remoteNodeId - Expected remote node ID
59
+ * @returns Unwrapped payload if frame is valid and addressed to us, null otherwise
60
+ */
61
+ function unwrapTransportFrame(raw, localNodeId, remoteNodeId) {
62
+ // Validate basic structure
63
+ if (!raw || typeof raw !== 'object') {
64
+ return null;
65
+ }
66
+ const frame = raw;
67
+ // Check version
68
+ if (frame.v !== exports.TRANSPORT_FRAME_VERSION) {
69
+ return null;
70
+ }
71
+ // Check src and dst
72
+ if (typeof frame.src !== 'string' || typeof frame.dst !== 'string') {
73
+ return null;
74
+ }
75
+ // Only accept frames addressed to us from the expected remote
76
+ if (frame.dst !== localNodeId || frame.src !== remoteNodeId) {
77
+ return null;
78
+ }
79
+ // Extract payload
80
+ if (!frame.payload || !Array.isArray(frame.payload)) {
81
+ return null;
82
+ }
83
+ // Convert array back to Uint8Array
84
+ return Uint8Array.from(frame.payload);
85
+ }
86
+ /**
87
+ * Check if raw data looks like a transport frame
88
+ *
89
+ * @param raw - Raw data from the bus
90
+ * @returns True if this appears to be a transport frame
91
+ */
92
+ function isTransportFrame(raw) {
93
+ if (!raw || typeof raw !== 'object') {
94
+ return false;
95
+ }
96
+ const frame = raw;
97
+ return (typeof frame.v === 'number' &&
98
+ typeof frame.src === 'string' &&
99
+ typeof frame.dst === 'string' &&
100
+ Array.isArray(frame.payload));
101
+ }
@@ -25,6 +25,14 @@ function isBroadcastChannelConnectionGrant(candidate) {
25
25
  record.inboxCapacity <= 0)) {
26
26
  return false;
27
27
  }
28
+ if (record.localNodeId !== undefined &&
29
+ (typeof record.localNodeId !== 'string' || record.localNodeId.length === 0)) {
30
+ return false;
31
+ }
32
+ if (record.remoteNodeId !== undefined &&
33
+ (typeof record.remoteNodeId !== 'string' || record.remoteNodeId.length === 0)) {
34
+ return false;
35
+ }
28
36
  return true;
29
37
  }
30
38
  function normalizeBroadcastChannelConnectionGrant(candidate) {
@@ -58,6 +66,20 @@ function normalizeBroadcastChannelConnectionGrant(candidate) {
58
66
  }
59
67
  result.inboxCapacity = Math.floor(inboxValue);
60
68
  }
69
+ const localNodeIdValue = candidate.localNodeId ?? candidate['local_node_id'];
70
+ if (localNodeIdValue !== undefined) {
71
+ if (typeof localNodeIdValue !== 'string' || localNodeIdValue.trim().length === 0) {
72
+ throw new TypeError('BroadcastChannelConnectionGrant "localNodeId" must be a non-empty string when provided');
73
+ }
74
+ result.localNodeId = localNodeIdValue.trim();
75
+ }
76
+ const remoteNodeIdValue = candidate.remoteNodeId ?? candidate['remote_node_id'];
77
+ if (remoteNodeIdValue !== undefined) {
78
+ if (typeof remoteNodeIdValue !== 'string' || remoteNodeIdValue.trim().length === 0) {
79
+ throw new TypeError('BroadcastChannelConnectionGrant "remoteNodeId" must be a non-empty string when provided');
80
+ }
81
+ result.remoteNodeId = remoteNodeIdValue.trim();
82
+ }
61
83
  return result;
62
84
  }
63
85
  function broadcastChannelGrantToConnectorConfig(grant) {
@@ -71,5 +93,11 @@ function broadcastChannelGrantToConnectorConfig(grant) {
71
93
  if (normalized.inboxCapacity !== undefined) {
72
94
  config.inboxCapacity = normalized.inboxCapacity;
73
95
  }
96
+ if (normalized.localNodeId) {
97
+ config.localNodeId = normalized.localNodeId;
98
+ }
99
+ if (normalized.remoteNodeId) {
100
+ config.remoteNodeId = normalized.remoteNodeId;
101
+ }
74
102
  return config;
75
103
  }
@@ -25,6 +25,14 @@ function isInPageConnectionGrant(candidate) {
25
25
  record.inboxCapacity <= 0)) {
26
26
  return false;
27
27
  }
28
+ if (record.localNodeId !== undefined &&
29
+ (typeof record.localNodeId !== 'string' || record.localNodeId.length === 0)) {
30
+ return false;
31
+ }
32
+ if (record.remoteNodeId !== undefined &&
33
+ (typeof record.remoteNodeId !== 'string' || record.remoteNodeId.length === 0)) {
34
+ return false;
35
+ }
28
36
  return true;
29
37
  }
30
38
  function normalizeInPageConnectionGrant(candidate) {
@@ -58,6 +66,20 @@ function normalizeInPageConnectionGrant(candidate) {
58
66
  }
59
67
  result.inboxCapacity = Math.floor(inboxValue);
60
68
  }
69
+ const localNodeIdValue = candidate.localNodeId ?? candidate['local_node_id'];
70
+ if (localNodeIdValue !== undefined) {
71
+ if (typeof localNodeIdValue !== 'string' || localNodeIdValue.trim().length === 0) {
72
+ throw new TypeError('InPageConnectionGrant "localNodeId" must be a non-empty string when provided');
73
+ }
74
+ result.localNodeId = localNodeIdValue.trim();
75
+ }
76
+ const remoteNodeIdValue = candidate.remoteNodeId ?? candidate['remote_node_id'];
77
+ if (remoteNodeIdValue !== undefined) {
78
+ if (typeof remoteNodeIdValue !== 'string' || remoteNodeIdValue.trim().length === 0) {
79
+ throw new TypeError('InPageConnectionGrant "remoteNodeId" must be a non-empty string when provided');
80
+ }
81
+ result.remoteNodeId = remoteNodeIdValue.trim();
82
+ }
61
83
  return result;
62
84
  }
63
85
  function inPageGrantToConnectorConfig(grant) {
@@ -71,5 +93,11 @@ function inPageGrantToConnectorConfig(grant) {
71
93
  if (normalized.inboxCapacity !== undefined) {
72
94
  config.inboxCapacity = normalized.inboxCapacity;
73
95
  }
96
+ if (normalized.localNodeId) {
97
+ config.localNodeId = normalized.localNodeId;
98
+ }
99
+ if (normalized.remoteNodeId) {
100
+ config.remoteNodeId = normalized.remoteNodeId;
101
+ }
74
102
  return config;
75
103
  }
@@ -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.942
3
+ // Generated from package.json version: 0.3.5-test.943
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.942';
10
+ exports.VERSION = '0.3.5-test.943';
@@ -86,6 +86,8 @@ export class BroadcastChannelConnectorFactory extends ConnectorFactory {
86
86
  type: BROADCAST_CHANNEL_CONNECTOR_TYPE,
87
87
  channelName,
88
88
  inboxCapacity,
89
+ localNodeId: normalized.localNodeId,
90
+ remoteNodeId: normalized.remoteNodeId,
89
91
  };
90
92
  const connector = new BroadcastChannelConnector(connectorConfig, baseConfig);
91
93
  if (options.authorization) {
@@ -147,6 +149,16 @@ export class BroadcastChannelConnectorFactory extends ConnectorFactory {
147
149
  if (candidate.authorizationContext !== undefined) {
148
150
  normalized.authorizationContext = candidate.authorizationContext;
149
151
  }
152
+ // Handle localNodeId
153
+ const localNodeId = candidate.localNodeId ?? candidate['local_node_id'];
154
+ if (typeof localNodeId === 'string' && localNodeId.trim().length > 0) {
155
+ normalized.localNodeId = localNodeId.trim();
156
+ }
157
+ // Handle remoteNodeId
158
+ const remoteNodeId = candidate.remoteNodeId ?? candidate['remote_node_id'];
159
+ if (typeof remoteNodeId === 'string' && remoteNodeId.trim().length > 0) {
160
+ normalized.remoteNodeId = remoteNodeId.trim();
161
+ }
150
162
  normalized.channelName = normalized.channelName ?? DEFAULT_CHANNEL;
151
163
  normalized.inboxCapacity =
152
164
  normalized.inboxCapacity ?? DEFAULT_INBOX_CAPACITY;
@@ -3,6 +3,7 @@ import { FameTransportClose } from '../errors/errors.js';
3
3
  import { getLogger } from '../util/logging.js';
4
4
  import { BoundedAsyncQueue, QueueFullError, } from '../util/bounded-async-queue.js';
5
5
  import { ConnectorState } from '@naylence/core';
6
+ import { wrapTransportFrame, unwrapTransportFrame, serializeTransportFrame, } from './transport-frame.js';
6
7
  const logger = getLogger('naylence.fame.connector.broadcast_channel_connector');
7
8
  export const BROADCAST_CHANNEL_CONNECTOR_TYPE = 'broadcast-channel-connector';
8
9
  const DEFAULT_CHANNEL = 'naylence-fabric';
@@ -68,9 +69,20 @@ export class BroadcastChannelConnector extends BaseAsyncConnector {
68
69
  this.inbox = new BoundedAsyncQueue(preferredCapacity);
69
70
  this.connectorId = BroadcastChannelConnector.generateConnectorId();
70
71
  this.channel = new BroadcastChannel(this.channelName);
72
+ // Set local and remote node IDs (defaults to connector ID for backwards compatibility)
73
+ this.localNodeId =
74
+ typeof config.localNodeId === 'string' && config.localNodeId.trim().length > 0
75
+ ? config.localNodeId.trim()
76
+ : this.connectorId;
77
+ this.remoteNodeId =
78
+ typeof config.remoteNodeId === 'string' && config.remoteNodeId.trim().length > 0
79
+ ? config.remoteNodeId.trim()
80
+ : '*'; // Accept from any remote if not specified
71
81
  logger.debug('broadcast_channel_connector_created', {
72
82
  channel: this.channelName,
73
83
  connector_id: this.connectorId,
84
+ local_node_id: this.localNodeId,
85
+ remote_node_id: this.remoteNodeId,
74
86
  inbox_capacity: preferredCapacity,
75
87
  timestamp: new Date().toISOString(),
76
88
  });
@@ -103,6 +115,46 @@ export class BroadcastChannelConnector extends BaseAsyncConnector {
103
115
  if (busMessage.senderId === this.connectorId) {
104
116
  return;
105
117
  }
118
+ // Try to unwrap as transport frame
119
+ const unwrapped = unwrapTransportFrame(busMessage.payload, this.localNodeId, this.remoteNodeId === '*' ? busMessage.senderId : this.remoteNodeId);
120
+ if (unwrapped) {
121
+ // Successfully unwrapped transport frame
122
+ logger.debug('broadcast_channel_transport_frame_received', {
123
+ channel: this.channelName,
124
+ sender_id: busMessage.senderId,
125
+ connector_id: this.connectorId,
126
+ local_node_id: this.localNodeId,
127
+ remote_node_id: this.remoteNodeId,
128
+ payload_length: unwrapped.byteLength,
129
+ });
130
+ if (this._shouldSkipDuplicateAck(busMessage.senderId, unwrapped)) {
131
+ return;
132
+ }
133
+ try {
134
+ if (typeof this.inbox.tryEnqueue === 'function') {
135
+ const accepted = this.inbox.tryEnqueue(unwrapped);
136
+ if (accepted) {
137
+ return;
138
+ }
139
+ }
140
+ this.inbox.enqueue(unwrapped);
141
+ }
142
+ catch (error) {
143
+ if (error instanceof QueueFullError) {
144
+ logger.warning('broadcast_channel_receive_queue_full', {
145
+ channel: this.channelName,
146
+ });
147
+ }
148
+ else {
149
+ logger.error('broadcast_channel_receive_error', {
150
+ channel: this.channelName,
151
+ error: error instanceof Error ? error.message : String(error),
152
+ });
153
+ }
154
+ }
155
+ return;
156
+ }
157
+ // Fall back to legacy format (no transport frame)
106
158
  const payload = BroadcastChannelConnector.coercePayload(busMessage.payload);
107
159
  if (!payload) {
108
160
  logger.debug('broadcast_channel_payload_rejected', {
@@ -241,10 +293,26 @@ export class BroadcastChannelConnector extends BaseAsyncConnector {
241
293
  logger.debug('broadcast_channel_message_sending', {
242
294
  channel: this.channelName,
243
295
  sender_id: this.connectorId,
296
+ local_node_id: this.localNodeId,
297
+ remote_node_id: this.remoteNodeId,
244
298
  });
299
+ // Only use transport framing if both localNodeId and remoteNodeId are explicitly set
300
+ // (not using default values). This ensures backwards compatibility.
301
+ const useTransportFrame = this.localNodeId !== this.connectorId ||
302
+ this.remoteNodeId !== '*';
303
+ let payload;
304
+ if (useTransportFrame) {
305
+ // Wrap payload in transport frame
306
+ const frame = wrapTransportFrame(data, this.localNodeId, this.remoteNodeId);
307
+ payload = serializeTransportFrame(frame);
308
+ }
309
+ else {
310
+ // Legacy format: send raw payload
311
+ payload = data;
312
+ }
245
313
  this.channel.postMessage({
246
314
  senderId: this.connectorId,
247
- payload: data,
315
+ payload,
248
316
  });
249
317
  }
250
318
  async _transportReceive() {
@@ -81,6 +81,8 @@ export class InPageConnectorFactory extends ConnectorFactory {
81
81
  type: INPAGE_CONNECTOR_TYPE,
82
82
  channelName,
83
83
  inboxCapacity,
84
+ localNodeId: normalized.localNodeId,
85
+ remoteNodeId: normalized.remoteNodeId,
84
86
  };
85
87
  const connector = new InPageConnector(connectorConfig, baseConfig);
86
88
  if (options.authorization) {
@@ -149,6 +151,16 @@ export class InPageConnectorFactory extends ConnectorFactory {
149
151
  if (candidate.authorizationContext !== undefined) {
150
152
  normalized.authorizationContext = candidate.authorizationContext;
151
153
  }
154
+ // Handle localNodeId
155
+ const localNodeId = candidate.localNodeId ?? candidate['local_node_id'];
156
+ if (typeof localNodeId === 'string' && localNodeId.trim().length > 0) {
157
+ normalized.localNodeId = localNodeId.trim();
158
+ }
159
+ // Handle remoteNodeId
160
+ const remoteNodeId = candidate.remoteNodeId ?? candidate['remote_node_id'];
161
+ if (typeof remoteNodeId === 'string' && remoteNodeId.trim().length > 0) {
162
+ normalized.remoteNodeId = remoteNodeId.trim();
163
+ }
152
164
  normalized.channelName = normalized.channelName ?? DEFAULT_CHANNEL;
153
165
  normalized.inboxCapacity =
154
166
  normalized.inboxCapacity ?? DEFAULT_INBOX_CAPACITY;
@@ -7,6 +7,7 @@ import { FameTransportClose } from '../errors/errors.js';
7
7
  import { getLogger } from '../util/logging.js';
8
8
  import { BoundedAsyncQueue, QueueFullError, } from '../util/bounded-async-queue.js';
9
9
  import { ConnectorState } from '@naylence/core';
10
+ import { wrapTransportFrame, unwrapTransportFrame, serializeTransportFrame, } from './transport-frame.js';
10
11
  const logger = getLogger('naylence.fame.connector.inpage_connector');
11
12
  export const INPAGE_CONNECTOR_TYPE = 'inpage-connector';
12
13
  const DEFAULT_CHANNEL = 'naylence-fabric';
@@ -77,9 +78,20 @@ export class InPageConnector extends BaseAsyncConnector {
77
78
  : DEFAULT_INBOX_CAPACITY;
78
79
  this.inbox = new BoundedAsyncQueue(preferredCapacity);
79
80
  this.connectorId = InPageConnector.generateConnectorId();
81
+ // Set local and remote node IDs (defaults to connector ID for backwards compatibility)
82
+ this.localNodeId =
83
+ typeof config.localNodeId === 'string' && config.localNodeId.trim().length > 0
84
+ ? config.localNodeId.trim()
85
+ : this.connectorId;
86
+ this.remoteNodeId =
87
+ typeof config.remoteNodeId === 'string' && config.remoteNodeId.trim().length > 0
88
+ ? config.remoteNodeId.trim()
89
+ : '*'; // Accept from any remote if not specified
80
90
  logger.debug('inpage_connector_initialized', {
81
91
  channel: this.channelName,
82
92
  connector_id: this.connectorId,
93
+ local_node_id: this.localNodeId,
94
+ remote_node_id: this.remoteNodeId,
83
95
  });
84
96
  this.onMsg = (event) => {
85
97
  const messageEvent = event;
@@ -113,6 +125,43 @@ export class InPageConnector extends BaseAsyncConnector {
113
125
  if (busMessage.senderId === this.connectorId) {
114
126
  return;
115
127
  }
128
+ // Try to unwrap as transport frame
129
+ const unwrapped = unwrapTransportFrame(busMessage.payload, this.localNodeId, this.remoteNodeId === '*' ? busMessage.senderId : this.remoteNodeId);
130
+ if (unwrapped) {
131
+ // Successfully unwrapped transport frame
132
+ logger.debug('inpage_transport_frame_received', {
133
+ channel: this.channelName,
134
+ sender_id: busMessage.senderId,
135
+ connector_id: this.connectorId,
136
+ local_node_id: this.localNodeId,
137
+ remote_node_id: this.remoteNodeId,
138
+ payload_length: unwrapped.byteLength,
139
+ });
140
+ try {
141
+ if (typeof this.inbox.tryEnqueue === 'function') {
142
+ const accepted = this.inbox.tryEnqueue(unwrapped);
143
+ if (accepted) {
144
+ return;
145
+ }
146
+ }
147
+ this.inbox.enqueue(unwrapped);
148
+ }
149
+ catch (error) {
150
+ if (error instanceof QueueFullError) {
151
+ logger.warning('inpage_receive_queue_full', {
152
+ channel: this.channelName,
153
+ });
154
+ }
155
+ else {
156
+ logger.error('inpage_receive_error', {
157
+ channel: this.channelName,
158
+ error: error instanceof Error ? error.message : String(error),
159
+ });
160
+ }
161
+ }
162
+ return;
163
+ }
164
+ // Fall back to legacy format (no transport frame)
116
165
  const payload = InPageConnector.coercePayload(busMessage.payload);
117
166
  if (!payload) {
118
167
  logger.debug('inpage_payload_rejected', {
@@ -271,11 +320,27 @@ export class InPageConnector extends BaseAsyncConnector {
271
320
  logger.debug('inpage_message_sending', {
272
321
  channel: this.channelName,
273
322
  sender_id: this.connectorId,
323
+ local_node_id: this.localNodeId,
324
+ remote_node_id: this.remoteNodeId,
274
325
  });
326
+ // Only use transport framing if both localNodeId and remoteNodeId are explicitly set
327
+ // (not using default values). This ensures backwards compatibility.
328
+ const useTransportFrame = this.localNodeId !== this.connectorId ||
329
+ this.remoteNodeId !== '*';
330
+ let payload;
331
+ if (useTransportFrame) {
332
+ // Wrap payload in transport frame
333
+ const frame = wrapTransportFrame(data, this.localNodeId, this.remoteNodeId);
334
+ payload = serializeTransportFrame(frame);
335
+ }
336
+ else {
337
+ // Legacy format: send raw payload
338
+ payload = data;
339
+ }
275
340
  const event = new MessageEvent(this.channelName, {
276
341
  data: {
277
342
  senderId: this.connectorId,
278
- payload: data,
343
+ payload,
279
344
  },
280
345
  });
281
346
  getSharedBus().dispatchEvent(event);
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Transport frame layer for multiplexing logical links on physical channels.
3
+ *
4
+ * This lightweight framing layer wraps raw FAME payloads to enable multiple
5
+ * logical connections over a single physical channel (BroadcastChannel or InPage bus).
6
+ *
7
+ * The transport frame does NOT modify FAME envelopes - it only wraps the raw
8
+ * Uint8Array payload at the connector level.
9
+ */
10
+ /**
11
+ * Transport frame version for future compatibility
12
+ */
13
+ export const TRANSPORT_FRAME_VERSION = 1;
14
+ /**
15
+ * Wrap a raw payload in a transport frame
16
+ *
17
+ * @param payload - Raw FAME envelope bytes
18
+ * @param srcNodeId - Local node ID (this connector)
19
+ * @param dstNodeId - Remote node ID (target connector)
20
+ * @returns Transport frame ready for transmission
21
+ */
22
+ export function wrapTransportFrame(payload, srcNodeId, dstNodeId) {
23
+ return {
24
+ v: TRANSPORT_FRAME_VERSION,
25
+ src: srcNodeId,
26
+ dst: dstNodeId,
27
+ payload,
28
+ };
29
+ }
30
+ /**
31
+ * Serialize a transport frame for transmission over the bus
32
+ *
33
+ * @param frame - Transport frame to serialize
34
+ * @returns Serialized frame data ready for postMessage/dispatchEvent
35
+ */
36
+ export function serializeTransportFrame(frame) {
37
+ // Convert Uint8Array to regular array for JSON serialization
38
+ const serializable = {
39
+ v: frame.v,
40
+ src: frame.src,
41
+ dst: frame.dst,
42
+ payload: Array.from(frame.payload),
43
+ };
44
+ return serializable;
45
+ }
46
+ /**
47
+ * Unwrap a transport frame, validating source and destination
48
+ *
49
+ * @param raw - Raw data from the bus
50
+ * @param localNodeId - This connector's node ID
51
+ * @param remoteNodeId - Expected remote node ID
52
+ * @returns Unwrapped payload if frame is valid and addressed to us, null otherwise
53
+ */
54
+ export function unwrapTransportFrame(raw, localNodeId, remoteNodeId) {
55
+ // Validate basic structure
56
+ if (!raw || typeof raw !== 'object') {
57
+ return null;
58
+ }
59
+ const frame = raw;
60
+ // Check version
61
+ if (frame.v !== TRANSPORT_FRAME_VERSION) {
62
+ return null;
63
+ }
64
+ // Check src and dst
65
+ if (typeof frame.src !== 'string' || typeof frame.dst !== 'string') {
66
+ return null;
67
+ }
68
+ // Only accept frames addressed to us from the expected remote
69
+ if (frame.dst !== localNodeId || frame.src !== remoteNodeId) {
70
+ return null;
71
+ }
72
+ // Extract payload
73
+ if (!frame.payload || !Array.isArray(frame.payload)) {
74
+ return null;
75
+ }
76
+ // Convert array back to Uint8Array
77
+ return Uint8Array.from(frame.payload);
78
+ }
79
+ /**
80
+ * Check if raw data looks like a transport frame
81
+ *
82
+ * @param raw - Raw data from the bus
83
+ * @returns True if this appears to be a transport frame
84
+ */
85
+ export function isTransportFrame(raw) {
86
+ if (!raw || typeof raw !== 'object') {
87
+ return false;
88
+ }
89
+ const frame = raw;
90
+ return (typeof frame.v === 'number' &&
91
+ typeof frame.src === 'string' &&
92
+ typeof frame.dst === 'string' &&
93
+ Array.isArray(frame.payload));
94
+ }