-
+
gateways/js/src/fjage.js
@@ -3755,7 +3729,7 @@
Returns a response message received by the gateway. This method returns a {Promise} which resolves when
a response is received or if no response is received after the timeout.
-
+
@@ -3772,10 +3746,8 @@
-
filter (function ?)
- original message to which a response is expected, or a MessageClass of the type
-of message to match, or a closure to use to match against the message
-
+
filter (any)
+
@@ -3798,7 +3770,7 @@
Returns
- Promise <Message ?>
:
+ Promise <(Message | void)>
:
received response message, null on timeout
@@ -3837,7 +3809,7 @@
-
+
gateways/js/src/fjage.js
@@ -3911,7 +3883,7 @@
-
+
gateways/js/src/fjage.js
@@ -3961,7 +3933,7 @@
-
+
gateways/js/src/fjage.js
@@ -4028,7 +4000,7 @@
-
+
gateways/js/src/fjage.js
@@ -4037,7 +4009,7 @@
Creates a unqualified message class based on a fully qualified name.
-
+
new MessageClass(name:
string , parent: any)
@@ -4063,10 +4035,9 @@
- parent (class
+ parent (any
= Message
)
- class of the parent MessageClass to inherit from
-
+
@@ -4077,12 +4048,17 @@
-
- Returns
- function
:
- constructor for the unqualified message class
+
+
+
+
+
+ Example
+
+ const ParameterReq = MessageClass ('org.arl.fjage.param.ParameterReq' );
+let pReq = new ParameterReq ()
@@ -4091,12 +4067,168 @@
- Example
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ParameterReq.Entry
+
+
+ Type:
+ Object
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Properties
+
-
const ParameterReq = MessageClass ('org.arl.fjage.param.ParameterReq' );
-let pReq = new ParameterReq ()
+
+
param (string )
+ : parameter name
+
+
+
+
+
+
value (Object )
+ : parameter value
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ A message that requests one or more parameters of an agent.
+
+ ParameterReq
+
+
+ Type:
+ Message
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Properties
+
+
+
+
param (string )
+ : parameters name to be get/set if only a single parameter is to be get/set
+
+
+
+
+
+
value (Object )
+ : parameters value to be set if only a single parameter is to be set
+
+
+
+
+
+
+
+
index (number ?)
+ : index of parameter(s) to be set
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gateways/js/dist/esm/fjage.js b/gateways/js/dist/esm/fjage.js
index f1871cc8..e3d6625b 100644
--- a/gateways/js/dist/esm/fjage.js
+++ b/gateways/js/dist/esm/fjage.js
@@ -37,29 +37,31 @@ var createConnection;
* @class
* @ignore
*/
-class TCPconnector {
+class TCPConnector {
/**
* Create an TCPConnector to connect to a fjage master over TCP
* @param {Object} opts
- * @param {string} opts.hostname - hostname/ip address of the master container to connect to
- * @param {string} opts.port - port number of the master container to connect to
- * @param {boolean} opts.keepAlive - try to reconnect if the connection is lost
+ * @param {string} [opts.hostname='localhost'] - hostname/ip address of the master container to connect to
+ * @param {number} [opts.port=1100] - port number of the master container to connect to
+ * @param {boolean} [opts.keepAlive=true] - try to reconnect if the connection is lost
+ * @param {boolean} [opts.debug=false] - debug info to be logged to console?
* @param {number} [opts.reconnectTime=5000] - time before reconnection is attempted after an error
*/
constructor(opts = {}) {
- let host = opts.hostname;
- let port = opts.port;
+ let host = opts.hostname || 'localhost';
+ let port = opts.port || 1100;
this._keepAlive = opts.keepAlive;
this._reconnectTime = opts.reconnectTime || DEFAULT_RECONNECT_TIME$1;
this.url = new URL('tcp://localhost');
this.url.hostname = host;
- this.url.port = port;
+ this.url.port = port.toString();
this._buf = '';
this._firstConn = true; // if the Gateway has managed to connect to a server before
this._firstReConn = true; // if the Gateway has attempted to reconnect to a server before
this.pendingOnOpen = []; // list of callbacks make as soon as gateway is open
this.connListeners = []; // external listeners wanting to listen connection events
+ this.debug = false;
this._sockInit(host, port);
}
@@ -73,6 +75,7 @@ class TCPconnector {
_sockInit(host, port){
if (!createConnection){
try {
+ // @ts-ignore
import('net').then(module => {
createConnection = module.createConnection;
this._sockSetup(host, port);
@@ -156,20 +159,20 @@ class TCPconnector {
return false;
}
+ /**
+ * @callback TCPConnectorReadCallback
+ * @ignore
+ * @param {string} s - incoming message string
+ */
+
/**
* Set a callback for receiving incoming strings from the connector
- * @param {TCPConnector~ReadCallback} cb - callback that is called when the connector gets a string
+ * @param {TCPConnectorReadCallback} cb - callback that is called when the connector gets a string
*/
setReadCallback(cb){
if (cb && {}.toString.call(cb) === '[object Function]') this._onSockRx = cb;
}
- /**
- * @callback TCPConnector~ReadCallback
- * @ignore
- * @param {string} s - incoming message string
- */
-
/**
* Add listener for connection events
* @param {function} listener - a listener callback that is called when the connection is opened/closed
@@ -226,17 +229,20 @@ class WSConnector {
/**
* Create an WSConnector to connect to a fjage master over WebSockets
* @param {Object} opts
- * @param {string} opts.hostname - hostname/ip address of the master container to connect to
- * @param {string} opts.port - port number of the master container to connect to
- * @param {string} opts.pathname - path of the master container to connect to
- * @param {boolean} opts.keepAlive - try to reconnect if the connection is lost
+ * @param {string} [opts.hostname='localhost'] - hostname/ip address of the master container to connect to
+ * @param {number} [opts.port=80] - port number of the master container to connect to
+ * @param {string} [opts.pathname="/"] - path of the master container to connect to
+ * @param {boolean} [opts.keepAlive=true] - try to reconnect if the connection is lost
+ * @param {boolean} [opts.debug=false] - debug info to be logged to console?
* @param {number} [opts.reconnectTime=5000] - time before reconnection is attempted after an error
*/
constructor(opts = {}) {
+ let host = opts.hostname || 'localhost';
+ let port = opts.port || 80;
this.url = new URL('ws://localhost');
- this.url.hostname = opts.hostname;
- this.url.port = opts.port;
- this.url.pathname = opts.pathname;
+ this.url.hostname = host;
+ this.url.port = port.toString();
+ this.url.pathname = opts.pathname || '/';
this._keepAlive = opts.keepAlive;
this._reconnectTime = opts.reconnectTime || DEFAULT_RECONNECT_TIME;
this.debug = opts.debug || false; // debug info to be logged to console?
@@ -311,19 +317,19 @@ class WSConnector {
}
/**
- * Set a callback for receiving incoming strings from the connector
- * @param {WSConnector~ReadCallback} cb - callback that is called when the connector gets a string
+ * @callback WSConnectorReadCallback
* @ignore
+ * @param {string} s - incoming message string
*/
- setReadCallback(cb){
- if (cb && {}.toString.call(cb) === '[object Function]') this._onWebsockRx = cb;
- }
/**
- * @callback WSConnector~ReadCallback
+ * Set a callback for receiving incoming strings from the connector
+ * @param {WSConnectorReadCallback} cb - callback that is called when the connector gets a string
* @ignore
- * @param {string} s - incoming message string
*/
+ setReadCallback(cb){
+ if (cb && {}.toString.call(cb) === '[object Function]') this._onWebsockRx = cb;
+ }
/**
* Add listener for connection events
@@ -395,13 +401,13 @@ const Performative = {
* An identifier for an agent or a topic.
* @class
* @param {string} name - name of the agent
- * @param {boolean} topic - name of topic
- * @param {Gateway} owner - Gateway owner for this AgentID
+ * @param {boolean} [topic=false] - name of topic
+ * @param {Gateway} [owner] - Gateway owner for this AgentID
*/
class AgentID {
- constructor(name, topic, owner) {
+ constructor(name, topic=false, owner) {
this.name = name;
this.topic = topic;
this.owner = owner;
@@ -428,12 +434,13 @@ class AgentID {
/**
* Sends a message to the agent represented by this id.
*
- * @param {string} msg - message to send
+ * @param {Message} msg - message to send
* @returns {void}
*/
send(msg) {
msg.recipient = this.toJSON();
- this.owner.send(msg);
+ if (this.owner) this.owner.send(msg);
+ else throw new Error('Unowned AgentID cannot send messages');
}
/**
@@ -445,7 +452,8 @@ class AgentID {
*/
async request(msg, timeout=1000) {
msg.recipient = this.toJSON();
- return this.owner.request(msg, timeout);
+ if (this.owner) return this.owner.request(msg, timeout);
+ else throw new Error('Unowned AgentID cannot send messages');
}
/**
@@ -564,12 +572,12 @@ class AgentID {
/**
* Base class for messages transmitted by one agent to another. Creates an empty message.
* @class
- * @param {Message} inReplyTo - message to which this response corresponds to
- * @param {Performative} - performative
+ * @param {Message} [inReplyTo] - message to which this response corresponds to
+ * @param {Performative} [perf=Performative.INFORM] - performative
*/
class Message {
- constructor(inReplyTo={msgID:null, sender:null}, perf='') {
+ constructor(inReplyTo={msgID:null, sender:null}, perf=Performative.INFORM) {
this.__clazz__ = 'org.arl.fjage.Message';
this.msgID = _guid(8);
this.sender = null;
@@ -610,7 +618,11 @@ class Message {
// convert a message into a JSON string
// NOTE: we don't do any base64 encoding for TX as
// we don't know what data type is intended
- /** @private */
+ /**
+ * @private
+ *
+ * @return {string} - JSON string representation of the message
+ */
_serialize() {
let clazz = this.__clazz__ || 'org.arl.fjage.Message';
let data = JSON.stringify(this, (k,v) => {
@@ -628,26 +640,27 @@ class Message {
}
// convert a dictionary (usually from decoding JSON) into a message
- /** @private */
- static _deserialize(obj) {
- if (typeof obj == 'string' || obj instanceof String) {
+ /**
+ * @private
+ *
+ * @param {(string|Object)} json - JSON string or object to be converted to a message
+ * @returns {Message} - message created from the JSON string or object
+ * */
+ static _deserialize(json) {
+ let obj = null;
+ if (typeof json == 'string') {
try {
- obj = JSON.parse(obj);
+ obj = JSON.parse(json);
}catch(e){
return null;
}
- }
- try {
- let qclazz = obj.clazz;
- let clazz = qclazz.replace(/^.*\./, '');
- let rv = MessageClass[clazz] ? new MessageClass[clazz] : new Message();
- rv.__clazz__ = qclazz;
- rv._inflate(obj.data);
- return rv;
- } catch (err) {
- console.warn('Error trying to deserialize JSON object : ', obj, err);
- return null;
- }
+ } else obj = json;
+ let qclazz = obj.clazz;
+ let clazz = qclazz.replace(/^.*\./, '');
+ let rv = MessageClass[clazz] ? new MessageClass[clazz] : new Message();
+ rv.__clazz__ = qclazz;
+ rv._inflate(obj.data);
+ return rv;
}
}
@@ -673,36 +686,18 @@ class GenericMessage extends Message {
*
* @class
* @param {Object} opts
- * @param {string} [opts.hostname="localhost"] - hostname/ip address of the master container to connect to
- * @param {string} [opts.port='1100'] - port number of the master container to connect to
- * @param {string} [opts.pathname=""] - path of the master container to connect to (for WebSockets)
- * @param {boolean} [opts.keepAlive=true] - try to reconnect if the connection is lost
- * @param {number} [opts.queueSize=128] - size of the queue of received messages that haven't been consumed yet
- * @param {number} [opts.timeout=1000] - timeout for fjage level messages in ms
+ * @param {string} [opts.hostname="localhost"] - hostname/ip address of the master container to connect to
+ * @param {number} [opts.port=1100] - port number of the master container to connect to
+ * @param {string} [opts.pathname=""] - path of the master container to connect to (for WebSockets)
+ * @param {string} [opts.keepAlive=true] - try to reconnect if the connection is lost
+ * @param {number} [opts.queueSize=128] - size of the queue of received messages that haven't been consumed yet
+ * @param {number} [opts.timeout=1000] - timeout for fjage level messages in ms
* @param {boolean} [opts.returnNullOnFailedResponse=true] - return null instead of throwing an error when a parameter is not found
- * @param {string} [hostname="localhost"] - Deprecated : hostname/ip address of the master container to connect to
- * @param {number} [port=] - Deprecated : port number of the master container to connect to
- * @param {string} [pathname=="/ws/"] - Deprecated : path of the master container to connect to (for WebSockets)
- * @param {number} [timeout=1000] - Deprecated : timeout for fjage level messages in ms
*/
class Gateway {
- constructor(opts = {}, port, pathname='/ws/', timeout=1000) {
- // Support for deprecated constructor
- if (typeof opts === 'string' || opts instanceof String){
- opts = {
- 'hostname': opts,
- 'port' : port,
- 'pathname' : pathname,
- 'timeout' : timeout
- };
- console.warn('Deprecated use of Gateway constructor');
- }
-
- // Set defaults
- for (var key in GATEWAY_DEFAULTS){
- if (opts[key] == undefined || opts[key] === '') opts[key] = GATEWAY_DEFAULTS[key];
- }
+ constructor(opts = {}) {
+ opts = Object.assign({}, GATEWAY_DEFAULTS, opts);
var url = DEFAULT_URL;
url.hostname = opts.hostname;
url.port = opts.port;
@@ -725,7 +720,12 @@ class Gateway {
this._addGWCache(this);
}
- /** @private */
+ /**
+ * Sends an event to all registered listeners of the given type.
+ * @private
+ * @param {string} type - type of event
+ * @param {Object|Message|string} val - value to be sent to the listeners
+ */
_sendEvent(type, val) {
if (Array.isArray(this.eventListeners[type])) {
this.eventListeners[type].forEach(l => {
@@ -740,7 +740,11 @@ class Gateway {
}
}
- /** @private */
+ /**
+ * @private
+ * @param {string} data - stringfied JSON data received from the master container to be processed
+ * @returns {void}
+ */
_onMsgRx(data) {
var obj;
if (this.debug) console.log('< '+data);
@@ -757,6 +761,7 @@ class Gateway {
delete this.pending[obj.id];
} else if (obj.action == 'send') {
// incoming message from master
+ // @ts-ignore
let msg = Message._deserialize(obj.message);
if (!msg) return;
this._sendEvent('rxmsg', msg);
@@ -817,7 +822,12 @@ class Gateway {
}
}
- /** @private */
+ /**
+ * Sends a message out to the master container.
+ * @private
+ * @param {string|Object} s - JSON object (either stringified or not) to be sent to the master container
+ * @returns {boolean} - true if the message was sent successfully
+ */
_msgTx(s) {
if (typeof s != 'string' && !(s instanceof String)) s = JSON.stringify(s);
if(this.debug) console.log('> '+s);
@@ -825,7 +835,11 @@ class Gateway {
return this.connector.write(s);
}
- /** @private */
+ /**
+ * @private
+ * @param {Object} rq - request to be sent to the master container as a JSON object
+ * @returns {Promise} - a promise which returns the response from the master container
+ */
_msgTxRx(rq) {
rq.id = _guid(8);
return new Promise(resolve => {
@@ -847,21 +861,27 @@ class Gateway {
});
}
- /** @private */
+ /**
+ * @private
+ * @param {URL} url - URL object of the master container to connect to
+ * @returns {TCPConnector|WSConnector} - connector object to connect to the master container
+ */
_createConnector(url){
let conn;
if (url.protocol.startsWith('ws')){
conn = new WSConnector({
'hostname':url.hostname,
- 'port':url.port,
+ 'port':parseInt(url.port),
'pathname':url.pathname,
- 'keepAlive': this._keepAlive
+ 'keepAlive': this._keepAlive,
+ 'debug': this.debug
});
}else if (url.protocol.startsWith('tcp')){
- conn = new TCPconnector({
+ conn = new TCPConnector({
'hostname':url.hostname,
- 'port':url.port,
- 'keepAlive': this._keepAlive
+ 'port':parseInt(url.port),
+ 'keepAlive': this._keepAlive,
+ 'debug': this.debug
});
} else return null;
conn.setReadCallback(this._onMsgRx.bind(this));
@@ -877,7 +897,13 @@ class Gateway {
return conn;
}
- /** @private */
+ /**
+ * Checks if the object is a constructor.
+ *
+ * @private
+ * @param {Object} value - an object to be checked if it is a constructor
+ * @returns {boolean} - if the object is a constructor
+ */
_isConstructor(value) {
try {
new new Proxy(value, {construct() { return {}; }});
@@ -887,7 +913,13 @@ class Gateway {
}
}
- /** @private */
+ /**
+ * Matches a message with a filter.
+ * @private
+ * @param {string|Object|function} filter - filter to be matched
+ * @param {Message} msg - message to be matched to the filter
+ * @returns {boolean} - true if the message matches the filter
+ */
_matchMessage(filter, msg){
if (typeof filter == 'string' || filter instanceof String) {
return 'inReplyTo' in msg && msg.inReplyTo == filter;
@@ -907,18 +939,25 @@ class Gateway {
}
}
- /** @private */
+ /**
+ * Gets the next message from the queue that matches the filter.
+ * @private
+ * @param {string|Object|function} filter - filter to be matched
+ */
_getMessageFromQueue(filter) {
if (!this.queue.length) return;
if (!filter) return this.queue.shift();
-
let matchedMsg = this.queue.find( msg => this._matchMessage(filter, msg));
if (matchedMsg) this.queue.splice(this.queue.indexOf(matchedMsg), 1);
-
return matchedMsg;
}
- /** @private */
+ /**
+ * Gets a cached gateway object for the given URL (if it exists).
+ * @private
+ * @param {URL} url - URL object of the master container to connect to
+ * @returns {Gateway|void} - gateway object for the given URL
+ */
_getGWCache(url){
if (!gObj.fjage || !gObj.fjage.gateways) return null;
var f = gObj.fjage.gateways.filter(g => g.connector.url.toString() == url.toString());
@@ -926,13 +965,21 @@ class Gateway {
return null;
}
- /** @private */
+ /**
+ * Adds a gateway object to the cache if it doesn't already exist.
+ * @private
+ * @param {Gateway} gw - gateway object to be added to the cache
+ */
_addGWCache(gw){
if (!gObj.fjage || !gObj.fjage.gateways) return;
gObj.fjage.gateways.push(gw);
}
- /** @private */
+ /**
+ * Removes a gateway object from the cache if it exists.
+ * @private
+ * @param {Gateway} gw - gateway object to be removed from the cache
+ */
_removeGWCache(gw){
if (!gObj.fjage || !gObj.fjage.gateways) return;
var index = gObj.fjage.gateways.indexOf(gw);
@@ -1020,7 +1067,7 @@ class Gateway {
/**
* Gets the agent ID associated with the gateway.
*
- * @returns {string} - agent ID
+ * @returns {AgentID} - agent ID
*/
getAgentID() {
return this.aid;
@@ -1061,6 +1108,7 @@ class Gateway {
if (!topic.isTopic()) topic = new AgentID(topic.getName() + '__ntf', true, this);
this.subscriptions[topic.toJSON()] = true;
this._update_watch();
+ return true;
}
/**
@@ -1152,6 +1200,7 @@ class Gateway {
}
this._sendEvent('txmsg', msg);
let rq = JSON.stringify({ action: 'send', relay: true, message: '###MSG###' });
+ // @ts-ignore
rq = rq.replace('"###MSG###"', msg._serialize());
return !!this._msgTx(rq);
}
@@ -1169,9 +1218,9 @@ class Gateway {
* Sends a request and waits for a response. This method returns a {Promise} which resolves when a response
* is received or if no response is received after the timeout.
*
- * @param {string} msg - message to send
+ * @param {Message} msg - message to send
* @param {number} [timeout=1000] - timeout in milliseconds
- * @returns {Promise} - a promise which resolves with the received response message, null on timeout
+ * @returns {Promise} - a promise which resolves with the received response message, null on timeout
*/
async request(msg, timeout=1000) {
this.send(msg);
@@ -1182,10 +1231,10 @@ class Gateway {
* Returns a response message received by the gateway. This method returns a {Promise} which resolves when
* a response is received or if no response is received after the timeout.
*
- * @param {function} [filter=] - original message to which a response is expected, or a MessageClass of the type
+ * @param {function|Message|typeof Message} filter - original message to which a response is expected, or a MessageClass of the type
* of message to match, or a closure to use to match against the message
* @param {number} [timeout=0] - timeout in milliseconds
- * @returns {Promise} - received response message, null on timeout
+ * @returns {Promise} - received response message, null on timeout
*/
async receive(filter, timeout=0) {
return new Promise(resolve => {
@@ -1240,8 +1289,8 @@ const Services = {
/**
* Creates a unqualified message class based on a fully qualified name.
* @param {string} name - fully qualified name of the message class to be created
- * @param {class} [parent=Message] - class of the parent MessageClass to inherit from
- * @returns {function} - constructor for the unqualified message class
+ * @param {typeof Message} [parent] - class of the parent MessageClass to inherit from
+ * @constructs Message
* @example
* const ParameterReq = MessageClass('org.arl.fjage.param.ParameterReq');
* let pReq = new ParameterReq()
@@ -1250,6 +1299,9 @@ function MessageClass(name, parent=Message) {
let sname = name.replace(/^.*\./, '');
if (MessageClass[sname]) return MessageClass[sname];
let cls = class extends parent {
+ /**
+ * @param {{ [x: string]: any; }} params
+ */
constructor(params) {
super();
this.__clazz__ = name;
@@ -1305,7 +1357,7 @@ function _b64toArray(base64, dtype, littleEndian=true) {
break;
case '[J': // long array
for (i = 0; i < len; i+=8)
- rv.push(view.getInt64(i, littleEndian));
+ rv.push(view.getBigInt64(i, littleEndian));
break;
case '[F': // float array
for (i = 0; i < len; i+=4)
@@ -1340,6 +1392,8 @@ function _decodeBase64(k, d) {
////// global
const GATEWAY_DEFAULTS = {};
+
+/** @type {Window & globalThis & Object} */
let gObj = {};
let DEFAULT_URL;
if (isBrowser || isWebWorker){
@@ -1356,7 +1410,7 @@ if (isBrowser || isWebWorker){
DEFAULT_URL = new URL('ws://localhost');
// Enable caching of Gateways
if (typeof gObj.fjage === 'undefined') gObj.fjage = {};
- if (typeof gObj.fjage.gateways == 'undefined')gObj.fjage.gateways = [];
+ if (typeof gObj.fjage.gateways == 'undefined') gObj.fjage.gateways = [];
} else if (isJsDom || isNode){
gObj = global;
Object.assign(GATEWAY_DEFAULTS, {
@@ -1372,6 +1426,23 @@ if (isBrowser || isWebWorker){
gObj.atob = a => Buffer.from(a, 'base64').toString('binary');
}
+/**
+ * @typedef {Object} ParameterReq.Entry
+ * @property {string} param - parameter name
+ * @property {Object} value - parameter value
+ * @exports ParameterReq.Entry
+ */
+
+
+/**
+ * A message that requests one or more parameters of an agent.
+ * @typedef {Message} ParameterReq
+ * @property {string} param - parameters name to be get/set if only a single parameter is to be get/set
+ * @property {Object} value - parameters value to be set if only a single parameter is to be set
+ * @property {Array} requests - a list of multiple parameters to be get/set
+ * @property {number} [index=-1] - index of parameter(s) to be set
+ * @exports ParameterReq
+ */
const ParameterReq = MessageClass('org.arl.fjage.param.ParameterReq');
-export { AgentID, Gateway, GenericMessage, Message, MessageClass, Performative, Services };
+export { AgentID, Gateway, GenericMessage, Message, MessageClass, ParameterReq, Performative, Services };
diff --git a/gateways/js/dist/fjage.js b/gateways/js/dist/fjage.js
index 3b4e9011..4073cd86 100644
--- a/gateways/js/dist/fjage.js
+++ b/gateways/js/dist/fjage.js
@@ -43,29 +43,31 @@
* @class
* @ignore
*/
- class TCPconnector {
+ class TCPConnector {
/**
* Create an TCPConnector to connect to a fjage master over TCP
* @param {Object} opts
- * @param {string} opts.hostname - hostname/ip address of the master container to connect to
- * @param {string} opts.port - port number of the master container to connect to
- * @param {boolean} opts.keepAlive - try to reconnect if the connection is lost
+ * @param {string} [opts.hostname='localhost'] - hostname/ip address of the master container to connect to
+ * @param {number} [opts.port=1100] - port number of the master container to connect to
+ * @param {boolean} [opts.keepAlive=true] - try to reconnect if the connection is lost
+ * @param {boolean} [opts.debug=false] - debug info to be logged to console?
* @param {number} [opts.reconnectTime=5000] - time before reconnection is attempted after an error
*/
constructor(opts = {}) {
- let host = opts.hostname;
- let port = opts.port;
+ let host = opts.hostname || 'localhost';
+ let port = opts.port || 1100;
this._keepAlive = opts.keepAlive;
this._reconnectTime = opts.reconnectTime || DEFAULT_RECONNECT_TIME$1;
this.url = new URL('tcp://localhost');
this.url.hostname = host;
- this.url.port = port;
+ this.url.port = port.toString();
this._buf = '';
this._firstConn = true; // if the Gateway has managed to connect to a server before
this._firstReConn = true; // if the Gateway has attempted to reconnect to a server before
this.pendingOnOpen = []; // list of callbacks make as soon as gateway is open
this.connListeners = []; // external listeners wanting to listen connection events
+ this.debug = false;
this._sockInit(host, port);
}
@@ -79,6 +81,7 @@
_sockInit(host, port){
if (!createConnection){
try {
+ // @ts-ignore
import('net').then(module => {
createConnection = module.createConnection;
this._sockSetup(host, port);
@@ -162,20 +165,20 @@
return false;
}
+ /**
+ * @callback TCPConnectorReadCallback
+ * @ignore
+ * @param {string} s - incoming message string
+ */
+
/**
* Set a callback for receiving incoming strings from the connector
- * @param {TCPConnector~ReadCallback} cb - callback that is called when the connector gets a string
+ * @param {TCPConnectorReadCallback} cb - callback that is called when the connector gets a string
*/
setReadCallback(cb){
if (cb && {}.toString.call(cb) === '[object Function]') this._onSockRx = cb;
}
- /**
- * @callback TCPConnector~ReadCallback
- * @ignore
- * @param {string} s - incoming message string
- */
-
/**
* Add listener for connection events
* @param {function} listener - a listener callback that is called when the connection is opened/closed
@@ -232,17 +235,20 @@
/**
* Create an WSConnector to connect to a fjage master over WebSockets
* @param {Object} opts
- * @param {string} opts.hostname - hostname/ip address of the master container to connect to
- * @param {string} opts.port - port number of the master container to connect to
- * @param {string} opts.pathname - path of the master container to connect to
- * @param {boolean} opts.keepAlive - try to reconnect if the connection is lost
+ * @param {string} [opts.hostname='localhost'] - hostname/ip address of the master container to connect to
+ * @param {number} [opts.port=80] - port number of the master container to connect to
+ * @param {string} [opts.pathname="/"] - path of the master container to connect to
+ * @param {boolean} [opts.keepAlive=true] - try to reconnect if the connection is lost
+ * @param {boolean} [opts.debug=false] - debug info to be logged to console?
* @param {number} [opts.reconnectTime=5000] - time before reconnection is attempted after an error
*/
constructor(opts = {}) {
+ let host = opts.hostname || 'localhost';
+ let port = opts.port || 80;
this.url = new URL('ws://localhost');
- this.url.hostname = opts.hostname;
- this.url.port = opts.port;
- this.url.pathname = opts.pathname;
+ this.url.hostname = host;
+ this.url.port = port.toString();
+ this.url.pathname = opts.pathname || '/';
this._keepAlive = opts.keepAlive;
this._reconnectTime = opts.reconnectTime || DEFAULT_RECONNECT_TIME;
this.debug = opts.debug || false; // debug info to be logged to console?
@@ -317,19 +323,19 @@
}
/**
- * Set a callback for receiving incoming strings from the connector
- * @param {WSConnector~ReadCallback} cb - callback that is called when the connector gets a string
+ * @callback WSConnectorReadCallback
* @ignore
+ * @param {string} s - incoming message string
*/
- setReadCallback(cb){
- if (cb && {}.toString.call(cb) === '[object Function]') this._onWebsockRx = cb;
- }
/**
- * @callback WSConnector~ReadCallback
+ * Set a callback for receiving incoming strings from the connector
+ * @param {WSConnectorReadCallback} cb - callback that is called when the connector gets a string
* @ignore
- * @param {string} s - incoming message string
*/
+ setReadCallback(cb){
+ if (cb && {}.toString.call(cb) === '[object Function]') this._onWebsockRx = cb;
+ }
/**
* Add listener for connection events
@@ -401,13 +407,13 @@
* An identifier for an agent or a topic.
* @class
* @param {string} name - name of the agent
- * @param {boolean} topic - name of topic
- * @param {Gateway} owner - Gateway owner for this AgentID
+ * @param {boolean} [topic=false] - name of topic
+ * @param {Gateway} [owner] - Gateway owner for this AgentID
*/
class AgentID {
- constructor(name, topic, owner) {
+ constructor(name, topic=false, owner) {
this.name = name;
this.topic = topic;
this.owner = owner;
@@ -434,12 +440,13 @@
/**
* Sends a message to the agent represented by this id.
*
- * @param {string} msg - message to send
+ * @param {Message} msg - message to send
* @returns {void}
*/
send(msg) {
msg.recipient = this.toJSON();
- this.owner.send(msg);
+ if (this.owner) this.owner.send(msg);
+ else throw new Error('Unowned AgentID cannot send messages');
}
/**
@@ -451,7 +458,8 @@
*/
async request(msg, timeout=1000) {
msg.recipient = this.toJSON();
- return this.owner.request(msg, timeout);
+ if (this.owner) return this.owner.request(msg, timeout);
+ else throw new Error('Unowned AgentID cannot send messages');
}
/**
@@ -570,12 +578,12 @@
/**
* Base class for messages transmitted by one agent to another. Creates an empty message.
* @class
- * @param {Message} inReplyTo - message to which this response corresponds to
- * @param {Performative} - performative
+ * @param {Message} [inReplyTo] - message to which this response corresponds to
+ * @param {Performative} [perf=Performative.INFORM] - performative
*/
class Message {
- constructor(inReplyTo={msgID:null, sender:null}, perf='') {
+ constructor(inReplyTo={msgID:null, sender:null}, perf=Performative.INFORM) {
this.__clazz__ = 'org.arl.fjage.Message';
this.msgID = _guid(8);
this.sender = null;
@@ -616,7 +624,11 @@
// convert a message into a JSON string
// NOTE: we don't do any base64 encoding for TX as
// we don't know what data type is intended
- /** @private */
+ /**
+ * @private
+ *
+ * @return {string} - JSON string representation of the message
+ */
_serialize() {
let clazz = this.__clazz__ || 'org.arl.fjage.Message';
let data = JSON.stringify(this, (k,v) => {
@@ -634,26 +646,27 @@
}
// convert a dictionary (usually from decoding JSON) into a message
- /** @private */
- static _deserialize(obj) {
- if (typeof obj == 'string' || obj instanceof String) {
+ /**
+ * @private
+ *
+ * @param {(string|Object)} json - JSON string or object to be converted to a message
+ * @returns {Message} - message created from the JSON string or object
+ * */
+ static _deserialize(json) {
+ let obj = null;
+ if (typeof json == 'string') {
try {
- obj = JSON.parse(obj);
+ obj = JSON.parse(json);
}catch(e){
return null;
}
- }
- try {
- let qclazz = obj.clazz;
- let clazz = qclazz.replace(/^.*\./, '');
- let rv = MessageClass[clazz] ? new MessageClass[clazz] : new Message();
- rv.__clazz__ = qclazz;
- rv._inflate(obj.data);
- return rv;
- } catch (err) {
- console.warn('Error trying to deserialize JSON object : ', obj, err);
- return null;
- }
+ } else obj = json;
+ let qclazz = obj.clazz;
+ let clazz = qclazz.replace(/^.*\./, '');
+ let rv = MessageClass[clazz] ? new MessageClass[clazz] : new Message();
+ rv.__clazz__ = qclazz;
+ rv._inflate(obj.data);
+ return rv;
}
}
@@ -679,36 +692,18 @@
*
* @class
* @param {Object} opts
- * @param {string} [opts.hostname="localhost"] - hostname/ip address of the master container to connect to
- * @param {string} [opts.port='1100'] - port number of the master container to connect to
- * @param {string} [opts.pathname=""] - path of the master container to connect to (for WebSockets)
- * @param {boolean} [opts.keepAlive=true] - try to reconnect if the connection is lost
- * @param {number} [opts.queueSize=128] - size of the queue of received messages that haven't been consumed yet
- * @param {number} [opts.timeout=1000] - timeout for fjage level messages in ms
+ * @param {string} [opts.hostname="localhost"] - hostname/ip address of the master container to connect to
+ * @param {number} [opts.port=1100] - port number of the master container to connect to
+ * @param {string} [opts.pathname=""] - path of the master container to connect to (for WebSockets)
+ * @param {string} [opts.keepAlive=true] - try to reconnect if the connection is lost
+ * @param {number} [opts.queueSize=128] - size of the queue of received messages that haven't been consumed yet
+ * @param {number} [opts.timeout=1000] - timeout for fjage level messages in ms
* @param {boolean} [opts.returnNullOnFailedResponse=true] - return null instead of throwing an error when a parameter is not found
- * @param {string} [hostname="localhost"] - Deprecated : hostname/ip address of the master container to connect to
- * @param {number} [port=] - Deprecated : port number of the master container to connect to
- * @param {string} [pathname=="/ws/"] - Deprecated : path of the master container to connect to (for WebSockets)
- * @param {number} [timeout=1000] - Deprecated : timeout for fjage level messages in ms
*/
class Gateway {
- constructor(opts = {}, port, pathname='/ws/', timeout=1000) {
- // Support for deprecated constructor
- if (typeof opts === 'string' || opts instanceof String){
- opts = {
- 'hostname': opts,
- 'port' : port,
- 'pathname' : pathname,
- 'timeout' : timeout
- };
- console.warn('Deprecated use of Gateway constructor');
- }
-
- // Set defaults
- for (var key in GATEWAY_DEFAULTS){
- if (opts[key] == undefined || opts[key] === '') opts[key] = GATEWAY_DEFAULTS[key];
- }
+ constructor(opts = {}) {
+ opts = Object.assign({}, GATEWAY_DEFAULTS, opts);
var url = DEFAULT_URL;
url.hostname = opts.hostname;
url.port = opts.port;
@@ -731,7 +726,12 @@
this._addGWCache(this);
}
- /** @private */
+ /**
+ * Sends an event to all registered listeners of the given type.
+ * @private
+ * @param {string} type - type of event
+ * @param {Object|Message|string} val - value to be sent to the listeners
+ */
_sendEvent(type, val) {
if (Array.isArray(this.eventListeners[type])) {
this.eventListeners[type].forEach(l => {
@@ -746,7 +746,11 @@
}
}
- /** @private */
+ /**
+ * @private
+ * @param {string} data - stringfied JSON data received from the master container to be processed
+ * @returns {void}
+ */
_onMsgRx(data) {
var obj;
if (this.debug) console.log('< '+data);
@@ -763,6 +767,7 @@
delete this.pending[obj.id];
} else if (obj.action == 'send') {
// incoming message from master
+ // @ts-ignore
let msg = Message._deserialize(obj.message);
if (!msg) return;
this._sendEvent('rxmsg', msg);
@@ -823,7 +828,12 @@
}
}
- /** @private */
+ /**
+ * Sends a message out to the master container.
+ * @private
+ * @param {string|Object} s - JSON object (either stringified or not) to be sent to the master container
+ * @returns {boolean} - true if the message was sent successfully
+ */
_msgTx(s) {
if (typeof s != 'string' && !(s instanceof String)) s = JSON.stringify(s);
if(this.debug) console.log('> '+s);
@@ -831,7 +841,11 @@
return this.connector.write(s);
}
- /** @private */
+ /**
+ * @private
+ * @param {Object} rq - request to be sent to the master container as a JSON object
+ * @returns {Promise} - a promise which returns the response from the master container
+ */
_msgTxRx(rq) {
rq.id = _guid(8);
return new Promise(resolve => {
@@ -853,21 +867,27 @@
});
}
- /** @private */
+ /**
+ * @private
+ * @param {URL} url - URL object of the master container to connect to
+ * @returns {TCPConnector|WSConnector} - connector object to connect to the master container
+ */
_createConnector(url){
let conn;
if (url.protocol.startsWith('ws')){
conn = new WSConnector({
'hostname':url.hostname,
- 'port':url.port,
+ 'port':parseInt(url.port),
'pathname':url.pathname,
- 'keepAlive': this._keepAlive
+ 'keepAlive': this._keepAlive,
+ 'debug': this.debug
});
}else if (url.protocol.startsWith('tcp')){
- conn = new TCPconnector({
+ conn = new TCPConnector({
'hostname':url.hostname,
- 'port':url.port,
- 'keepAlive': this._keepAlive
+ 'port':parseInt(url.port),
+ 'keepAlive': this._keepAlive,
+ 'debug': this.debug
});
} else return null;
conn.setReadCallback(this._onMsgRx.bind(this));
@@ -883,7 +903,13 @@
return conn;
}
- /** @private */
+ /**
+ * Checks if the object is a constructor.
+ *
+ * @private
+ * @param {Object} value - an object to be checked if it is a constructor
+ * @returns {boolean} - if the object is a constructor
+ */
_isConstructor(value) {
try {
new new Proxy(value, {construct() { return {}; }});
@@ -893,7 +919,13 @@
}
}
- /** @private */
+ /**
+ * Matches a message with a filter.
+ * @private
+ * @param {string|Object|function} filter - filter to be matched
+ * @param {Message} msg - message to be matched to the filter
+ * @returns {boolean} - true if the message matches the filter
+ */
_matchMessage(filter, msg){
if (typeof filter == 'string' || filter instanceof String) {
return 'inReplyTo' in msg && msg.inReplyTo == filter;
@@ -913,18 +945,25 @@
}
}
- /** @private */
+ /**
+ * Gets the next message from the queue that matches the filter.
+ * @private
+ * @param {string|Object|function} filter - filter to be matched
+ */
_getMessageFromQueue(filter) {
if (!this.queue.length) return;
if (!filter) return this.queue.shift();
-
let matchedMsg = this.queue.find( msg => this._matchMessage(filter, msg));
if (matchedMsg) this.queue.splice(this.queue.indexOf(matchedMsg), 1);
-
return matchedMsg;
}
- /** @private */
+ /**
+ * Gets a cached gateway object for the given URL (if it exists).
+ * @private
+ * @param {URL} url - URL object of the master container to connect to
+ * @returns {Gateway|void} - gateway object for the given URL
+ */
_getGWCache(url){
if (!gObj.fjage || !gObj.fjage.gateways) return null;
var f = gObj.fjage.gateways.filter(g => g.connector.url.toString() == url.toString());
@@ -932,13 +971,21 @@
return null;
}
- /** @private */
+ /**
+ * Adds a gateway object to the cache if it doesn't already exist.
+ * @private
+ * @param {Gateway} gw - gateway object to be added to the cache
+ */
_addGWCache(gw){
if (!gObj.fjage || !gObj.fjage.gateways) return;
gObj.fjage.gateways.push(gw);
}
- /** @private */
+ /**
+ * Removes a gateway object from the cache if it exists.
+ * @private
+ * @param {Gateway} gw - gateway object to be removed from the cache
+ */
_removeGWCache(gw){
if (!gObj.fjage || !gObj.fjage.gateways) return;
var index = gObj.fjage.gateways.indexOf(gw);
@@ -1026,7 +1073,7 @@
/**
* Gets the agent ID associated with the gateway.
*
- * @returns {string} - agent ID
+ * @returns {AgentID} - agent ID
*/
getAgentID() {
return this.aid;
@@ -1067,6 +1114,7 @@
if (!topic.isTopic()) topic = new AgentID(topic.getName() + '__ntf', true, this);
this.subscriptions[topic.toJSON()] = true;
this._update_watch();
+ return true;
}
/**
@@ -1158,6 +1206,7 @@
}
this._sendEvent('txmsg', msg);
let rq = JSON.stringify({ action: 'send', relay: true, message: '###MSG###' });
+ // @ts-ignore
rq = rq.replace('"###MSG###"', msg._serialize());
return !!this._msgTx(rq);
}
@@ -1175,9 +1224,9 @@
* Sends a request and waits for a response. This method returns a {Promise} which resolves when a response
* is received or if no response is received after the timeout.
*
- * @param {string} msg - message to send
+ * @param {Message} msg - message to send
* @param {number} [timeout=1000] - timeout in milliseconds
- * @returns {Promise} - a promise which resolves with the received response message, null on timeout
+ * @returns {Promise} - a promise which resolves with the received response message, null on timeout
*/
async request(msg, timeout=1000) {
this.send(msg);
@@ -1188,10 +1237,10 @@
* Returns a response message received by the gateway. This method returns a {Promise} which resolves when
* a response is received or if no response is received after the timeout.
*
- * @param {function} [filter=] - original message to which a response is expected, or a MessageClass of the type
+ * @param {function|Message|typeof Message} filter - original message to which a response is expected, or a MessageClass of the type
* of message to match, or a closure to use to match against the message
* @param {number} [timeout=0] - timeout in milliseconds
- * @returns {Promise} - received response message, null on timeout
+ * @returns {Promise} - received response message, null on timeout
*/
async receive(filter, timeout=0) {
return new Promise(resolve => {
@@ -1246,8 +1295,8 @@
/**
* Creates a unqualified message class based on a fully qualified name.
* @param {string} name - fully qualified name of the message class to be created
- * @param {class} [parent=Message] - class of the parent MessageClass to inherit from
- * @returns {function} - constructor for the unqualified message class
+ * @param {typeof Message} [parent] - class of the parent MessageClass to inherit from
+ * @constructs Message
* @example
* const ParameterReq = MessageClass('org.arl.fjage.param.ParameterReq');
* let pReq = new ParameterReq()
@@ -1256,6 +1305,9 @@
let sname = name.replace(/^.*\./, '');
if (MessageClass[sname]) return MessageClass[sname];
let cls = class extends parent {
+ /**
+ * @param {{ [x: string]: any; }} params
+ */
constructor(params) {
super();
this.__clazz__ = name;
@@ -1311,7 +1363,7 @@
break;
case '[J': // long array
for (i = 0; i < len; i+=8)
- rv.push(view.getInt64(i, littleEndian));
+ rv.push(view.getBigInt64(i, littleEndian));
break;
case '[F': // float array
for (i = 0; i < len; i+=4)
@@ -1346,6 +1398,8 @@
////// global
const GATEWAY_DEFAULTS = {};
+
+ /** @type {Window & globalThis & Object} */
let gObj = {};
let DEFAULT_URL;
if (isBrowser || isWebWorker){
@@ -1362,7 +1416,7 @@
DEFAULT_URL = new URL('ws://localhost');
// Enable caching of Gateways
if (typeof gObj.fjage === 'undefined') gObj.fjage = {};
- if (typeof gObj.fjage.gateways == 'undefined')gObj.fjage.gateways = [];
+ if (typeof gObj.fjage.gateways == 'undefined') gObj.fjage.gateways = [];
} else if (isJsDom || isNode){
gObj = global;
Object.assign(GATEWAY_DEFAULTS, {
@@ -1378,6 +1432,23 @@
gObj.atob = a => Buffer.from(a, 'base64').toString('binary');
}
+ /**
+ * @typedef {Object} ParameterReq.Entry
+ * @property {string} param - parameter name
+ * @property {Object} value - parameter value
+ * @exports ParameterReq.Entry
+ */
+
+
+ /**
+ * A message that requests one or more parameters of an agent.
+ * @typedef {Message} ParameterReq
+ * @property {string} param - parameters name to be get/set if only a single parameter is to be get/set
+ * @property {Object} value - parameters value to be set if only a single parameter is to be set
+ * @property {Array} requests - a list of multiple parameters to be get/set
+ * @property {number} [index=-1] - index of parameter(s) to be set
+ * @exports ParameterReq
+ */
const ParameterReq = MessageClass('org.arl.fjage.param.ParameterReq');
exports.AgentID = AgentID;
@@ -1385,6 +1456,7 @@
exports.GenericMessage = GenericMessage;
exports.Message = Message;
exports.MessageClass = MessageClass;
+ exports.ParameterReq = ParameterReq;
exports.Performative = Performative;
exports.Services = Services;
diff --git a/gateways/js/eslint.config.js b/gateways/js/eslint.config.js
index dcf0497f..8671cd7d 100644
--- a/gateways/js/eslint.config.js
+++ b/gateways/js/eslint.config.js
@@ -15,7 +15,7 @@ export default [
'quotes': [
'error',
'single'
- ]
+ ],
}
}
];
\ No newline at end of file
diff --git a/gateways/js/package-lock.json b/gateways/js/package-lock.json
index 5169e2e9..a8133448 100644
--- a/gateways/js/package-lock.json
+++ b/gateways/js/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "fjage",
- "version": "1.12.2",
+ "version": "1.13.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "fjage",
- "version": "1.12.2",
+ "version": "1.13.0",
"license": "BSD-2-Clause",
"dependencies": {
"browser-or-node": "^2.1.1"
@@ -16,12 +16,14 @@
"@playwright/test": "^1.41.0",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-terser": "^0.4.4",
+ "@types/node": "^22.7.5",
"documentation": "^14.0.2",
"eslint": "^8.55.0",
"globals": "^13.24.0",
"jasmine": "^5.1.0",
"node-static": "^0.7.11",
- "rollup": "^4.8.0"
+ "rollup": "^4.8.0",
+ "typescript": "^5.6.3"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@@ -957,6 +959,16 @@
"integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==",
"dev": true
},
+ "node_modules/@types/node": {
+ "version": "22.7.5",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz",
+ "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.19.2"
+ }
+ },
"node_modules/@types/normalize-package-data": {
"version": "2.4.4",
"resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz",
@@ -5151,6 +5163,20 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/typescript": {
+ "version": "5.6.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
+ "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
"node_modules/unc-path-regex": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz",
@@ -5160,6 +5186,13 @@
"node": ">=0.10.0"
}
},
+ "node_modules/undici-types": {
+ "version": "6.19.8",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
+ "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/unified": {
"version": "10.1.2",
"resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz",
diff --git a/gateways/js/package.json b/gateways/js/package.json
index 4af934f3..f43a6bfb 100644
--- a/gateways/js/package.json
+++ b/gateways/js/package.json
@@ -12,11 +12,11 @@
"dist/**"
],
"scripts": {
- "build": "npx rimraf -rf dist/ && eslint src/*.js && rollup --silent -c rollup.config.js",
- "pretest": "npx playwright install --with-deps && node test/spec/create-spec.cjs",
+ "build": "eslint src/*.js && tsc && rollup --silent -c rollup.config.js",
+ "pretest": "playwright install --with-deps && node test/spec/create-spec.cjs",
"test": "node test/run-tests.cjs",
"docs": "documentation build src/fjage.js -f html --github --document-exported -o ../../docs/jsdoc",
- "clean": "npx rimraf -rf dist/"
+ "clean": "rimraf -rf dist/"
},
"repository": {
"type": "git",
@@ -36,12 +36,14 @@
"@playwright/test": "^1.41.0",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-terser": "^0.4.4",
+ "@types/node": "^22.7.5",
"documentation": "^14.0.2",
"eslint": "^8.55.0",
"globals": "^13.24.0",
"jasmine": "^5.1.0",
"node-static": "^0.7.11",
- "rollup": "^4.8.0"
+ "rollup": "^4.8.0",
+ "typescript": "^5.6.3"
},
"dependencies": {
"browser-or-node": "^2.1.1"
diff --git a/gateways/js/src/TCPConnector.js b/gateways/js/src/TCPConnector.js
index 3c0ced7f..6f2eb87e 100644
--- a/gateways/js/src/TCPConnector.js
+++ b/gateways/js/src/TCPConnector.js
@@ -8,7 +8,7 @@ var createConnection;
* @class
* @ignore
*/
-export default class TCPConnector {
+class TCPConnector {
/**
* Create an TCPConnector to connect to a fjage master over TCP
@@ -130,20 +130,20 @@ export default class TCPConnector {
return false;
}
+ /**
+ * @callback TCPConnectorReadCallback
+ * @ignore
+ * @param {string} s - incoming message string
+ */
+
/**
* Set a callback for receiving incoming strings from the connector
- * @param {TCPConnector~ReadCallback} cb - callback that is called when the connector gets a string
+ * @param {TCPConnectorReadCallback} cb - callback that is called when the connector gets a string
*/
setReadCallback(cb){
if (cb && {}.toString.call(cb) === '[object Function]') this._onSockRx = cb;
}
- /**
- * @callback TCPConnector~ReadCallback
- * @ignore
- * @param {string} s - incoming message string
- */
-
/**
* Add listener for connection events
* @param {function} listener - a listener callback that is called when the connection is opened/closed
@@ -188,3 +188,5 @@ export default class TCPConnector {
}
}
}
+
+export default TCPConnector;
\ No newline at end of file
diff --git a/gateways/js/src/WSConnector.js b/gateways/js/src/WSConnector.js
index 89e6214e..28beee6a 100644
--- a/gateways/js/src/WSConnector.js
+++ b/gateways/js/src/WSConnector.js
@@ -4,7 +4,7 @@ const DEFAULT_RECONNECT_TIME = 5000; // ms, delay between retries to conne
* @class
* @ignore
*/
-export default class WSConnector {
+class WSConnector {
/**
* Create an WSConnector to connect to a fjage master over WebSockets
@@ -23,6 +23,7 @@ export default class WSConnector {
this.url.hostname = host;
this.url.port = port.toString();
this.url.pathname = opts.pathname || '/';
+ this._keepAlive = opts.keepAlive;
this._reconnectTime = opts.reconnectTime || DEFAULT_RECONNECT_TIME;
this.debug = opts.debug || false; // debug info to be logged to console?
this._firstConn = true; // if the Gateway has managed to connect to a server before
@@ -96,19 +97,19 @@ export default class WSConnector {
}
/**
- * Set a callback for receiving incoming strings from the connector
- * @param {WSConnector~ReadCallback} cb - callback that is called when the connector gets a string
+ * @callback WSConnectorReadCallback
* @ignore
+ * @param {string} s - incoming message string
*/
- setReadCallback(cb){
- if (cb && {}.toString.call(cb) === '[object Function]') this._onWebsockRx = cb;
- }
/**
- * @callback WSConnector~ReadCallback
+ * Set a callback for receiving incoming strings from the connector
+ * @param {WSConnectorReadCallback} cb - callback that is called when the connector gets a string
* @ignore
- * @param {string} s - incoming message string
*/
+ setReadCallback(cb){
+ if (cb && {}.toString.call(cb) === '[object Function]') this._onWebsockRx = cb;
+ }
/**
* Add listener for connection events
@@ -149,4 +150,6 @@ export default class WSConnector {
this.sock.close();
}
}
-}
\ No newline at end of file
+}
+
+export default WSConnector;
\ No newline at end of file
diff --git a/gateways/js/src/fjage.js b/gateways/js/src/fjage.js
index 16367fb3..0601445a 100644
--- a/gateways/js/src/fjage.js
+++ b/gateways/js/src/fjage.js
@@ -30,13 +30,13 @@ export const Performative = {
* An identifier for an agent or a topic.
* @class
* @param {string} name - name of the agent
- * @param {boolean} topic - name of topic
- * @param {Gateway} owner - Gateway owner for this AgentID
+ * @param {boolean} [topic=false] - name of topic
+ * @param {Gateway} [owner] - Gateway owner for this AgentID
*/
export class AgentID {
- constructor(name, topic, owner) {
+ constructor(name, topic=false, owner) {
this.name = name;
this.topic = topic;
this.owner = owner;
@@ -68,7 +68,8 @@ export class AgentID {
*/
send(msg) {
msg.recipient = this.toJSON();
- this.owner.send(msg);
+ if (this.owner) this.owner.send(msg);
+ else throw new Error('Unowned AgentID cannot send messages');
}
/**
@@ -80,7 +81,8 @@ export class AgentID {
*/
async request(msg, timeout=1000) {
msg.recipient = this.toJSON();
- return this.owner.request(msg, timeout);
+ if (this.owner) return this.owner.request(msg, timeout);
+ else throw new Error('Unowned AgentID cannot send messages');
}
/**
@@ -199,8 +201,8 @@ export class AgentID {
/**
* Base class for messages transmitted by one agent to another. Creates an empty message.
* @class
- * @param {Message} inReplyTo - message to which this response corresponds to
- * @param {Performative} perf - performative
+ * @param {Message} [inReplyTo] - message to which this response corresponds to
+ * @param {Performative} [perf=Performative.INFORM] - performative
*/
export class Message {
@@ -347,7 +349,12 @@ export class Gateway {
this._addGWCache(this);
}
- /** @private */
+ /**
+ * Sends an event to all registered listeners of the given type.
+ * @private
+ * @param {string} type - type of event
+ * @param {Object|Message|string} val - value to be sent to the listeners
+ */
_sendEvent(type, val) {
if (Array.isArray(this.eventListeners[type])) {
this.eventListeners[type].forEach(l => {
@@ -362,7 +369,11 @@ export class Gateway {
}
}
- /** @private */
+ /**
+ * @private
+ * @param {string} data - stringfied JSON data received from the master container to be processed
+ * @returns {void}
+ */
_onMsgRx(data) {
var obj;
if (this.debug) console.log('< '+data);
@@ -440,7 +451,12 @@ export class Gateway {
}
}
- /** @private */
+ /**
+ * Sends a message out to the master container.
+ * @private
+ * @param {string|Object} s - JSON object (either stringified or not) to be sent to the master container
+ * @returns {boolean} - true if the message was sent successfully
+ */
_msgTx(s) {
if (typeof s != 'string' && !(s instanceof String)) s = JSON.stringify(s);
if(this.debug) console.log('> '+s);
@@ -448,7 +464,11 @@ export class Gateway {
return this.connector.write(s);
}
- /** @private */
+ /**
+ * @private
+ * @param {Object} rq - request to be sent to the master container as a JSON object
+ * @returns {Promise} - a promise which returns the response from the master container
+ */
_msgTxRx(rq) {
rq.id = _guid(8);
return new Promise(resolve => {
@@ -470,13 +490,17 @@ export class Gateway {
});
}
- /** @private */
+ /**
+ * @private
+ * @param {URL} url - URL object of the master container to connect to
+ * @returns {TCPConnector|WSConnector} - connector object to connect to the master container
+ */
_createConnector(url){
let conn;
if (url.protocol.startsWith('ws')){
conn = new WSConnector({
'hostname':url.hostname,
- 'port':url.port,
+ 'port':parseInt(url.port),
'pathname':url.pathname,
'keepAlive': this._keepAlive,
'debug': this.debug
@@ -484,7 +508,7 @@ export class Gateway {
}else if (url.protocol.startsWith('tcp')){
conn = new TCPConnector({
'hostname':url.hostname,
- 'port':url.port,
+ 'port':parseInt(url.port),
'keepAlive': this._keepAlive,
'debug': this.debug
});
@@ -502,7 +526,13 @@ export class Gateway {
return conn;
}
- /** @private */
+ /**
+ * Checks if the object is a constructor.
+ *
+ * @private
+ * @param {Object} value - an object to be checked if it is a constructor
+ * @returns {boolean} - if the object is a constructor
+ */
_isConstructor(value) {
try {
new new Proxy(value, {construct() { return {}; }});
@@ -512,7 +542,13 @@ export class Gateway {
}
}
- /** @private */
+ /**
+ * Matches a message with a filter.
+ * @private
+ * @param {string|Object|function} filter - filter to be matched
+ * @param {Message} msg - message to be matched to the filter
+ * @returns {boolean} - true if the message matches the filter
+ */
_matchMessage(filter, msg){
if (typeof filter == 'string' || filter instanceof String) {
return 'inReplyTo' in msg && msg.inReplyTo == filter;
@@ -532,18 +568,25 @@ export class Gateway {
}
}
- /** @private */
+ /**
+ * Gets the next message from the queue that matches the filter.
+ * @private
+ * @param {string|Object|function} filter - filter to be matched
+ */
_getMessageFromQueue(filter) {
if (!this.queue.length) return;
if (!filter) return this.queue.shift();
-
let matchedMsg = this.queue.find( msg => this._matchMessage(filter, msg));
if (matchedMsg) this.queue.splice(this.queue.indexOf(matchedMsg), 1);
-
return matchedMsg;
}
- /** @private */
+ /**
+ * Gets a cached gateway object for the given URL (if it exists).
+ * @private
+ * @param {URL} url - URL object of the master container to connect to
+ * @returns {Gateway|void} - gateway object for the given URL
+ */
_getGWCache(url){
if (!gObj.fjage || !gObj.fjage.gateways) return null;
var f = gObj.fjage.gateways.filter(g => g.connector.url.toString() == url.toString());
@@ -551,13 +594,21 @@ export class Gateway {
return null;
}
- /** @private */
+ /**
+ * Adds a gateway object to the cache if it doesn't already exist.
+ * @private
+ * @param {Gateway} gw - gateway object to be added to the cache
+ */
_addGWCache(gw){
if (!gObj.fjage || !gObj.fjage.gateways) return;
gObj.fjage.gateways.push(gw);
}
- /** @private */
+ /**
+ * Removes a gateway object from the cache if it exists.
+ * @private
+ * @param {Gateway} gw - gateway object to be removed from the cache
+ */
_removeGWCache(gw){
if (!gObj.fjage || !gObj.fjage.gateways) return;
var index = gObj.fjage.gateways.indexOf(gw);
@@ -867,7 +918,7 @@ export const Services = {
/**
* Creates a unqualified message class based on a fully qualified name.
* @param {string} name - fully qualified name of the message class to be created
- * @param {typeof Message} [parent=Message] - class of the parent MessageClass to inherit from
+ * @param {typeof Message} [parent] - class of the parent MessageClass to inherit from
* @constructs Message
* @example
* const ParameterReq = MessageClass('org.arl.fjage.param.ParameterReq');
@@ -970,6 +1021,8 @@ function _decodeBase64(k, d) {
////// global
const GATEWAY_DEFAULTS = {};
+
+/** @type {Window & globalThis & Object} */
let gObj = {};
let DEFAULT_URL;
if (isBrowser || isWebWorker){
@@ -986,7 +1039,7 @@ if (isBrowser || isWebWorker){
DEFAULT_URL = new URL('ws://localhost');
// Enable caching of Gateways
if (typeof gObj.fjage === 'undefined') gObj.fjage = {};
- if (typeof gObj.fjage.gateways == 'undefined')gObj.fjage.gateways = [];
+ if (typeof gObj.fjage.gateways == 'undefined') gObj.fjage.gateways = [];
} else if (isJsDom || isNode){
gObj = global;
Object.assign(GATEWAY_DEFAULTS, {
@@ -1002,4 +1055,21 @@ if (isBrowser || isWebWorker){
gObj.atob = a => Buffer.from(a, 'base64').toString('binary');
}
-const ParameterReq = MessageClass('org.arl.fjage.param.ParameterReq');
+/**
+ * @typedef {Object} ParameterReq.Entry
+ * @property {string} param - parameter name
+ * @property {Object} value - parameter value
+ * @exports ParameterReq.Entry
+ */
+
+
+/**
+ * A message that requests one or more parameters of an agent.
+ * @typedef {Message} ParameterReq
+ * @property {string} param - parameters name to be get/set if only a single parameter is to be get/set
+ * @property {Object} value - parameters value to be set if only a single parameter is to be set
+ * @property {Array} requests - a list of multiple parameters to be get/set
+ * @property {number} [index=-1] - index of parameter(s) to be set
+ * @exports ParameterReq
+ */
+export const ParameterReq = MessageClass('org.arl.fjage.param.ParameterReq');
diff --git a/gateways/js/tsconfig.json b/gateways/js/tsconfig.json
index b0ca08ac..4af0085a 100644
--- a/gateways/js/tsconfig.json
+++ b/gateways/js/tsconfig.json
@@ -1,12 +1,16 @@
{
"compileOnSave": false,
"compilerOptions": {
- "noEmit": true,
+ // "noEmit": true,
"allowJs": true,
"checkJs": true,
- "target": "es6",
+ "declaration": true,
+ "target": "es2020",
"resolveJsonModule": true,
- "moduleResolution": "node"
+ "moduleResolution": "node",
+ "emitDeclarationOnly": true,
+ "outDir": "dist/",
+ "declarationMap": true
},
- "include": ["./src"]
+ "include": ["./src/*.js"]
}
\ No newline at end of file