@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
@@ -98,12 +98,12 @@ installProcessEnvShim();
98
98
  // --- END ENV SHIM ---
99
99
 
100
100
  // This file is auto-generated during build - do not edit manually
101
- // Generated from package.json version: 0.3.5-test.942
101
+ // Generated from package.json version: 0.3.5-test.943
102
102
  /**
103
103
  * The package version, injected at build time.
104
104
  * @internal
105
105
  */
106
- const VERSION = '0.3.5-test.942';
106
+ const VERSION = '0.3.5-test.943';
107
107
 
108
108
  /**
109
109
  * Fame protocol specific error classes with WebSocket close codes and proper inheritance.
@@ -9818,6 +9818,85 @@ class BoundedAsyncQueue {
9818
9818
  }
9819
9819
  }
9820
9820
 
9821
+ /**
9822
+ * Transport frame layer for multiplexing logical links on physical channels.
9823
+ *
9824
+ * This lightweight framing layer wraps raw FAME payloads to enable multiple
9825
+ * logical connections over a single physical channel (BroadcastChannel or InPage bus).
9826
+ *
9827
+ * The transport frame does NOT modify FAME envelopes - it only wraps the raw
9828
+ * Uint8Array payload at the connector level.
9829
+ */
9830
+ /**
9831
+ * Transport frame version for future compatibility
9832
+ */
9833
+ const TRANSPORT_FRAME_VERSION = 1;
9834
+ /**
9835
+ * Wrap a raw payload in a transport frame
9836
+ *
9837
+ * @param payload - Raw FAME envelope bytes
9838
+ * @param srcNodeId - Local node ID (this connector)
9839
+ * @param dstNodeId - Remote node ID (target connector)
9840
+ * @returns Transport frame ready for transmission
9841
+ */
9842
+ function wrapTransportFrame(payload, srcNodeId, dstNodeId) {
9843
+ return {
9844
+ v: TRANSPORT_FRAME_VERSION,
9845
+ src: srcNodeId,
9846
+ dst: dstNodeId,
9847
+ payload,
9848
+ };
9849
+ }
9850
+ /**
9851
+ * Serialize a transport frame for transmission over the bus
9852
+ *
9853
+ * @param frame - Transport frame to serialize
9854
+ * @returns Serialized frame data ready for postMessage/dispatchEvent
9855
+ */
9856
+ function serializeTransportFrame(frame) {
9857
+ // Convert Uint8Array to regular array for JSON serialization
9858
+ const serializable = {
9859
+ v: frame.v,
9860
+ src: frame.src,
9861
+ dst: frame.dst,
9862
+ payload: Array.from(frame.payload),
9863
+ };
9864
+ return serializable;
9865
+ }
9866
+ /**
9867
+ * Unwrap a transport frame, validating source and destination
9868
+ *
9869
+ * @param raw - Raw data from the bus
9870
+ * @param localNodeId - This connector's node ID
9871
+ * @param remoteNodeId - Expected remote node ID
9872
+ * @returns Unwrapped payload if frame is valid and addressed to us, null otherwise
9873
+ */
9874
+ function unwrapTransportFrame(raw, localNodeId, remoteNodeId) {
9875
+ // Validate basic structure
9876
+ if (!raw || typeof raw !== 'object') {
9877
+ return null;
9878
+ }
9879
+ const frame = raw;
9880
+ // Check version
9881
+ if (frame.v !== TRANSPORT_FRAME_VERSION) {
9882
+ return null;
9883
+ }
9884
+ // Check src and dst
9885
+ if (typeof frame.src !== 'string' || typeof frame.dst !== 'string') {
9886
+ return null;
9887
+ }
9888
+ // Only accept frames addressed to us from the expected remote
9889
+ if (frame.dst !== localNodeId || frame.src !== remoteNodeId) {
9890
+ return null;
9891
+ }
9892
+ // Extract payload
9893
+ if (!frame.payload || !Array.isArray(frame.payload)) {
9894
+ return null;
9895
+ }
9896
+ // Convert array back to Uint8Array
9897
+ return Uint8Array.from(frame.payload);
9898
+ }
9899
+
9821
9900
  const logger$_ = getLogger('naylence.fame.connector.broadcast_channel_connector');
9822
9901
  const BROADCAST_CHANNEL_CONNECTOR_TYPE = 'broadcast-channel-connector';
9823
9902
  const DEFAULT_CHANNEL$7 = 'naylence-fabric';
@@ -9883,9 +9962,20 @@ let BroadcastChannelConnector$2 = class BroadcastChannelConnector extends BaseAs
9883
9962
  this.inbox = new BoundedAsyncQueue(preferredCapacity);
9884
9963
  this.connectorId = BroadcastChannelConnector.generateConnectorId();
9885
9964
  this.channel = new BroadcastChannel(this.channelName);
9965
+ // Set local and remote node IDs (defaults to connector ID for backwards compatibility)
9966
+ this.localNodeId =
9967
+ typeof config.localNodeId === 'string' && config.localNodeId.trim().length > 0
9968
+ ? config.localNodeId.trim()
9969
+ : this.connectorId;
9970
+ this.remoteNodeId =
9971
+ typeof config.remoteNodeId === 'string' && config.remoteNodeId.trim().length > 0
9972
+ ? config.remoteNodeId.trim()
9973
+ : '*'; // Accept from any remote if not specified
9886
9974
  logger$_.debug('broadcast_channel_connector_created', {
9887
9975
  channel: this.channelName,
9888
9976
  connector_id: this.connectorId,
9977
+ local_node_id: this.localNodeId,
9978
+ remote_node_id: this.remoteNodeId,
9889
9979
  inbox_capacity: preferredCapacity,
9890
9980
  timestamp: new Date().toISOString(),
9891
9981
  });
@@ -9918,6 +10008,46 @@ let BroadcastChannelConnector$2 = class BroadcastChannelConnector extends BaseAs
9918
10008
  if (busMessage.senderId === this.connectorId) {
9919
10009
  return;
9920
10010
  }
10011
+ // Try to unwrap as transport frame
10012
+ const unwrapped = unwrapTransportFrame(busMessage.payload, this.localNodeId, this.remoteNodeId === '*' ? busMessage.senderId : this.remoteNodeId);
10013
+ if (unwrapped) {
10014
+ // Successfully unwrapped transport frame
10015
+ logger$_.debug('broadcast_channel_transport_frame_received', {
10016
+ channel: this.channelName,
10017
+ sender_id: busMessage.senderId,
10018
+ connector_id: this.connectorId,
10019
+ local_node_id: this.localNodeId,
10020
+ remote_node_id: this.remoteNodeId,
10021
+ payload_length: unwrapped.byteLength,
10022
+ });
10023
+ if (this._shouldSkipDuplicateAck(busMessage.senderId, unwrapped)) {
10024
+ return;
10025
+ }
10026
+ try {
10027
+ if (typeof this.inbox.tryEnqueue === 'function') {
10028
+ const accepted = this.inbox.tryEnqueue(unwrapped);
10029
+ if (accepted) {
10030
+ return;
10031
+ }
10032
+ }
10033
+ this.inbox.enqueue(unwrapped);
10034
+ }
10035
+ catch (error) {
10036
+ if (error instanceof QueueFullError) {
10037
+ logger$_.warning('broadcast_channel_receive_queue_full', {
10038
+ channel: this.channelName,
10039
+ });
10040
+ }
10041
+ else {
10042
+ logger$_.error('broadcast_channel_receive_error', {
10043
+ channel: this.channelName,
10044
+ error: error instanceof Error ? error.message : String(error),
10045
+ });
10046
+ }
10047
+ }
10048
+ return;
10049
+ }
10050
+ // Fall back to legacy format (no transport frame)
9921
10051
  const payload = BroadcastChannelConnector.coercePayload(busMessage.payload);
9922
10052
  if (!payload) {
9923
10053
  logger$_.debug('broadcast_channel_payload_rejected', {
@@ -10056,10 +10186,26 @@ let BroadcastChannelConnector$2 = class BroadcastChannelConnector extends BaseAs
10056
10186
  logger$_.debug('broadcast_channel_message_sending', {
10057
10187
  channel: this.channelName,
10058
10188
  sender_id: this.connectorId,
10059
- });
10189
+ local_node_id: this.localNodeId,
10190
+ remote_node_id: this.remoteNodeId,
10191
+ });
10192
+ // Only use transport framing if both localNodeId and remoteNodeId are explicitly set
10193
+ // (not using default values). This ensures backwards compatibility.
10194
+ const useTransportFrame = this.localNodeId !== this.connectorId ||
10195
+ this.remoteNodeId !== '*';
10196
+ let payload;
10197
+ if (useTransportFrame) {
10198
+ // Wrap payload in transport frame
10199
+ const frame = wrapTransportFrame(data, this.localNodeId, this.remoteNodeId);
10200
+ payload = serializeTransportFrame(frame);
10201
+ }
10202
+ else {
10203
+ // Legacy format: send raw payload
10204
+ payload = data;
10205
+ }
10060
10206
  this.channel.postMessage({
10061
10207
  senderId: this.connectorId,
10062
- payload: data,
10208
+ payload,
10063
10209
  });
10064
10210
  }
10065
10211
  async _transportReceive() {
@@ -10352,6 +10498,14 @@ function isBroadcastChannelConnectionGrant(candidate) {
10352
10498
  record.inboxCapacity <= 0)) {
10353
10499
  return false;
10354
10500
  }
10501
+ if (record.localNodeId !== undefined &&
10502
+ (typeof record.localNodeId !== 'string' || record.localNodeId.length === 0)) {
10503
+ return false;
10504
+ }
10505
+ if (record.remoteNodeId !== undefined &&
10506
+ (typeof record.remoteNodeId !== 'string' || record.remoteNodeId.length === 0)) {
10507
+ return false;
10508
+ }
10355
10509
  return true;
10356
10510
  }
10357
10511
  function normalizeBroadcastChannelConnectionGrant(candidate) {
@@ -10385,6 +10539,20 @@ function normalizeBroadcastChannelConnectionGrant(candidate) {
10385
10539
  }
10386
10540
  result.inboxCapacity = Math.floor(inboxValue);
10387
10541
  }
10542
+ const localNodeIdValue = candidate.localNodeId ?? candidate['local_node_id'];
10543
+ if (localNodeIdValue !== undefined) {
10544
+ if (typeof localNodeIdValue !== 'string' || localNodeIdValue.trim().length === 0) {
10545
+ throw new TypeError('BroadcastChannelConnectionGrant "localNodeId" must be a non-empty string when provided');
10546
+ }
10547
+ result.localNodeId = localNodeIdValue.trim();
10548
+ }
10549
+ const remoteNodeIdValue = candidate.remoteNodeId ?? candidate['remote_node_id'];
10550
+ if (remoteNodeIdValue !== undefined) {
10551
+ if (typeof remoteNodeIdValue !== 'string' || remoteNodeIdValue.trim().length === 0) {
10552
+ throw new TypeError('BroadcastChannelConnectionGrant "remoteNodeId" must be a non-empty string when provided');
10553
+ }
10554
+ result.remoteNodeId = remoteNodeIdValue.trim();
10555
+ }
10388
10556
  return result;
10389
10557
  }
10390
10558
  function broadcastChannelGrantToConnectorConfig(grant) {
@@ -10398,6 +10566,12 @@ function broadcastChannelGrantToConnectorConfig(grant) {
10398
10566
  if (normalized.inboxCapacity !== undefined) {
10399
10567
  config.inboxCapacity = normalized.inboxCapacity;
10400
10568
  }
10569
+ if (normalized.localNodeId) {
10570
+ config.localNodeId = normalized.localNodeId;
10571
+ }
10572
+ if (normalized.remoteNodeId) {
10573
+ config.remoteNodeId = normalized.remoteNodeId;
10574
+ }
10401
10575
  return config;
10402
10576
  }
10403
10577
 
@@ -20344,9 +20518,20 @@ class InPageConnector extends BaseAsyncConnector {
20344
20518
  : DEFAULT_INBOX_CAPACITY$6;
20345
20519
  this.inbox = new BoundedAsyncQueue(preferredCapacity);
20346
20520
  this.connectorId = InPageConnector.generateConnectorId();
20521
+ // Set local and remote node IDs (defaults to connector ID for backwards compatibility)
20522
+ this.localNodeId =
20523
+ typeof config.localNodeId === 'string' && config.localNodeId.trim().length > 0
20524
+ ? config.localNodeId.trim()
20525
+ : this.connectorId;
20526
+ this.remoteNodeId =
20527
+ typeof config.remoteNodeId === 'string' && config.remoteNodeId.trim().length > 0
20528
+ ? config.remoteNodeId.trim()
20529
+ : '*'; // Accept from any remote if not specified
20347
20530
  logger$G.debug('inpage_connector_initialized', {
20348
20531
  channel: this.channelName,
20349
20532
  connector_id: this.connectorId,
20533
+ local_node_id: this.localNodeId,
20534
+ remote_node_id: this.remoteNodeId,
20350
20535
  });
20351
20536
  this.onMsg = (event) => {
20352
20537
  const messageEvent = event;
@@ -20380,6 +20565,43 @@ class InPageConnector extends BaseAsyncConnector {
20380
20565
  if (busMessage.senderId === this.connectorId) {
20381
20566
  return;
20382
20567
  }
20568
+ // Try to unwrap as transport frame
20569
+ const unwrapped = unwrapTransportFrame(busMessage.payload, this.localNodeId, this.remoteNodeId === '*' ? busMessage.senderId : this.remoteNodeId);
20570
+ if (unwrapped) {
20571
+ // Successfully unwrapped transport frame
20572
+ logger$G.debug('inpage_transport_frame_received', {
20573
+ channel: this.channelName,
20574
+ sender_id: busMessage.senderId,
20575
+ connector_id: this.connectorId,
20576
+ local_node_id: this.localNodeId,
20577
+ remote_node_id: this.remoteNodeId,
20578
+ payload_length: unwrapped.byteLength,
20579
+ });
20580
+ try {
20581
+ if (typeof this.inbox.tryEnqueue === 'function') {
20582
+ const accepted = this.inbox.tryEnqueue(unwrapped);
20583
+ if (accepted) {
20584
+ return;
20585
+ }
20586
+ }
20587
+ this.inbox.enqueue(unwrapped);
20588
+ }
20589
+ catch (error) {
20590
+ if (error instanceof QueueFullError) {
20591
+ logger$G.warning('inpage_receive_queue_full', {
20592
+ channel: this.channelName,
20593
+ });
20594
+ }
20595
+ else {
20596
+ logger$G.error('inpage_receive_error', {
20597
+ channel: this.channelName,
20598
+ error: error instanceof Error ? error.message : String(error),
20599
+ });
20600
+ }
20601
+ }
20602
+ return;
20603
+ }
20604
+ // Fall back to legacy format (no transport frame)
20383
20605
  const payload = InPageConnector.coercePayload(busMessage.payload);
20384
20606
  if (!payload) {
20385
20607
  logger$G.debug('inpage_payload_rejected', {
@@ -20538,11 +20760,27 @@ class InPageConnector extends BaseAsyncConnector {
20538
20760
  logger$G.debug('inpage_message_sending', {
20539
20761
  channel: this.channelName,
20540
20762
  sender_id: this.connectorId,
20541
- });
20763
+ local_node_id: this.localNodeId,
20764
+ remote_node_id: this.remoteNodeId,
20765
+ });
20766
+ // Only use transport framing if both localNodeId and remoteNodeId are explicitly set
20767
+ // (not using default values). This ensures backwards compatibility.
20768
+ const useTransportFrame = this.localNodeId !== this.connectorId ||
20769
+ this.remoteNodeId !== '*';
20770
+ let payload;
20771
+ if (useTransportFrame) {
20772
+ // Wrap payload in transport frame
20773
+ const frame = wrapTransportFrame(data, this.localNodeId, this.remoteNodeId);
20774
+ payload = serializeTransportFrame(frame);
20775
+ }
20776
+ else {
20777
+ // Legacy format: send raw payload
20778
+ payload = data;
20779
+ }
20542
20780
  const event = new MessageEvent(this.channelName, {
20543
20781
  data: {
20544
20782
  senderId: this.connectorId,
20545
- payload: data,
20783
+ payload,
20546
20784
  },
20547
20785
  });
20548
20786
  getSharedBus$1().dispatchEvent(event);
@@ -27907,6 +28145,14 @@ function isInPageConnectionGrant(candidate) {
27907
28145
  record.inboxCapacity <= 0)) {
27908
28146
  return false;
27909
28147
  }
28148
+ if (record.localNodeId !== undefined &&
28149
+ (typeof record.localNodeId !== 'string' || record.localNodeId.length === 0)) {
28150
+ return false;
28151
+ }
28152
+ if (record.remoteNodeId !== undefined &&
28153
+ (typeof record.remoteNodeId !== 'string' || record.remoteNodeId.length === 0)) {
28154
+ return false;
28155
+ }
27910
28156
  return true;
27911
28157
  }
27912
28158
  function normalizeInPageConnectionGrant(candidate) {
@@ -27940,6 +28186,20 @@ function normalizeInPageConnectionGrant(candidate) {
27940
28186
  }
27941
28187
  result.inboxCapacity = Math.floor(inboxValue);
27942
28188
  }
28189
+ const localNodeIdValue = candidate.localNodeId ?? candidate['local_node_id'];
28190
+ if (localNodeIdValue !== undefined) {
28191
+ if (typeof localNodeIdValue !== 'string' || localNodeIdValue.trim().length === 0) {
28192
+ throw new TypeError('InPageConnectionGrant "localNodeId" must be a non-empty string when provided');
28193
+ }
28194
+ result.localNodeId = localNodeIdValue.trim();
28195
+ }
28196
+ const remoteNodeIdValue = candidate.remoteNodeId ?? candidate['remote_node_id'];
28197
+ if (remoteNodeIdValue !== undefined) {
28198
+ if (typeof remoteNodeIdValue !== 'string' || remoteNodeIdValue.trim().length === 0) {
28199
+ throw new TypeError('InPageConnectionGrant "remoteNodeId" must be a non-empty string when provided');
28200
+ }
28201
+ result.remoteNodeId = remoteNodeIdValue.trim();
28202
+ }
27943
28203
  return result;
27944
28204
  }
27945
28205
  function inPageGrantToConnectorConfig(grant) {
@@ -27953,6 +28213,12 @@ function inPageGrantToConnectorConfig(grant) {
27953
28213
  if (normalized.inboxCapacity !== undefined) {
27954
28214
  config.inboxCapacity = normalized.inboxCapacity;
27955
28215
  }
28216
+ if (normalized.localNodeId) {
28217
+ config.localNodeId = normalized.localNodeId;
28218
+ }
28219
+ if (normalized.remoteNodeId) {
28220
+ config.remoteNodeId = normalized.remoteNodeId;
28221
+ }
27956
28222
  return config;
27957
28223
  }
27958
28224
 
@@ -28574,6 +28840,8 @@ class InPageConnectorFactory extends ConnectorFactory {
28574
28840
  type: INPAGE_CONNECTOR_TYPE,
28575
28841
  channelName,
28576
28842
  inboxCapacity,
28843
+ localNodeId: normalized.localNodeId,
28844
+ remoteNodeId: normalized.remoteNodeId,
28577
28845
  };
28578
28846
  const connector = new InPageConnector(connectorConfig, baseConfig);
28579
28847
  if (options.authorization) {
@@ -28642,6 +28910,16 @@ class InPageConnectorFactory extends ConnectorFactory {
28642
28910
  if (candidate.authorizationContext !== undefined) {
28643
28911
  normalized.authorizationContext = candidate.authorizationContext;
28644
28912
  }
28913
+ // Handle localNodeId
28914
+ const localNodeId = candidate.localNodeId ?? candidate['local_node_id'];
28915
+ if (typeof localNodeId === 'string' && localNodeId.trim().length > 0) {
28916
+ normalized.localNodeId = localNodeId.trim();
28917
+ }
28918
+ // Handle remoteNodeId
28919
+ const remoteNodeId = candidate.remoteNodeId ?? candidate['remote_node_id'];
28920
+ if (typeof remoteNodeId === 'string' && remoteNodeId.trim().length > 0) {
28921
+ normalized.remoteNodeId = remoteNodeId.trim();
28922
+ }
28645
28923
  normalized.channelName = normalized.channelName ?? DEFAULT_CHANNEL$5;
28646
28924
  normalized.inboxCapacity =
28647
28925
  normalized.inboxCapacity ?? DEFAULT_INBOX_CAPACITY$5;
@@ -28741,6 +29019,8 @@ class BroadcastChannelConnectorFactory extends ConnectorFactory {
28741
29019
  type: BROADCAST_CHANNEL_CONNECTOR_TYPE,
28742
29020
  channelName,
28743
29021
  inboxCapacity,
29022
+ localNodeId: normalized.localNodeId,
29023
+ remoteNodeId: normalized.remoteNodeId,
28744
29024
  };
28745
29025
  const connector = new BroadcastChannelConnector(connectorConfig, baseConfig);
28746
29026
  if (options.authorization) {
@@ -28802,6 +29082,16 @@ class BroadcastChannelConnectorFactory extends ConnectorFactory {
28802
29082
  if (candidate.authorizationContext !== undefined) {
28803
29083
  normalized.authorizationContext = candidate.authorizationContext;
28804
29084
  }
29085
+ // Handle localNodeId
29086
+ const localNodeId = candidate.localNodeId ?? candidate['local_node_id'];
29087
+ if (typeof localNodeId === 'string' && localNodeId.trim().length > 0) {
29088
+ normalized.localNodeId = localNodeId.trim();
29089
+ }
29090
+ // Handle remoteNodeId
29091
+ const remoteNodeId = candidate.remoteNodeId ?? candidate['remote_node_id'];
29092
+ if (typeof remoteNodeId === 'string' && remoteNodeId.trim().length > 0) {
29093
+ normalized.remoteNodeId = remoteNodeId.trim();
29094
+ }
28805
29095
  normalized.channelName = normalized.channelName ?? DEFAULT_CHANNEL$4;
28806
29096
  normalized.inboxCapacity =
28807
29097
  normalized.inboxCapacity ?? DEFAULT_INBOX_CAPACITY$4;