UNPKG

@lf-lang/reactor-ts

Version:

A reactor-oriented programming framework in TypeScript

1,075 lines (1,072 loc) 84.1 kB
"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 { /**