@lf-lang/reactor-ts
Version:
A reactor-oriented programming framework in TypeScript
1,075 lines (1,072 loc) • 84.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RemoteFederatePort = exports.FederatedApp = exports.NetworkReceiver = exports.NetworkSender = exports.CONNECT_NUM_RETRIES = exports.CONNECT_RETRY_INTERVAL = exports.BUFFER_SIZE = void 0;
const net_1 = require("net");
const events_1 = require("events");
const internal_1 = require("./internal");
// ---------------------------------------------------------------------//
// Federated Execution Constants and Enums //
// ---------------------------------------------------------------------//
// FIXME: For now this constant is unused.
/**
* Size of the buffer used for messages sent between federates.
* This is used by both the federates and the rti, so message lengths
* should generally match.
*/
exports.BUFFER_SIZE = 256;
/**
* Number of seconds that elapse between a federate's attempts
* to connect to the RTI.
*/
exports.CONNECT_RETRY_INTERVAL = internal_1.TimeValue.secs(2);
/**
* Bound on the number of retries to connect to the RTI.
* A federate will retry every CONNECT_RETRY_INTERVAL seconds
* this many times before giving up. E.g., 500 retries every
* 2 seconds results in retrying for about 16 minutes.
*/
exports.CONNECT_NUM_RETRIES = 500;
/**
* Message types defined for communication between a federate and the
* RTI (Run Time Infrastructure).
* In the C reactor target these message types are encoded as an unsigned char,
* so to maintain compatability in TypeScript the magnitude must not exceed 255
*/
var RTIMessageTypes;
(function (RTIMessageTypes) {
/**
* Byte identifying a rejection of the previously received message.
* The reason for the rejection is included as an additional byte
* (uchar) (see below for encodings of rejection reasons).
*/
RTIMessageTypes[RTIMessageTypes["MSG_TYPE_REJECT"] = 0] = "MSG_TYPE_REJECT";
/**
* Byte identifying a message from a federate to an RTI containing
* the federation ID and the federate ID. The message contains, in
* this order:
* * One byte equal to MSG_TYPE_FED_IDS.
* * Two bytes (ushort) giving the federate ID.
* * One byte (uchar) giving the length N of the federation ID.
* * N bytes containing the federation ID.
* Each federate needs to have a unique ID between 0 and
* NUMBER_OF_FEDERATES-1.
* Each federate, when starting up, should send this message
* to the RTI. This is its first message to the RTI.
* The RTI will respond with either MSG_TYPE_REJECT, MSG_TYPE_ACK, or MSG_TYPE_UDP_PORT.
* If the federate is a C target LF program, the generated federate
* code does this by calling synchronize_with_other_federates(),
* passing to it its federate ID.
*/
RTIMessageTypes[RTIMessageTypes["MSG_TYPE_FED_IDS"] = 1] = "MSG_TYPE_FED_IDS";
/**
* Byte identifying a timestamp message, which is 64 bits long.
* Each federate sends its starting physical time as a message of this
* type, and the RTI broadcasts to all the federates the starting logical
* time as a message of this type.
*/
RTIMessageTypes[RTIMessageTypes["MSG_TYPE_TIMESTAMP"] = 2] = "MSG_TYPE_TIMESTAMP";
/**
* Byte identifying a message to forward to another federate.
* The next two bytes will be the ID of the destination port.
* The next two bytes are the destination federate ID.
* The four bytes after that will be the length of the message.
* The remaining bytes are the message.
* NOTE: This is currently not used. All messages are tagged, even
* on physical connections, because if "after" is used, the message
* may preserve the logical timestamp rather than using the physical time.
*/
RTIMessageTypes[RTIMessageTypes["MSG_TYPE_MESSAGE"] = 3] = "MSG_TYPE_MESSAGE";
/**
* Byte identifying that the federate is ending its execution.
*/
RTIMessageTypes[RTIMessageTypes["MSG_TYPE_RESIGN"] = 4] = "MSG_TYPE_RESIGN";
/**
* Byte identifying a timestamped message to forward to another federate.
* The next two bytes will be the ID of the destination reactor port.
* The next two bytes are the destination federate ID.
* The four bytes after that will be the length of the message.
* The next eight bytes will be the timestamp of the message.
* The next four bytes will be the microstep of the message.
* The remaining bytes are the message.
*
* With centralized coordination, all such messages flow through the RTI.
* With decentralized coordination, tagged messages are sent peer-to-peer
* between federates and are marked with MSG_TYPE_P2P_TAGGED_MESSAGE.
*/
RTIMessageTypes[RTIMessageTypes["MSG_TYPE_TAGGED_MESSAGE"] = 5] = "MSG_TYPE_TAGGED_MESSAGE";
/**
* Byte identifying a next event tag (NET) message sent from a federate
* in centralized coordination.
* The next eight bytes will be the timestamp.
* The next four bytes will be the microstep.
* This message from a federate tells the RTI the tag of the earliest event
* on that federate's event queue. In other words, absent any further inputs
* from other federates, this will be the least tag of the next set of
* reactions on that federate. If the event queue is empty and a timeout
* time has been specified, then the timeout time will be sent. If there is
* no timeout time, then FOREVER will be sent. Note that this message should
* not be sent if there are physical actions and the earliest event on the event
* queue has a tag that is ahead of physical time (or the queue is empty).
* In that case, send TAN instead.
*/
RTIMessageTypes[RTIMessageTypes["MSG_TYPE_NEXT_EVENT_TAG"] = 6] = "MSG_TYPE_NEXT_EVENT_TAG";
/**
* Byte identifying a time advance grant (TAG) sent by the RTI to a federate
* in centralized coordination. This message is a promise by the RTI to the federate
* that no later message sent to the federate will have a tag earlier than or
* equal to the tag carried by this TAG message.
* The next eight bytes will be the timestamp.
* The next four bytes will be the microstep.
*/
RTIMessageTypes[RTIMessageTypes["MSG_TYPE_TAG_ADVANCE_GRANT"] = 7] = "MSG_TYPE_TAG_ADVANCE_GRANT";
/**
* Byte identifying a provisional time advance grant (PTAG) sent by the RTI to a federate
* in centralized coordination. This message is a promise by the RTI to the federate
* that no later message sent to the federate will have a tag earlier than the tag
* carried by this PTAG message.
* The next eight bytes will be the timestamp.
* The next four bytes will be the microstep.
*/
RTIMessageTypes[RTIMessageTypes["MSG_TYPE_PROVISIONAL_TAG_ADVANCE_GRANT"] = 8] = "MSG_TYPE_PROVISIONAL_TAG_ADVANCE_GRANT";
/**
* Byte identifying a logical tag complete (LTC) message sent by a federate
* to the RTI.
* The next eight bytes will be the timestep of the completed tag.
* The next four bytes will be the microsteps of the completed tag.
*/
RTIMessageTypes[RTIMessageTypes["MSG_TYPE_LOGICAL_TAG_COMPLETE"] = 9] = "MSG_TYPE_LOGICAL_TAG_COMPLETE";
// For more information on the algorithm for stop request protocol, please see following link:
// https://github.com/lf-lang/lingua-franca/wiki/Federated-Execution-Protocol#overview-of-the-algorithm
/**
* Byte identifying a stop request. This message is first sent to the RTI by a federate
* that would like to stop execution at the specified tag. The RTI will forward
* the MSG_TYPE_STOP_REQUEST to all other federates. Those federates will either agree to
* the requested tag or propose a larger tag. The RTI will collect all proposed
* tags and broadcast the largest of those to all federates. All federates
* will then be expected to stop at the granted tag.
*
* The next 8 bytes will be the timestamp.
* The next 4 bytes will be the microstep.
*
* NOTE: The RTI may reply with a larger tag than the one specified in this message.
* It has to be that way because if any federate can send a MSG_TYPE_STOP_REQUEST message
* that specifies the stop time on all other federates, then every federate
* depends on every other federate and time cannot be advanced.
* Hence, the actual stop time may be nondeterministic.
*
* If, on the other hand, the federate requesting the stop is upstream of every
* other federate, then it should be possible to respect its requested stop tag.
*/
RTIMessageTypes[RTIMessageTypes["MSG_TYPE_STOP_REQUEST"] = 10] = "MSG_TYPE_STOP_REQUEST";
/**
* Byte indicating a federate's reply to a MSG_TYPE_STOP_REQUEST that was sent
* by the RTI. The payload is a proposed stop tag that is at least as large
* as the one sent to the federate in a MSG_TYPE_STOP_REQUEST message.
*
* The next 8 bytes will be the timestamp.
* The next 4 bytes will be the microstep.
*/
RTIMessageTypes[RTIMessageTypes["MSG_TYPE_STOP_REQUEST_REPLY"] = 11] = "MSG_TYPE_STOP_REQUEST_REPLY";
/**
* Byte sent by the RTI indicating that the stop request from some federate
* has been granted. The payload is the tag at which all federates have
* agreed that they can stop.
* The next 8 bytes will be the time at which the federates will stop.
* The next 4 bytes will be the microstep at which the federates will stop.
*/
RTIMessageTypes[RTIMessageTypes["MSG_TYPE_STOP_GRANTED"] = 12] = "MSG_TYPE_STOP_GRANTED";
/**
* A port absent message, informing the receiver that a given port
* will not have event for the current logical time.
*
* The next 2 bytes are the port id.
* The next 2 bytes will be the federate id of the destination federate.
* This is needed for the centralized coordination so that the RTI knows where
* to forward the message.
* The next 8 bytes are the intended time of the absent message
* The next 4 bytes are the intended microstep of the absent message
*/
RTIMessageTypes[RTIMessageTypes["MSG_TYPE_PORT_ABSENT"] = 23] = "MSG_TYPE_PORT_ABSENT";
/**
* A message that informs the RTI about connections between this federate and
* other federates where messages are routed through the RTI. Currently, this
* only includes logical connections when the coordination is centralized. This
* information is needed for the RTI to perform the centralized coordination.
*
* @note Only information about the immediate neighbors is required. The RTI can
* transitively obtain the structure of the federation based on each federate's
* immediate neighbor information.
*
* The next 4 bytes are the number of upstream federates.
* The next 4 bytes are the number of downstream federates.
*
* Depending on the first four bytes, the next bytes are pairs of (fed ID (2
* bytes), delay (8 bytes)) for this federate's connection to upstream federates
* (by direct connection). The delay is the minimum "after" delay of all
* connections from the upstream federate.
*
* Depending on the second four bytes, the next bytes are fed IDs (2
* bytes each), of this federate's downstream federates (by direct connection).
*
* @note The upstream and downstream connections are transmitted on the same
* message to prevent (at least to some degree) the scenario where the RTI has
* information about one, but not the other (which is a critical error).
*/
RTIMessageTypes[RTIMessageTypes["MSG_TYPE_NEIGHBOR_STRUCTURE"] = 24] = "MSG_TYPE_NEIGHBOR_STRUCTURE";
/**
* Byte identifying a downstream next event tag (DNET) message sent
* from the RTI in centralized coordination.
* The next eight bytes will be the timestamp.
* The next four bytes will be the microstep.
* This signal from the RTI tells the destination federate the latest tag that
* the federate can safely skip sending a next event tag (NET) signal.
* In other words, the federate doesn't have to send NET signals with tags
* earlier than or equal to this tag unless it cannot advance to its next event tag.
*/
RTIMessageTypes[RTIMessageTypes["MSG_TYPE_DOWNSTREAM_NEXT_EVENT_TAG"] = 26] = "MSG_TYPE_DOWNSTREAM_NEXT_EVENT_TAG";
/**
* Byte identifying an acknowledgment of the previously received MSG_TYPE_FED_IDS message
* sent by the RTI to the federate
* with a payload indicating the UDP port to use for clock synchronization.
* The next four bytes will be the port number for the UDP server, or
* 0 or USHRT_MAX if there is no UDP server. 0 means that initial clock synchronization
* is enabled, whereas USHRT_MAX mean that no synchronization should be performed at all.
*/
RTIMessageTypes[RTIMessageTypes["MSG_TYPE_UDP_PORT"] = 254] = "MSG_TYPE_UDP_PORT";
/**
* Byte identifying an acknowledgment of the previously received message.
* This message carries no payload.
*/
RTIMessageTypes[RTIMessageTypes["MSG_TYPE_ACK"] = 255] = "MSG_TYPE_ACK";
})(RTIMessageTypes || (RTIMessageTypes = {}));
/**
* Custom type guard for a NodeJsCodedError
* @param e The Error to be tested as being a NodeJSCodedError
*/
function isANodeJSCodedError(e) {
return typeof e.code === "string";
}
class NetworkReactor extends internal_1.Reactor {
// TPO level of this NetworkReactor
tpoLevel;
constructor(parent, tpoLevel) {
super(parent);
this.tpoLevel = tpoLevel;
}
/**
* Getter for the TPO level of this NetworkReactor.
*/
getTpoLevel() {
return this.tpoLevel;
}
/**
* This function returns this network reactor's own reactions.
* The edges of those reactions (e.g. port absent reactions, port present reactions, ...)
* should be added to the dependency graph according to TPO levels.
* @returns
*/
getReactions() {
return this._getReactions();
}
}
/**
* A network sender is a reactor containing a portAbsentReaction.
*/
class NetworkSender extends NetworkReactor {
/**
* The last reaction of a NetworkSender reactor is the "port absent" reaction.
* @returns the "port absent" of this reactor
*/
getPortAbsentReaction() {
return this._getLastReactionOrMutation();
}
}
exports.NetworkSender = NetworkSender;
/**
* A network receiver is a reactor handling a network input.
*/
class NetworkReceiver extends NetworkReactor {
/**
* A schedulable action of this NetworkReceiver's network input.
*/
networkInputSchedAction;
/**
* The information of origin of this NetworkReceiver's network input action.
*/
networkInputActionOrigin;
/**
* Last known status of the port, either via a timed message, a port absent,
* or a TAG from the RTI.
*/
lastKnownStatusTag;
constructor(parent, tpoLevel) {
super(parent, tpoLevel);
// this.portStatus = PortStatus.UNKNOWN;
this.lastKnownStatusTag = new internal_1.Tag(internal_1.TimeValue.never());
}
/**
* Register a federate port's action with the network receiver.
* @param networkInputAction The federate port's action for registration.
*/
registerNetworkInputAction(networkInputAction) {
this.networkInputSchedAction = networkInputAction.asSchedulable(this._getKey(networkInputAction));
this.networkInputActionOrigin = networkInputAction.origin;
}
getNetworkInputActionOrigin() {
return this.networkInputActionOrigin;
}
/**
* Handle a timed message being received from the RTI.
* This function is for NetworkReceiver reactors.
* @param portID The destination port ID of the message.
* @param value The payload of the message.
*/
handleMessage(value) {
// Schedule this federate port's action.
// This message is untimed, so schedule it immediately.
if (this.networkInputSchedAction !== undefined) {
this.networkInputSchedAction.schedule(0, value);
}
}
/**
* Handle a timed message being received from the RTI.
* @param portID The destination port ID of the message.
* @param value The payload of the message.
*/
handleTimedMessage(value, intendedTag) {
// Schedule this federate port's action.
/**
* Definitions:
* Ts = timestamp of message at the sending end.
* A = after value on connection
* Tr = timestamp assigned to the message at the receiving end.
* r = physical time at the receiving end when message is received (when schedule() is called).
* R = logical time at the receiving end when the message is received (when schedule() is called).
* We assume that always R <= r.
* Logical connection, centralized control: Tr = Ts + A
* Logical connection, decentralized control: Tr = Ts + A or, if R > Ts + A,
* ERROR triggers at a logical time >= R
* Physical connection, centralized or decentralized control: Tr = max(r, R + A)
*
*/
// FIXME: implement decentralized control.
if (this.networkInputSchedAction !== undefined) {
if (this.networkInputActionOrigin === internal_1.Origin.logical) {
this.networkInputSchedAction.schedule(0, value, intendedTag);
}
else if (this.networkInputActionOrigin === internal_1.Origin.physical) {
// The schedule function for physical actions implements
// Tr = max(r, R + A)
this.networkInputSchedAction.schedule(0, value);
}
}
}
}
exports.NetworkReceiver = NetworkReceiver;
/**
* An RTIClient is used within a federate to abstract the socket
* connection to the RTI and the RTI's binary protocol over the socket.
* RTIClient exposes functions for federate-level operations like
* establishing a connection to the RTI or sending a message.
* RTIClient is an EventEmitter, and asynchronously emits events for:
* 'startTime', 'connected', 'message', 'timedMessage', and
* 'timeAdvanceGrant'. The federatedApp is responsible for handling the
* events to ensure a correct execution.
*/
class RTIClient extends events_1.EventEmitter {
// ID of federation that this federate will join.
federationID;
// ID of this federate.
id;
// The socket descriptor for communicating with this federate.
socket = null;
/**
* Constructor for an RTIClient
* @param id The ID of the federate this client communicates
* on behalf of.
*/
constructor(federationID, id) {
super();
this.federationID = federationID;
this.id = id;
}
// If the last data sent to handleSocketData contained an incomplete
// or chunked message, that data is copied over to chunkedBuffer so it can
// be saved until the next time handleSocketData is called. If no data has been
// saved, chunkedBuffer is null.
chunkedBuffer = null;
// The number of attempts made by this federate to connect to the RTI.
connectionAttempts = 0;
/**
* Create a socket connection to the RTI and register this federate's
* ID with the RTI. If unable to make a connection, retry.
* @param port The RTI's remote port number.
* @param host The RTI's remote host name.
*/
connectToRTI(port, host) {
// Create an IPv4 socket for TCP (not UDP) communication over IP (0)
const options = {
port,
family: 4, // IPv4,
localAddress: "0.0.0.0", // All interfaces, 0.0.0.0.
host
};
this.socket = (0, net_1.createConnection)(options, () => {
// This function is a listener to the 'connection' socket
// event.
// Only set up an event handler for close if the connection is
// created. Otherwise this handler will go off on every reconnection
// attempt.
this.socket?.on("close", () => {
internal_1.Log.info(this, () => {
return "RTI socket has closed.";
});
});
internal_1.Log.debug(this, () => {
return `Federate ID: ${this.id} connected to RTI.`;
});
// Immediately send a federate ID message after connecting.
const buffer = Buffer.alloc(4);
buffer.writeUInt8(RTIMessageTypes.MSG_TYPE_FED_IDS, 0);
buffer.writeUInt16LE(this.id, 1);
buffer.writeUInt8(this.federationID.length, 3);
try {
internal_1.Log.debug(this, () => {
return `Sending a FED ID message (ID: ${this.federationID}) to the RTI.`;
});
this.socket?.write(buffer);
this.socket?.write(this.federationID);
}
catch (e) {
internal_1.Log.error(this, () => {
return `${e}`;
});
}
// Finally, emit a connected event.
this.emit("connected");
});
this.socket?.on("data", this.handleSocketData.bind(this));
// If the socket reports a connection refused error,
// suppress the message and try to reconnect.
this.socket?.on("error", (err) => {
if (isANodeJSCodedError(err) && err.code === "ECONNREFUSED") {
internal_1.Log.info(this, () => {
return `Failed to connect to RTI with error: ${err}.`;
});
if (this.connectionAttempts < exports.CONNECT_NUM_RETRIES) {
internal_1.Log.info(this, () => {
return `Retrying RTI connection in ${String(exports.CONNECT_RETRY_INTERVAL)}.`;
});
this.connectionAttempts++;
const a = new internal_1.Alarm();
a.set(this.connectToRTI.bind(this, port, host), exports.CONNECT_RETRY_INTERVAL);
}
else {
internal_1.Log.error(this, () => {
return `Could not connect to RTI after ${exports.CONNECT_NUM_RETRIES} attempts.`;
});
}
}
else {
internal_1.Log.error(this, () => {
return err.toString();
});
}
});
}
/**
* Close the RTI Client's socket connection to the RTI.
*/
closeRTIConnection() {
internal_1.Log.debug(this, () => {
return "Closing RTI connection by ending and unrefing socket.";
});
this.socket?.end();
this.socket?.unref(); // Allow the program to exit
}
sendNeighborStructure(upstreamFedIDs, upstreamFedDelays, downstreamFedIDs) {
const msg = Buffer.alloc(9 + upstreamFedIDs.length * 10 + downstreamFedIDs.length * 2);
msg.writeUInt8(RTIMessageTypes.MSG_TYPE_NEIGHBOR_STRUCTURE);
msg.writeUInt32LE(upstreamFedIDs.length, 1);
msg.writeUInt32LE(downstreamFedIDs.length, 5);
let bufferIndex = 9;
for (let i = 0; i < upstreamFedIDs.length; i++) {
msg.writeUInt16LE(upstreamFedIDs[i], bufferIndex);
const delay = upstreamFedDelays[i].toBinary();
delay.copy(msg, bufferIndex + 2);
bufferIndex += 10;
}
for (let i = 0; i < downstreamFedIDs.length; i++) {
msg.writeUInt16LE(downstreamFedIDs[i], bufferIndex);
bufferIndex += 2;
}
try {
this.socket?.write(msg);
}
catch (e) {
internal_1.Log.error(this, () => {
return `${e}`;
});
}
}
sendUDPPortNumToRTI(udpPort) {
const msg = Buffer.alloc(3);
msg.writeUInt8(RTIMessageTypes.MSG_TYPE_UDP_PORT, 0);
msg.writeUInt16BE(udpPort, 1);
try {
this.socket?.write(msg);
}
catch (e) {
internal_1.Log.error(this, () => {
return `${e}`;
});
}
}
/**
* Send the specified TimeValue to the RTI and set up
* a handler for the response.
* The specified TimeValue should be current physical time of the
* federate, and the response will be the designated start time for
* the federate. May only be called after the federate emits a
* 'connected' event. When the RTI responds, this federate will
* emit a 'startTime' event.
* @param myPhysicalTime The physical time at this federate.
*/
requestStartTimeFromRTI(myPhysicalTime) {
const msg = Buffer.alloc(9);
msg.writeUInt8(RTIMessageTypes.MSG_TYPE_TIMESTAMP, 0);
const time = myPhysicalTime.toBinary();
time.copy(msg, 1);
try {
internal_1.Log.debug(this, () => {
return `Sending RTI start time: ${myPhysicalTime}`;
});
this.socket?.write(msg);
}
catch (e) {
internal_1.Log.error(this, () => {
return `${e}`;
});
}
}
/**
* Send an RTI (untimed) message to a remote federate.
* @param data The message encoded as a Buffer. The data may be
* arbitrary length.
* @param destFederateID The federate ID of the federate
* to which this message should be sent.
* @param destPortID The port ID for the port on the destination
* federate to which this message should be sent.
*/
sendRTIMessage(data, destFederateID, destPortID) {
const value = Buffer.from(JSON.stringify(data), "utf-8");
const msg = Buffer.alloc(value.length + 9);
msg.writeUInt8(RTIMessageTypes.MSG_TYPE_MESSAGE, 0);
msg.writeUInt16LE(destPortID, 1);
msg.writeUInt16LE(destFederateID, 3);
msg.writeUInt32LE(value.length, 5);
value.copy(msg, 9); // Copy data into the message
try {
internal_1.Log.debug(this, () => {
return ("Sending RTI (untimed) message to " +
`federate ID: ${destFederateID} and port ID: ${destPortID}.`);
});
this.socket?.write(msg);
}
catch (e) {
internal_1.Log.error(this, () => {
return `${e}`;
});
}
}
/**
* Send an RTI timed message to a remote federate.
* @param data The message encoded as a Buffer. The data may be
* arbitrary length.
* @param destFederateID The federate ID of the federate
* to which this message should be sent.
* @param destPortID The port ID for the port on the destination
* federate to which this message should be sent.
* @param time The time of the message encoded as a 64 bit little endian
* unsigned integer in a Buffer.
*/
sendRTITimedMessage(data, destFederateID, destPortID, time) {
const value = Buffer.from(JSON.stringify(data), "utf-8");
const msg = Buffer.alloc(value.length + 21);
msg.writeUInt8(RTIMessageTypes.MSG_TYPE_TAGGED_MESSAGE, 0);
msg.writeUInt16LE(destPortID, 1);
msg.writeUInt16LE(destFederateID, 3);
msg.writeUInt32LE(value.length, 5);
time.copy(msg, 9); // Copy the current time into the message
// FIXME: Add microstep properly.
value.copy(msg, 21); // Copy data into the message
try {
internal_1.Log.debug(this, () => {
return ("Sending RTI (timed) message to " +
`federate ID: ${destFederateID}, port ID: ${destPortID} ` +
`, time: ${time.toString("hex")}.`);
});
this.socket?.write(msg);
}
catch (e) {
internal_1.Log.error(this, () => {
return `${e}`;
});
}
}
/**
* Send the RTI a logical time complete message. This should be
* called when the federate has completed all events for a given
* logical time.
* @param completeTime The logical time that is complete. The time
* should be encoded as a 64 bit little endian unsigned integer in
* a Buffer.
*/
sendRTILogicalTimeComplete(completeTime) {
const msg = Buffer.alloc(13);
msg.writeUInt8(RTIMessageTypes.MSG_TYPE_LOGICAL_TAG_COMPLETE, 0);
completeTime.copy(msg, 1);
// FIXME: Add microstep properly.
try {
internal_1.Log.debug(this, () => {
return ("Sending RTI logical time complete: " + completeTime.toString("hex"));
});
this.socket?.write(msg);
}
catch (e) {
internal_1.Log.error(this, () => {
return `${e}`;
});
}
}
/**
* Send the RTI a resign message. This should be called when
* the federate is shutting down.
*/
sendRTIResign() {
const msg = Buffer.alloc(1);
msg.writeUInt8(RTIMessageTypes.MSG_TYPE_RESIGN, 0);
try {
internal_1.Log.debug(this, () => {
return "Sending RTI resign.";
});
this.socket?.write(msg);
}
catch (e) {
internal_1.Log.error(this, () => {
return `${e}`;
});
}
}
/**
* Send the RTI a next event tag message. This should be called when
* the federate would like to advance logical time, but has not yet
* received a sufficiently large time advance grant.
* @param nextTag The time of the message encoded as a 64 bit unsigned
* integer in a Buffer.
*/
sendRTINextEventTag(nextTag) {
const msg = Buffer.alloc(13);
msg.writeUInt8(RTIMessageTypes.MSG_TYPE_NEXT_EVENT_TAG, 0);
nextTag.copy(msg, 1);
try {
internal_1.Log.debug(this, () => {
return "Sending RTI Next Event Time.";
});
this.socket?.write(msg);
}
catch (e) {
internal_1.Log.error(this, () => {
return `${e}`;
});
}
}
/**
* Send the RTI a stop request message.
*/
sendRTIStopRequest(stopTag) {
const msg = Buffer.alloc(13);
msg.writeUInt8(RTIMessageTypes.MSG_TYPE_STOP_REQUEST, 0);
stopTag.copy(msg, 1);
try {
internal_1.Log.debug(this, () => {
return "Sending RTI Stop Request.";
});
this.socket?.write(msg);
}
catch (e) {
internal_1.Log.error(this, () => {
return `${e}`;
});
}
}
/**
* Send the RTI a stop request reply message.
*/
sendRTIStopRequestReply(stopTag) {
const msg = Buffer.alloc(13);
msg.writeUInt8(RTIMessageTypes.MSG_TYPE_STOP_REQUEST_REPLY, 0);
stopTag.copy(msg, 1);
try {
internal_1.Log.debug(this, () => {
return "Sending RTI Stop Request Reply.";
});
this.socket?.write(msg);
}
catch (e) {
internal_1.Log.error(this, () => {
return `${e}`;
});
}
}
/**
* Send a port absent message to federate with fed_ID, informing the
* remote federate that the current federate will not produce an event
* on this network port at the current logical time.
*
* @param intendedTag The last tag that the federate can assure that this port is absent
* @param federateID The fed ID of the receiving federate.
* @param federatePortID The ID of the receiving port.
*/
sendRTIPortAbsent(federateID, federatePortID, intendedTag) {
const msg = Buffer.alloc(17);
msg.writeUInt8(RTIMessageTypes.MSG_TYPE_PORT_ABSENT, 0);
msg.writeUInt16LE(federatePortID, 1);
msg.writeUInt16LE(federateID, 3);
intendedTag.toBinary().copy(msg, 5);
try {
internal_1.Log.debug(this, () => {
return `Sending RTI Port Absent message, tag: ${intendedTag}`;
});
this.socket?.write(msg);
}
catch (e) {
internal_1.Log.error(this, () => {
return `${e}`;
});
}
}
/**
* The handler for the socket's data event.
* The data Buffer given to the handler may contain 0 or more complete messages.
* Iterate through the complete messages, and if the last message is incomplete
* save it as this.chunkedBuffer so it can be prepended onto the
* data when handleSocketData is called again.
* @param assembledData The Buffer of data received by the socket. It may
* contain 0 or more complete messages.
*/
handleSocketData(data) {
if (data.length < 1) {
throw new Error("Received a message from the RTI with 0 length.");
}
// Used to track the current location within the data Buffer.
let bufferIndex = 0;
// Append the new data to leftover data from chunkedBuffer (if any)
// The result is assembledData.
let assembledData;
if (this.chunkedBuffer != null) {
assembledData = Buffer.alloc(this.chunkedBuffer.length + data.length);
this.chunkedBuffer.copy(assembledData, 0, 0, this.chunkedBuffer.length);
data.copy(assembledData, this.chunkedBuffer.length);
this.chunkedBuffer = null;
}
else {
assembledData = data;
}
internal_1.Log.debug(this, () => {
return `Assembled data is: ${assembledData.toString("hex")}`;
});
while (bufferIndex < assembledData.length) {
const messageTypeByte = assembledData[bufferIndex];
switch (messageTypeByte) {
case RTIMessageTypes.MSG_TYPE_FED_IDS: {
// MessageType: 1 byte.
// Federate ID: 2 bytes long.
// Should never be received by a federate.
internal_1.Log.error(this, () => {
return "Received MSG_TYPE_FED_IDS message from the RTI.";
});
throw new Error("Received a MSG_TYPE_FED_IDS message from the RTI. " +
"MSG_TYPE_FED_IDS messages may only be sent by federates");
}
case RTIMessageTypes.MSG_TYPE_TIMESTAMP: {
// MessageType: 1 byte.
// Timestamp: 8 bytes.
const incomplete = assembledData.length < 9 + bufferIndex;
if (incomplete) {
this.chunkedBuffer = Buffer.alloc(assembledData.length - bufferIndex);
assembledData.copy(this.chunkedBuffer, 0, bufferIndex);
}
else {
const timeBuffer = Buffer.alloc(8);
assembledData.copy(timeBuffer, 0, bufferIndex + 1, bufferIndex + 9);
const startTime = internal_1.TimeValue.fromBinary(timeBuffer);
internal_1.Log.debug(this, () => {
return ("Received MSG_TYPE_TIMESTAMP buffer from the RTI " +
`with startTime: ${timeBuffer.toString("hex")}`);
});
internal_1.Log.debug(this, () => {
return ("Received MSG_TYPE_TIMESTAMP message from the RTI " +
`with startTime: ${startTime}`);
});
this.emit("startTime", startTime);
}
bufferIndex += 9;
break;
}
case RTIMessageTypes.MSG_TYPE_MESSAGE: {
// MessageType: 1 byte.
// Message: The next two bytes will be the ID of the destination port
// The next two bytes are the destination federate ID (which can be ignored).
// The next four bytes after that will be the length of the message
// The remaining bytes are the message.
const incomplete = assembledData.length < 9 + bufferIndex;
if (incomplete) {
this.chunkedBuffer = Buffer.alloc(assembledData.length - bufferIndex);
assembledData.copy(this.chunkedBuffer, 0, bufferIndex);
bufferIndex += 9;
}
else {
const destPortID = assembledData.readUInt16LE(bufferIndex + 1);
const messageLength = assembledData.readUInt32LE(bufferIndex + 5);
// Once the message length is parsed, we can determine whether
// the body of the message has been chunked.
const isChunked = messageLength > assembledData.length - (bufferIndex + 9);
if (isChunked) {
// Copy the unprocessed remainder of assembledData into chunkedBuffer
this.chunkedBuffer = Buffer.alloc(assembledData.length - bufferIndex);
assembledData.copy(this.chunkedBuffer, 0, bufferIndex);
}
else {
// Finish processing the complete message.
const messageBuffer = Buffer.alloc(messageLength);
assembledData.copy(messageBuffer, 0, bufferIndex + 9, bufferIndex + 9 + messageLength);
this.emit("message", destPortID, messageBuffer);
}
bufferIndex += messageLength + 9;
}
break;
}
case RTIMessageTypes.MSG_TYPE_TAGGED_MESSAGE: {
// MessageType: 1 byte.
// The next two bytes will be the ID of the destination port.
// The next two bytes are the destination federate ID.
// The next four bytes after that will be the length of the message
// The next eight bytes will be the timestamp.
// The next four bytes will be the microstep of the message.
// The remaining bytes are the message.
const incomplete = assembledData.length < 21 + bufferIndex;
if (incomplete) {
this.chunkedBuffer = Buffer.alloc(assembledData.length - bufferIndex);
assembledData.copy(this.chunkedBuffer, 0, bufferIndex);
bufferIndex += 21;
}
else {
const destPortID = assembledData.readUInt16LE(bufferIndex + 1);
const messageLength = assembledData.readUInt32LE(bufferIndex + 5);
const tagBuffer = Buffer.alloc(12);
assembledData.copy(tagBuffer, 0, bufferIndex + 9, bufferIndex + 21);
const tag = internal_1.Tag.fromBinary(tagBuffer);
internal_1.Log.debug(this, () => {
return `Received an RTI MSG_TYPE_TAGGED_MESSAGE: Tag Buffer: ${String(tag)}`;
});
// FIXME: Process microstep properly.
const isChunked = messageLength > assembledData.length - (bufferIndex + 21);
if (isChunked) {
// Copy the unprocessed remainder of assembledData into chunkedBuffer
this.chunkedBuffer = Buffer.alloc(assembledData.length - bufferIndex);
assembledData.copy(this.chunkedBuffer, 0, bufferIndex);
}
else {
// Finish processing the complete message.
const messageBuffer = Buffer.alloc(messageLength);
assembledData.copy(messageBuffer, 0, bufferIndex + 21, bufferIndex + 21 + messageLength);
this.emit("timedMessage", destPortID, messageBuffer, tag);
}
bufferIndex += messageLength + 21;
break;
}
// TODO (axmmisaka): was this intended to be a fallthrough?
break;
}
// FIXME: It's unclear what should happen if a federate gets this
// message.
case RTIMessageTypes.MSG_TYPE_RESIGN: {
// MessageType: 1 byte.
internal_1.Log.debug(this, () => {
return "Received an RTI MSG_TYPE_RESIGN.";
});
internal_1.Log.error(this, () => {
return ("FIXME: No functionality has " +
"been implemented yet for a federate receiving a MSG_TYPE_RESIGN message from " +
"the RTI");
});
bufferIndex += 1;
break;
}
case RTIMessageTypes.MSG_TYPE_NEXT_EVENT_TAG: {
// MessageType: 1 byte.
// Timestamp: 8 bytes.
// Microstep: 4 bytes.
internal_1.Log.error(this, () => {
return ("Received an RTI MSG_TYPE_NEXT_EVENT_TAG. This message type " +
"should not be received by a federate");
});
bufferIndex += 13;
break;
}
case RTIMessageTypes.MSG_TYPE_TAG_ADVANCE_GRANT: {
// MessageType: 1 byte.
// Timestamp: 8 bytes.
// Microstep: 4 bytes.
internal_1.Log.debug(this, () => {
return "Received an RTI MSG_TYPE_TAG_ADVANCE_GRANT";
});
const tagBuffer = Buffer.alloc(12);
assembledData.copy(tagBuffer, 0, bufferIndex + 1, bufferIndex + 13);
const tag = internal_1.Tag.fromBinary(tagBuffer);
this.emit("timeAdvanceGrant", tag);
bufferIndex += 13;
break;
}
case RTIMessageTypes.MSG_TYPE_PROVISIONAL_TAG_ADVANCE_GRANT: {
internal_1.Log.debug(this, () => {
return "Received an RTI MSG_TYPE_PROVISIONAL_TAG_ADVANCE_GRANT";
});
const tagBuffer = Buffer.alloc(12);
assembledData.copy(tagBuffer, 0, bufferIndex + 1, bufferIndex + 13);
const tag = internal_1.Tag.fromBinary(tagBuffer);
internal_1.Log.debug(this, () => {
return `PTAG value: ${tag}`;
});
this.emit("provisionalTimeAdvanceGrant", tag);
bufferIndex += 13;
break;
}
case RTIMessageTypes.MSG_TYPE_LOGICAL_TAG_COMPLETE: {
// Logial Time Complete: The next eight bytes will be the timestamp.
internal_1.Log.error(this, () => {
return ("Received an RTI MSG_TYPE_LOGICAL_TAG_COMPLETE. This message type " +
"should not be received by a federate");
});
bufferIndex += 13;
break;
}
case RTIMessageTypes.MSG_TYPE_STOP_REQUEST: {
// The next 8 bytes will be the timestamp.
// The next 4 bytes will be the microstep.
internal_1.Log.debug(this, () => {
return "Received an RTI MSG_TYPE_STOP_REQUEST";
});
const tagBuffer = Buffer.alloc(12);
assembledData.copy(tagBuffer, 0, bufferIndex + 1, bufferIndex + 13);
const tag = internal_1.Tag.fromBinary(tagBuffer);
this.emit("stopRequest", tag);
bufferIndex += 13;
break;
}
case RTIMessageTypes.MSG_TYPE_STOP_GRANTED: {
// The next 8 bytes will be the time at which the federates will stop.
// The next 4 bytes will be the microstep at which the federates will stop.
internal_1.Log.debug(this, () => {
return "Received an RTI MSG_TYPE_STOP_GRANTED";
});
const tagBuffer = Buffer.alloc(12);
assembledData.copy(tagBuffer, 0, bufferIndex + 1, bufferIndex + 13);
const tag = internal_1.Tag.fromBinary(tagBuffer);
this.emit("stopRequestGranted", tag);
bufferIndex += 13;
break;
}
case RTIMessageTypes.MSG_TYPE_PORT_ABSENT: {
// The next 2 bytes are the port id.
// The next 2 bytes will be the federate id of the destination federate.
// The next 8 bytes are the intended time of the absent message
// The next 4 bytes are the intended microstep of the absent message
const portID = assembledData.readUInt16LE(bufferIndex + 1);
// The next part of the message is the federate_id, but we don't need it.
// let federateID = assembledData.readUInt16LE(bufferIndex + 3);
const tagBuffer = Buffer.alloc(12);
assembledData.copy(tagBuffer, 0, bufferIndex + 5, bufferIndex + 17);
const intendedTag = internal_1.Tag.fromBinary(tagBuffer);
internal_1.Log.debug(this, () => {
return `Handling port absent for tag ${String(intendedTag)} for port ${portID}.`;
});
this.emit("portAbsent", portID, intendedTag);
bufferIndex += 17;
break;
}
case RTIMessageTypes.MSG_TYPE_DOWNSTREAM_NEXT_EVENT_TAG: {
// The next eight bytes are the timestamp.
// The next four bytes are the microstep.
const tagBuffer = Buffer.alloc(12);
assembledData.copy(tagBuffer, 0, bufferIndex + 1, bufferIndex + 13);
const tag = internal_1.Tag.fromBinary(tagBuffer);
internal_1.Log.debug(this, () => {
return `Downstream next event tag (DNET) received from RTI for ${tag}.
DNET is not yet supported in TypeScript. Ignored.`;
});
bufferIndex += 13;
break;
}
case RTIMessageTypes.MSG_TYPE_ACK: {
internal_1.Log.debug(this, () => {
return "Received an RTI MSG_TYPE_ACK";
});
bufferIndex += 1;
break;
}
case RTIMessageTypes.MSG_TYPE_REJECT: {
const rejectionReason = assembledData.readUInt8(bufferIndex + 1);
internal_1.Log.error(this, () => {
return `Received an RTI MSG_TYPE_REJECT. Rejection reason: ${rejectionReason}`;
});
bufferIndex += 2;
break;
}
default: {
throw new Error(`Unrecognized message type in message from the RTI: ${assembledData.toString("hex")}.`);
}
}
}
internal_1.Log.debug(this, () => {
return "exiting handleSocketData";
});
}
}
/**
* Enum type to store the state of stop request.
* */
var StopRequestState;
(function (StopRequestState) {
StopRequestState[StopRequestState["NOT_SENT"] = 0] = "NOT_SENT";
StopRequestState[StopRequestState["SENT"] = 1] = "SENT";
StopRequestState[StopRequestState["GRANTED"] = 2] = "GRANTED";
})(StopRequestState || (StopRequestState = {}));
/**
* Class for storing stop request-related information
* including the current state and the tag associated with the stop requested or stop granted.
*/
class StopRequestInfo {
constructor(state, tag) {
this.state = state;
this.tag = tag;
}
state;
tag;
}
/**
* A federated app is an app containing federates as its top level reactors.
* A federate is a component in a distributed reactor execution in which
* reactors from the same (abstract) model run in distinct networked processes.
* A federated app contains the federates designated to run in a particular
* process. The federated program is coordinated by the RTI (Run Time Infrastructure).
* Like an app, a federated app is the top level reactor for a particular process,
* but a federated app must follow the direction of the RTI for beginning execution,
* advancing time, and exchanging messages with other federates.
*
* Note: There is no special class for a federate. A federate is the name for a top
* level reactor of a federated app.
*/
class FederatedApp extends internal_1.App {
/**