unetjs 4.0.2 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/unetjs.js CHANGED
@@ -4,32 +4,161 @@
4
4
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.unet = {}));
5
5
  })(this, (function (exports) { 'use strict';
6
6
 
7
- /* fjage.js v2.1.0 */
7
+ /* fjage.js v2.2.2 */
8
8
 
9
- const isBrowser =
10
- typeof window !== "undefined" && typeof window.document !== "undefined";
9
+ /**
10
+ * An action represented by a message. The performative actions are a subset of the
11
+ * FIPA ACL recommendations for interagent communication.
12
+ * @enum {string}
13
+ */
14
+ const Performative = {
15
+ REQUEST: 'REQUEST', // Request an action to be performed
16
+ AGREE: 'AGREE', // Agree to performing the requested action
17
+ REFUSE: 'REFUSE', // Refuse to perform the requested action
18
+ FAILURE: 'FAILURE', // Notification of failure to perform a requested or agreed action
19
+ INFORM: 'INFORM', // Notification of an event
20
+ CONFIRM: 'CONFIRM', // Confirm that the answer to a query is true
21
+ DISCONFIRM: 'DISCONFIRM', // Confirm that the answer to a query is false
22
+ QUERY_IF: 'QUERY_IF', // Query if some statement is true or false
23
+ NOT_UNDERSTOOD: 'NOT_UNDERSTOOD', // Notification that a message was not understood
24
+ CFP: 'CFP', // Call for proposal
25
+ PROPOSE: 'PROPOSE', // Response for CFP
26
+ CANCEL: 'CANCEL' // Cancel pending request
27
+ };
11
28
 
12
- const isNode =
13
- typeof process !== "undefined" &&
14
- process.versions != null &&
15
- process.versions.node != null;
29
+ ////// common utilities
30
+
31
+ // generate random ID with length 4*len characters
32
+ /**
33
+ *
34
+ * @private
35
+ * @param {number} len
36
+ */
37
+ function _guid(len) {
38
+ const s4 = () => Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
39
+ return Array.from({ length: len }, s4).join('');
40
+ }
16
41
 
17
- const isWebWorker =
18
- typeof self === "object" &&
19
- self.constructor &&
20
- self.constructor.name === "DedicatedWorkerGlobalScope";
21
42
 
22
43
  /**
23
- * @see https://github.com/jsdom/jsdom/releases/tag/12.0.0
24
- * @see https://github.com/jsdom/jsdom/issues/1537
44
+ * A simple and lightweight implementation of UUIDv7.
45
+ *
46
+ * UUIDv7 is a time-based UUID version that is lexicographically sortable and
47
+ * is designed to be used as a database key.
48
+ *
49
+ * The structure is as follows:
50
+ * 0 1 2 3
51
+ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
52
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
53
+ * | unix_ts_ms |
54
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
55
+ * | unix_ts_ms | ver | rand_a |
56
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
57
+ * |var| rand_b |
58
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
59
+ * | rand_b |
60
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
61
+ *
62
+ * - unix_ts_ms (48 bits): Unix timestamp in milliseconds.
63
+ * - ver (4 bits): Version, set to 7.
64
+ * - rand_a (12 bits): Random data.
65
+ * - var (2 bits): Variant, set to '10'.
66
+ * - rand_b (62 bits): Random data.
25
67
  */
26
- const isJsDom =
27
- (typeof window !== "undefined" && window.name === "nodejs") ||
28
- (typeof navigator !== "undefined" &&
29
- (navigator.userAgent.includes("Node.js") ||
30
- navigator.userAgent.includes("jsdom")));
68
+ class UUID7 {
69
+ /**
70
+ * Private constructor to create a UUID7 from a byte array.
71
+ * @param {Uint8Array} bytes The 16 bytes of the UUID.
72
+ */
73
+ constructor(bytes) {
74
+ if (bytes.length !== 16) {
75
+ throw new Error('UUID7 must be constructed with a 16-byte array.');
76
+ }
77
+ this.bytes = bytes;
78
+ }
79
+
80
+ /**
81
+ * Generates a new UUIDv7.
82
+ * @returns {UUID7} A new UUIDv7 instance.
83
+ */
84
+ static generate() {
85
+ const bytes = new Uint8Array(16);
86
+ const randomBytes = crypto.getRandomValues(new Uint8Array(10));
87
+ const timestamp = Date.now();
88
+
89
+ // Set the 48-bit timestamp
90
+ // JavaScript numbers are 64-bit floats, but bitwise operations treat them
91
+ // as 32-bit signed integers. We need to handle the 48-bit timestamp carefully.
92
+ const timestampHi = Math.floor(timestamp / 2 ** 16);
93
+ const timestampLo = timestamp % 2 ** 16;
94
+
95
+ bytes[0] = (timestampHi >> 24) & 0xff;
96
+ bytes[1] = (timestampHi >> 16) & 0xff;
97
+ bytes[2] = (timestampHi >> 8) & 0xff;
98
+ bytes[3] = timestampHi & 0xff;
99
+ bytes[4] = (timestampLo >> 8) & 0xff;
100
+ bytes[5] = timestampLo & 0xff;
101
+
102
+ // Copy the 10 random bytes
103
+ bytes.set(randomBytes, 6);
104
+
105
+ // Set the 4-bit version (0111) in byte 6
106
+ bytes[6] = (bytes[6] & 0x0f) | 0x70;
107
+
108
+ // Set the 2-bit variant (10) in byte 8
109
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
110
+
111
+ return new UUID7(bytes);
112
+ }
113
+
114
+ /**
115
+ * Extracts the timestamp from the UUID.
116
+ * @returns {number} The Unix timestamp in milliseconds.
117
+ */
118
+ getTimestamp() {
119
+ let timestamp = 0;
120
+ timestamp = this.bytes[0] * 2 ** 40;
121
+ timestamp += this.bytes[1] * 2 ** 32;
122
+ timestamp += this.bytes[2] * 2 ** 24;
123
+ timestamp += this.bytes[3] * 2 ** 16;
124
+ timestamp += this.bytes[4] * 2 ** 8;
125
+ timestamp += this.bytes[5];
126
+ return timestamp;
127
+ }
128
+
129
+ /**
130
+ * Formats the UUID into the standard string representation.
131
+ * @returns {string} The UUID string.
132
+ */
133
+ toString() {
134
+ let result = '';
135
+ for (let i = 0; i < 16; i++) {
136
+ result += this.bytes[i].toString(16).padStart(2, '0');
137
+ if (i === 3 || i === 5 || i === 7 || i === 9) {
138
+ result += '-';
139
+ }
140
+ }
141
+ return result;
142
+ }
143
+ }
31
144
 
32
- typeof Deno !== "undefined" && typeof Deno.core !== "undefined";
145
+ // src/index.ts
146
+ var isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined";
147
+ var isNode = (
148
+ // @ts-expect-error
149
+ typeof process !== "undefined" && // @ts-expect-error
150
+ process.versions != null && // @ts-expect-error
151
+ process.versions.node != null
152
+ );
153
+ var isWebWorker = typeof self === "object" && self.constructor && self.constructor.name === "DedicatedWorkerGlobalScope";
154
+ var isJsDom = typeof window !== "undefined" && window.name === "nodejs" || typeof navigator !== "undefined" && "userAgent" in navigator && typeof navigator.userAgent === "string" && (navigator.userAgent.includes("Node.js") || navigator.userAgent.includes("jsdom"));
155
+ (
156
+ // @ts-expect-error
157
+ typeof Deno !== "undefined" && // @ts-expect-error
158
+ typeof Deno.version !== "undefined" && // @ts-expect-error
159
+ typeof Deno.version.deno !== "undefined"
160
+ );
161
+ typeof process !== "undefined" && process.versions != null && process.versions.bun != null;
33
162
 
34
163
  const SOCKET_OPEN = 'open';
35
164
  const SOCKET_OPENING = 'opening';
@@ -38,20 +167,20 @@
38
167
  var createConnection;
39
168
 
40
169
  /**
41
- * @class
42
- * @ignore
43
- */
170
+ * @class
171
+ * @ignore
172
+ */
44
173
  class TCPConnector {
45
174
 
46
175
  /**
47
- * Create an TCPConnector to connect to a fjage master over TCP
48
- * @param {Object} opts
49
- * @param {string} [opts.hostname='localhost'] - hostname/ip address of the master container to connect to
50
- * @param {number} [opts.port=1100] - port number of the master container to connect to
51
- * @param {boolean} [opts.keepAlive=true] - try to reconnect if the connection is lost
52
- * @param {boolean} [opts.debug=false] - debug info to be logged to console?
53
- * @param {number} [opts.reconnectTime=5000] - time before reconnection is attempted after an error
54
- */
176
+ * Create an TCPConnector to connect to a fjage master over TCP
177
+ * @param {Object} opts
178
+ * @param {string} [opts.hostname='localhost'] - hostname/ip address of the master container to connect to
179
+ * @param {number} [opts.port=1100] - port number of the master container to connect to
180
+ * @param {boolean} [opts.keepAlive=true] - try to reconnect if the connection is lost
181
+ * @param {boolean} [opts.debug=false] - debug info to be logged to console?
182
+ * @param {number} [opts.reconnectTime=5000] - time before reconnection is attempted after an error
183
+ */
55
184
  constructor(opts = {}) {
56
185
  let host = opts.hostname || 'localhost';
57
186
  let port = opts.port || 1100;
@@ -146,10 +275,10 @@
146
275
  }
147
276
 
148
277
  /**
149
- * Write a string to the connector
150
- * @param {string} s - string to be written out of the connector to the master
151
- * @return {boolean} - true if connect was able to write or queue the string to the underlying socket
152
- */
278
+ * Write a string to the connector
279
+ * @param {string} s - string to be written out of the connector to the master
280
+ * @return {boolean} - true if connect was able to write or queue the string to the underlying socket
281
+ */
153
282
  write(s){
154
283
  if (!this.sock || this.sock.readyState == SOCKET_OPENING){
155
284
  this.pendingOnOpen.push(() => {
@@ -164,32 +293,32 @@
164
293
  }
165
294
 
166
295
  /**
167
- * @callback TCPConnectorReadCallback
168
- * @ignore
169
- * @param {string} s - incoming message string
170
- */
296
+ * @callback TCPConnectorReadCallback
297
+ * @ignore
298
+ * @param {string} s - incoming message string
299
+ */
171
300
 
172
301
  /**
173
- * Set a callback for receiving incoming strings from the connector
174
- * @param {TCPConnectorReadCallback} cb - callback that is called when the connector gets a string
175
- */
302
+ * Set a callback for receiving incoming strings from the connector
303
+ * @param {TCPConnectorReadCallback} cb - callback that is called when the connector gets a string
304
+ */
176
305
  setReadCallback(cb){
177
306
  if (cb && {}.toString.call(cb) === '[object Function]') this._onSockRx = cb;
178
307
  }
179
308
 
180
309
  /**
181
- * Add listener for connection events
182
- * @param {function} listener - a listener callback that is called when the connection is opened/closed
183
- */
310
+ * Add listener for connection events
311
+ * @param {function} listener - a listener callback that is called when the connection is opened/closed
312
+ */
184
313
  addConnectionListener(listener){
185
314
  this.connListeners.push(listener);
186
315
  }
187
316
 
188
317
  /**
189
- * Remove listener for connection events
190
- * @param {function} listener - remove the listener for connection
191
- * @return {boolean} - true if the listner was removed successfully
192
- */
318
+ * Remove listener for connection events
319
+ * @param {function} listener - remove the listener for connection
320
+ * @return {boolean} - true if the listner was removed successfully
321
+ */
193
322
  removeConnectionListener(listener) {
194
323
  let ndx = this.connListeners.indexOf(listener);
195
324
  if (ndx >= 0) {
@@ -200,8 +329,8 @@
200
329
  }
201
330
 
202
331
  /**
203
- * Close the connector
204
- */
332
+ * Close the connector
333
+ */
205
334
  close(){
206
335
  if (!this.sock) return;
207
336
  if (this.sock.readyState == SOCKET_OPENING) {
@@ -225,21 +354,21 @@
225
354
  const DEFAULT_RECONNECT_TIME = 5000; // ms, delay between retries to connect to the server.
226
355
 
227
356
  /**
228
- * @class
229
- * @ignore
230
- */
357
+ * @class
358
+ * @ignore
359
+ */
231
360
  class WSConnector {
232
361
 
233
362
  /**
234
- * Create an WSConnector to connect to a fjage master over WebSockets
235
- * @param {Object} opts
236
- * @param {string} [opts.hostname='localhost'] - hostname/ip address of the master container to connect to
237
- * @param {number} [opts.port=80] - port number of the master container to connect to
238
- * @param {string} [opts.pathname="/"] - path of the master container to connect to
239
- * @param {boolean} [opts.keepAlive=true] - try to reconnect if the connection is lost
240
- * @param {boolean} [opts.debug=false] - debug info to be logged to console?
241
- * @param {number} [opts.reconnectTime=5000] - time before reconnection is attempted after an error
242
- */
363
+ * Create an WSConnector to connect to a fjage master over WebSockets
364
+ * @param {Object} opts
365
+ * @param {string} [opts.hostname='localhost'] - hostname/ip address of the master container to connect to
366
+ * @param {number} [opts.port=80] - port number of the master container to connect to
367
+ * @param {string} [opts.pathname="/"] - path of the master container to connect to
368
+ * @param {boolean} [opts.keepAlive=true] - try to reconnect if the connection is lost
369
+ * @param {boolean} [opts.debug=false] - debug info to be logged to console?
370
+ * @param {number} [opts.reconnectTime=5000] - time before reconnection is attempted after an error
371
+ */
243
372
  constructor(opts = {}) {
244
373
  let host = opts.hostname || 'localhost';
245
374
  let port = opts.port || 80;
@@ -304,9 +433,9 @@
304
433
  }
305
434
 
306
435
  /**
307
- * Write a string to the connector
308
- * @param {string} s - string to be written out of the connector to the master
309
- */
436
+ * Write a string to the connector
437
+ * @param {string} s - string to be written out of the connector to the master
438
+ */
310
439
  write(s){
311
440
  if (!this.sock || this.sock.readyState == this.sock.CONNECTING){
312
441
  this.pendingOnOpen.push(() => {
@@ -321,33 +450,33 @@
321
450
  }
322
451
 
323
452
  /**
324
- * @callback WSConnectorReadCallback
325
- * @ignore
326
- * @param {string} s - incoming message string
327
- */
453
+ * @callback WSConnectorReadCallback
454
+ * @ignore
455
+ * @param {string} s - incoming message string
456
+ */
328
457
 
329
458
  /**
330
- * Set a callback for receiving incoming strings from the connector
331
- * @param {WSConnectorReadCallback} cb - callback that is called when the connector gets a string
332
- * @ignore
333
- */
459
+ * Set a callback for receiving incoming strings from the connector
460
+ * @param {WSConnectorReadCallback} cb - callback that is called when the connector gets a string
461
+ * @ignore
462
+ */
334
463
  setReadCallback(cb){
335
464
  if (cb && {}.toString.call(cb) === '[object Function]') this._onWebsockRx = cb;
336
465
  }
337
466
 
338
467
  /**
339
- * Add listener for connection events
340
- * @param {function} listener - a listener callback that is called when the connection is opened/closed
341
- */
468
+ * Add listener for connection events
469
+ * @param {function} listener - a listener callback that is called when the connection is opened/closed
470
+ */
342
471
  addConnectionListener(listener){
343
472
  this.connListeners.push(listener);
344
473
  }
345
474
 
346
475
  /**
347
- * Remove listener for connection events
348
- * @param {function} listener - remove the listener for connection
349
- * @return {boolean} - true if the listner was removed successfully
350
- */
476
+ * Remove listener for connection events
477
+ * @param {function} listener - remove the listener for connection
478
+ * @return {boolean} - true if the listner was removed successfully
479
+ */
351
480
  removeConnectionListener(listener) {
352
481
  let ndx = this.connListeners.indexOf(listener);
353
482
  if (ndx >= 0) {
@@ -358,8 +487,8 @@
358
487
  }
359
488
 
360
489
  /**
361
- * Close the connector
362
- */
490
+ * Close the connector
491
+ */
363
492
  close(){
364
493
  if (!this.sock) return;
365
494
  if (this.sock.readyState == this.sock.CONNECTING) {
@@ -376,328 +505,389 @@
376
505
  }
377
506
  }
378
507
 
379
- /* global global Buffer */
380
-
381
-
382
- const DEFAULT_QUEUE_SIZE = 128; // max number of old unreceived messages to store
508
+ /* global Buffer */
383
509
 
384
510
  /**
385
- * An action represented by a message. The performative actions are a subset of the
386
- * FIPA ACL recommendations for interagent communication.
387
- * @enum {string}
388
- */
389
- const Performative = {
390
- REQUEST: 'REQUEST', // Request an action to be performed
391
- AGREE: 'AGREE', // Agree to performing the requested action
392
- REFUSE: 'REFUSE', // Refuse to perform the requested action
393
- FAILURE: 'FAILURE', // Notification of failure to perform a requested or agreed action
394
- INFORM: 'INFORM', // Notification of an event
395
- CONFIRM: 'CONFIRM', // Confirm that the answer to a query is true
396
- DISCONFIRM: 'DISCONFIRM', // Confirm that the answer to a query is false
397
- QUERY_IF: 'QUERY_IF', // Query if some statement is true or false
398
- NOT_UNDERSTOOD: 'NOT_UNDERSTOOD', // Notification that a message was not understood
399
- CFP: 'CFP', // Call for proposal
400
- PROPOSE: 'PROPOSE', // Response for CFP
401
- CANCEL: 'CANCEL' // Cancel pending request
402
- };
403
-
404
- /**
405
- * An identifier for an agent or a topic.
406
- * @class
407
- * @param {string} name - name of the agent
408
- * @param {boolean} [topic=false] - name of topic
409
- * @param {Gateway} [owner] - Gateway owner for this AgentID
410
- */
411
- class AgentID {
412
-
413
-
414
- constructor(name, topic=false, owner) {
415
- this.name = name;
416
- this.topic = topic;
417
- this.owner = owner;
418
- }
511
+ * Class representing a fjage's on-the-wire JSON message. A JSONMessage object
512
+ * contains all the fields that can be a part of a fjage JSON message. The class
513
+ * provides methods to create JSONMessage objects from raw strings and to
514
+ * convert JSONMessage objects to JSON strings in the format of the fjage on-the-wire
515
+ * protocol. See {@link https://fjage.readthedocs.io/en/latest/protocol.html#json-message-request-response-attributes fjage documentation}
516
+ * for more details on the JSON message format.
517
+ *
518
+ * Most users will not need to create JSONMessage objects directly, but rather use the Gateway and Message classes
519
+ * to send and receive messages. However, this class can be useful for low-level access to the fjage protocol
520
+ * or for generating/consuming the fjåge protocol messages without having them be transmitted over a network.
521
+ *
522
+ * @example
523
+ * const jsonMsg = new JSONMessage();
524
+ * jsonMsg.action = 'send';
525
+ * jsonMsg.message = new Message();
526
+ * jsonMsg.message.sender = new AgentID('agent1');
527
+ * jsonMsg.message.recipient = new AgentID('agent2');
528
+ * jsonMsg.message.perf = Performative.INFORM;
529
+ * jsonMsg.toJSON(); // Converts to JSON string in the fjage on-the-wire protocol format
530
+ *
531
+ * @example
532
+ * const jsonString = '{"id":"1234",...}'; // JSON string representation of a JSONMessage
533
+ * const jsonMsg = new JSONMessage(jsonString); // Parses the JSON string into a JSONMessage object
534
+ * jsonMsg.message; // Access the Message object contained in the JSONMessage
535
+ *
536
+ * @class
537
+ * @property {string} [id] - A UUID assigned to each JSONMessage object.
538
+ * @property {string} [action] - Denotes the main action the object is supposed to perform.
539
+ * @property {string} [inResponseTo] - This attribute contains the action to which this object is a response to.
540
+ * @property {AgentID} [agentID] - An AgentID. This attribute is populated in objects which are responses to objects requesting the ID of an agent providing a specific service.
541
+ * @property {Array<AgentID>} [agentIDs] - This attribute is populated in objects which are responses to objects requesting the IDs of agents providing a specific service, or objects which are responses to objects requesting a list of all agents running in a container.
542
+ * @property {Array<string>} [agentTypes] - This attribute is optionally populated in objects which are responses to objects requesting a list of all agents running in a container. If populated, it contains a list of agent types running in the container, with a one-to-one mapping to the agent IDs in the "agentIDs" attribute.
543
+ * @property {string} [service] - Used in conjunction with "action" : "agentForService" and "action" : "agentsForService" to query for agent(s) providing this specific service.
544
+ * @property {Array<string>} [services] - This attribute is populated in objects which are responses to objects requesting the services available with "action" : "services".
545
+ * @property {boolean} [answer] - This attribute is populated in objects which are responses to query objects with "action" : "containsAgent".
546
+ * @property {Message} [message] - This holds the main payload of the message. The structure and format of this object is discussed in the {@link https://fjage.readthedocs.io/en/latest/protocol.html#json-message-request-response-attributes fjage documentation}.
547
+ * @property {boolean} [relay] - This attribute defines if the target container should relay (forward) the message to other containers it is connected to or not.
548
+ * @property {Object} [creds] - Credentials to be used for authentication.
549
+ * @property {Object} [auth] - Authentication information to be used for the message.
550
+ *
551
+ *
552
+ */
553
+ class JSONMessage {
419
554
 
420
555
  /**
421
- * Gets the name of the agent or topic.
422
- *
423
- * @returns {string} - name of agent or topic
424
- */
425
- getName() {
426
- return this.name;
427
- }
556
+ * @param {String} [jsonString] - JSON string to be parsed into a JSONMessage object.
557
+ * @param {Object} [owner] - The owner of the JSONMessage object, typically the Gateway instance.
558
+ */
559
+ constructor(jsonString, owner) {
560
+ this.id = UUID7.generate().toString(); // unique JSON message ID
561
+ this.action = null;
562
+ this.inResponseTo = null;
563
+ this.agentID = null;
564
+ this.agentIDs = null;
565
+ this.agentTypes = null;
566
+ this.service = null;
567
+ this.services = null;
568
+ this.answer = null;
569
+ this.message = null;
570
+ this.relay = null;
571
+ this.creds = null;
572
+ this.auth = null;
573
+ this.name = null;
574
+ if (jsonString && typeof jsonString === 'string') {
575
+ try {
576
+ const parsed = JSON.parse(jsonString, _decodeBase64);
577
+ if (parsed.message) parsed.message = Message.fromJSON(parsed.message);
578
+ if (parsed.agentID) parsed.agentID = AgentID.fromJSON(parsed.agentID, owner);
579
+ if (parsed.agentIDs) parsed.agentIDs = parsed.agentIDs.map(id => AgentID.fromJSON(id, owner));
580
+ Object.assign(this, parsed);
581
+ } catch (e) {
582
+ throw new Error('Invalid JSON string: ' + e.message);
583
+ }
584
+ } }
428
585
 
429
586
  /**
430
- * Returns true if the agent id represents a topic.
431
- *
432
- * @returns {boolean} - true if the agent id represents a topic, false if it represents an agent
433
- */
434
- isTopic() {
435
- return this.topic;
587
+ * Creates a JSONMessage object to send a message.
588
+ *
589
+ * @param {Message} msg
590
+ * @param {boolean} [relay=false] - whether to relay the message
591
+ * @returns {JSONMessage} - JSONMessage object with request to send a message
592
+ */
593
+ static createSend(msg, relay=false){
594
+ if (!(msg instanceof Message)) {
595
+ throw new Error('Invalid message type');
596
+ }
597
+ const jsonMsg = new JSONMessage();
598
+ jsonMsg.action = Actions.SEND;
599
+ jsonMsg.relay = relay;
600
+ jsonMsg.message = msg;
601
+ return jsonMsg;
436
602
  }
437
603
 
438
604
  /**
439
- * Sends a message to the agent represented by this id.
440
- *
441
- * @param {Message} msg - message to send
442
- * @returns {void}
443
- */
444
- send(msg) {
445
- msg.recipient = this.toJSON();
446
- if (this.owner) this.owner.send(msg);
447
- else throw new Error('Unowned AgentID cannot send messages');
605
+ * Creates a JSONMessage object to update WantsMessagesFor list.
606
+ *
607
+ * @param {Array<AgentID>} agentIDs - array of AgentID objects for which the gateway wants messages
608
+ * @returns {JSONMessage} - JSONMessage object with request to update WantsMessagesFor list
609
+ */
610
+ static createWantsMessagesFor(agentIDs) {
611
+ if (!Array.isArray(agentIDs) || agentIDs.length === 0) {
612
+ throw new Error('agentIDNames must be a non-empty array');
613
+ }
614
+ const jsonMsg = new JSONMessage();
615
+ jsonMsg.action = Actions.WANTS_MESSAGES_FOR;
616
+ jsonMsg.agentIDs = agentIDs;
617
+ return jsonMsg;
448
618
  }
449
619
 
450
620
  /**
451
- * Sends a request to the agent represented by this id and waits for a reponse.
452
- *
453
- * @param {Message} msg - request to send
454
- * @param {number} [timeout=1000] - timeout in milliseconds
455
- * @returns {Promise<Message>} - response
456
- */
457
- async request(msg, timeout=1000) {
458
- msg.recipient = this.toJSON();
459
- if (this.owner) return this.owner.request(msg, timeout);
460
- else throw new Error('Unowned AgentID cannot send messages');
621
+ * Creates a JSONMessage object to request the list of agents.
622
+ *
623
+ * @returns {JSONMessage} - JSONMessage object with request for the list of agents
624
+ */
625
+ static createAgents(){
626
+ const jsonMsg = new JSONMessage();
627
+ jsonMsg.action = Actions.AGENTS;
628
+ jsonMsg.id = UUID7.generate().toString(); // unique JSON message ID
629
+ return jsonMsg;
461
630
  }
462
631
 
463
632
  /**
464
- * Gets a string representation of the agent id.
465
- *
466
- * @returns {string} - string representation of the agent id
467
- */
468
- toString() {
469
- return this.toJSON() + ((this.owner && this.owner.connector) ? ` on ${this.owner.connector.url}` : '');
633
+ * Creates a JSONMessage object to check if an agent is contained
634
+ *
635
+ * @param {AgentID} agentID - AgentID of the agent to check
636
+ * @returns {JSONMessage} - JSONMessage object with request to check if the agent is contained
637
+ */
638
+ static createContainsAgent(agentID) {
639
+ if (!(agentID instanceof AgentID)) {
640
+ throw new Error('agentID must be an instance of AgentID');
641
+ }
642
+ const jsonMsg = new JSONMessage();
643
+ jsonMsg.action = Actions.CONTAINS_AGENT;
644
+ jsonMsg.id = UUID7.generate().toString(); // unique JSON message ID
645
+ jsonMsg.agentID = agentID;
646
+ return jsonMsg;
470
647
  }
471
648
 
472
649
  /**
473
- * Gets a JSON string representation of the agent id.
474
- *
475
- * @returns {string} - JSON string representation of the agent id
476
- */
477
- toJSON() {
478
- return (this.topic ? '#' : '') + this.name;
650
+ * Creates a JSONMessage object to get an agent for a service.
651
+ *
652
+ * @param {string} service - service which the agent must provide
653
+ * @returns {JSONMessage} - JSONMessage object with request for an agent providing the service
654
+ */
655
+ static createAgentForService(service) {
656
+ if (typeof service !== 'string' || service.length === 0) {
657
+ throw new Error('service must be a non-empty string');
658
+ }
659
+ const jsonMsg = new JSONMessage();
660
+ jsonMsg.action = Actions.AGENT_FOR_SERVICE;
661
+ jsonMsg.id = UUID7.generate().toString(); // unique JSON message ID
662
+ jsonMsg.service = service;
663
+ return jsonMsg;
479
664
  }
480
665
 
481
666
  /**
482
- * Sets parameter(s) on the Agent referred to by this AgentID.
483
- *
484
- * @param {(string|string[])} params - parameters name(s) to be set
485
- * @param {(Object|Object[])} values - parameters value(s) to be set
486
- * @param {number} [index=-1] - index of parameter(s) to be set
487
- * @param {number} [timeout=5000] - timeout for the response
488
- * @returns {Promise<(Object|Object[])>} - a promise which returns the new value(s) of the parameters
489
- */
490
- async set (params, values, index=-1, timeout=5000) {
491
- if (!params) return null;
492
- let msg = new ParameterReq();
493
- msg.recipient = this.name;
494
- if (Array.isArray(params)){
495
- msg.param = params.shift();
496
- msg.value = values.shift();
497
- msg.requests = params.map((p, i) => {
498
- return {
499
- 'param': p,
500
- 'value': values[i]
501
- };
502
- });
503
- // Add back for generating a response
504
- params.unshift(msg.param);
505
- } else {
506
- msg.param = params;
507
- msg.value = values;
508
- }
509
- msg.index = Number.isInteger(index) ? index : -1;
510
- const rsp = await this.owner.request(msg, timeout);
511
- var ret = Array.isArray(params) ? new Array(params.length).fill(null) : null;
512
- if (!rsp || rsp.perf != Performative.INFORM || !rsp.param) {
513
- if (this.owner._returnNullOnFailedResponse) return ret;
514
- else throw new Error(`Unable to set ${this.name}.${params} to ${values}`);
515
- }
516
- if (Array.isArray(params)) {
517
- if (!rsp.values) rsp.values = {};
518
- if (rsp.param) rsp.values[rsp.param] = rsp.value;
519
- const rkeys = Object.keys(rsp.values);
520
- return params.map( p => {
521
- if (p.includes('.')) p = p.split('.').pop();
522
- let f = rkeys.find(k => (k.includes('.') ? k.split('.').pop() : k) == p);
523
- return f ? rsp.values[f] : undefined;
524
- });
525
- } else {
526
- return rsp.value;
667
+ * Creates a JSONMessage object to get all agents for a service.
668
+ *
669
+ * @param {string} service - service which the agents must provide
670
+ * @returns {JSONMessage} - JSONMessage object with request for all agent providing the service
671
+ */
672
+ static createAgentsForService(service) {
673
+ if (typeof service !== 'string' || service.length === 0) {
674
+ throw new Error('service must be a non-empty string');
527
675
  }
676
+ const jsonMsg = new JSONMessage();
677
+ jsonMsg.action = Actions.AGENTS_FOR_SERVICE;
678
+ jsonMsg.id = UUID7.generate().toString(); // unique JSON message ID
679
+ jsonMsg.service = service;
680
+ return jsonMsg;
528
681
  }
529
682
 
530
-
531
683
  /**
532
- * Gets parameter(s) on the Agent referred to by this AgentID.
533
- *
534
- * @param {(?string|?string[])} params - parameters name(s) to be get, null implies get value of all parameters on the Agent
535
- * @param {number} [index=-1] - index of parameter(s) to be get
536
- * @param {number} [timeout=5000] - timeout for the response
537
- * @returns {Promise<(?Object|?Object[])>} - a promise which returns the value(s) of the parameters
538
- */
539
- async get(params, index=-1, timeout=5000) {
540
- let msg = new ParameterReq();
541
- msg.recipient = this.name;
542
- if (params){
543
- if (Array.isArray(params)) {
544
- msg.param = params.shift();
545
- msg.requests = params.map(p => {return {'param': p};});
546
- // Add back for generating a response
547
- params.unshift(msg.param);
548
- }
549
- else msg.param = params;
684
+ * Converts the JSONMessage object to a JSON string in the format of the
685
+ * fjage on-the-wire protocol. If the JSONMessage contains a Message or
686
+ * AgentID objects, they will be serialized as per the fjåge protocol.
687
+ *
688
+ * @returns {string} - JSON string representation of the message
689
+ */
690
+ toJSON() {
691
+ if (!this.action && !this.id) {
692
+ throw new Error('Neither action nor id is set. Cannot serialize JSONMessage.');
550
693
  }
551
- msg.index = Number.isInteger(index) ? index : -1;
552
- const rsp = await this.owner.request(msg, timeout);
553
- var ret = Array.isArray(params) ? new Array(params.length).fill(null) : null;
554
- if (!rsp || rsp.perf != Performative.INFORM || !rsp.param) {
555
- if (this.owner._returnNullOnFailedResponse) return ret;
556
- else throw new Error(`Unable to get ${this.name}.${params}`);
694
+ const jsonObj = {};
695
+ // Add property if not null or undefined
696
+ if (this.id) jsonObj.id = this.id;
697
+ if (this.action) jsonObj.action = this.action;
698
+ if (this.inResponseTo) jsonObj.inResponseTo = this.inResponseTo;
699
+ if (this.agentID) jsonObj.agentID = this.agentID.toJSON();
700
+ if (this.agentIDs) {
701
+ jsonObj.agentIDs = this.agentIDs.map(id => id.toJSON());
702
+ if (jsonObj.agentIDs.length === 0) delete jsonObj.agentIDs; // remove empty array
557
703
  }
558
- // Request for listing of all parameters.
559
- if (!params) {
560
- if (!rsp.values) rsp.values = {};
561
- if (rsp.param) rsp.values[rsp.param] = rsp.value;
562
- return rsp.values;
563
- } else if (Array.isArray(params)) {
564
- if (!rsp.values) rsp.values = {};
565
- if (rsp.param) rsp.values[rsp.param] = rsp.value;
566
- const rkeys = Object.keys(rsp.values);
567
- return params.map( p => {
568
- if (p.includes('.')) p = p.split('.').pop();
569
- let f = rkeys.find(k => (k.includes('.') ? k.split('.').pop() : k) == p);
570
- return f ? rsp.values[f] : undefined;
571
- });
572
- } else {
573
- return rsp.value;
704
+ if (this.service) jsonObj.service = this.service;
705
+ if (this.services) {
706
+ jsonObj.services = this.services;
707
+ if (jsonObj.services.length === 0) delete jsonObj.services; // remove empty array
574
708
  }
709
+ if (this.answer != undefined) jsonObj.answer = this.answer;
710
+ if (this.message) jsonObj.message = this.message;
711
+ if (this.relay) jsonObj.relay = this.relay;
712
+ if (this.creds) jsonObj.creds = this.creds;
713
+ if (this.auth) jsonObj.auth = this.auth;
714
+ if (this.name) jsonObj.name = this.name;
715
+ return JSON.stringify(jsonObj);
716
+ }
717
+
718
+ toString() {
719
+ return this.toJSON();
575
720
  }
576
721
  }
577
722
 
578
- // protected String msgID = UUID.randomUUID().toString();
579
- // protected Performative perf;
580
- // protected AgentID recipient;
581
- // protected AgentID sender = null;
582
- // protected String inReplyTo = null;
583
- // protected Long sentAt = null;
584
723
 
585
724
  /**
586
- * Base class for messages transmitted by one agent to another. Creates an empty message.
587
- * @class
588
- * @param {string} [msgID] - unique identifier for the message
589
- * @param {Performative} [perf=Performative.INFORM] - performative
590
- * @param {AgentID} [recipient] - recipient of the message
591
- * @param {AgentID} [sender] - sender of the message
592
- * @param {string} [inReplyTo] - message to which this response corresponds to
593
- * @param {Message} [inReplyTo] - message to which this response corresponds to
594
- * @param {number} [sentAt] - time at which the message was sent
595
- */
596
- class Message {
725
+ * Actions supported by the fjåge JSON message protocol. See
726
+ * {@link https://fjage.readthedocs.io/en/latest/protocol.html#json-message-request-response-attributes fjage documentation} for more details.
727
+ *
728
+ * @enum {string} Actions
729
+ */
730
+ const Actions = {
731
+ AGENTS : 'agents',
732
+ CONTAINS_AGENT : 'containsAgent',
733
+ AGENT_FOR_SERVICE : 'agentForService',
734
+ AGENTS_FOR_SERVICE : 'agentsForService',
735
+ SEND : 'send',
736
+ WANTS_MESSAGES_FOR : 'wantsMessagesFor'};
597
737
 
598
- constructor(inReplyTo={msgID:null, sender:null}, perf=Performative.INFORM) {
599
- this.__clazz__ = 'org.arl.fjage.Message';
600
- this.msgID = _guid(8);
601
- this.sender = null;
602
- this.recipient = inReplyTo.sender;
603
- this.perf = perf;
604
- this.inReplyTo = inReplyTo.msgID || null;
605
- }
738
+ ////// private utilities
606
739
 
607
- /**
608
- * Gets a string representation of the message.
609
- *
610
- * @returns {string} - string representation
611
- */
612
- toString() {
613
- let s = '';
614
- let suffix = '';
615
- if (!this.__clazz__) return '';
616
- let clazz = this.__clazz__;
617
- clazz = clazz.replace(/^.*\./, '');
618
- let perf = this.perf;
619
- for (var k in this) {
620
- if (k.startsWith('__')) continue;
621
- if (k == 'sender') continue;
622
- if (k == 'recipient') continue;
623
- if (k == 'msgID') continue;
624
- if (k == 'perf') continue;
625
- if (k == 'inReplyTo') continue;
626
- if (typeof this[k] == 'object') {
627
- suffix = ' ...';
628
- continue;
629
- }
630
- s += ' ' + k + ':' + this[k];
631
- }
632
- s += suffix;
633
- return clazz+':'+perf+'['+s.replace(/^ /, '')+']';
634
- }
635
740
 
636
- // convert a message into a JSON string
637
- // NOTE: we don't do any base64 encoding for TX as
638
- // we don't know what data type is intended
639
- /**
640
- * @private
641
- *
642
- * @return {string} - JSON string representation of the message
643
- */
644
- _serialize() {
645
- let clazz = this.__clazz__ || 'org.arl.fjage.Message';
646
- let data = JSON.stringify(this, (k,v) => {
647
- if (k.startsWith('__')) return;
648
- return v;
649
- });
650
- return '{ "clazz": "'+clazz+'", "data": '+data+' }';
741
+ /**
742
+ * Decode large numeric arrays encoded in base64 back to array format.
743
+ *
744
+ * @private
745
+ *
746
+ * @param {string} _k - key (unused)
747
+ * @param {any} d - data
748
+ * @returns {Array} - decoded data in array format
749
+ * */
750
+ function _decodeBase64(_k, d) {
751
+ if (d === null) return null;
752
+ if (typeof d == 'object' && 'clazz' in d && 'data' in d && d.clazz.startsWith('[') && d.clazz.length == 2) {
753
+ return _b64toArray(d.data, d.clazz) || d;
651
754
  }
755
+ return d;
756
+ }
652
757
 
653
- // inflate a data dictionary into the message
654
- /** @private */
655
- _inflate(data) {
656
- for (var key in data)
657
- this[key] = data[key];
758
+ /**
759
+ * Convert a base64 encoded string to an array of numbers of the specified data type.
760
+ *
761
+ * @private
762
+ *
763
+ * @param {string} base64 - base64 encoded string
764
+ * @param {string} dtype - data type, e.g. '[B' for byte array, '[S' for short array, etc.
765
+ * @param {boolean} [littleEndian=true] - whether to use little-endian byte order
766
+ */
767
+ function _b64toArray(base64, dtype, littleEndian=true) {
768
+ let s = _atob(base64);
769
+ let len = s.length;
770
+ let bytes = new Uint8Array(len);
771
+ for (var i = 0; i < len; i++)
772
+ bytes[i] = s.charCodeAt(i);
773
+ let rv = [];
774
+ let view = new DataView(bytes.buffer);
775
+ switch (dtype) {
776
+ case '[B': // byte array
777
+ for (i = 0; i < len; i++)
778
+ rv.push(view.getUint8(i));
779
+ break;
780
+ case '[S': // short array
781
+ for (i = 0; i < len; i+=2)
782
+ rv.push(view.getInt16(i, littleEndian));
783
+ break;
784
+ case '[I': // integer array
785
+ for (i = 0; i < len; i+=4)
786
+ rv.push(view.getInt32(i, littleEndian));
787
+ break;
788
+ case '[J': // long array
789
+ for (i = 0; i < len; i+=8)
790
+ rv.push(view.getBigInt64(i, littleEndian));
791
+ break;
792
+ case '[F': // float array
793
+ for (i = 0; i < len; i+=4)
794
+ rv.push(view.getFloat32(i, littleEndian));
795
+ break;
796
+ case '[D': // double array
797
+ for (i = 0; i < len; i+=8)
798
+ rv.push(view.getFloat64(i, littleEndian));
799
+ break;
800
+ default:
801
+ return;
658
802
  }
803
+ return rv;
804
+ }
659
805
 
660
- // convert a dictionary (usually from decoding JSON) into a message
661
- /**
662
- * @private
663
- *
664
- * @param {(string|Object)} json - JSON string or object to be converted to a message
665
- * @returns {Message} - message created from the JSON string or object
666
- * */
667
- static _deserialize(json) {
668
- let obj = null;
669
- if (typeof json == 'string') {
670
- try {
671
- obj = JSON.parse(json);
672
- }catch(e){
673
- return null;
674
- }
675
- } else obj = json;
676
- let qclazz = obj.clazz;
677
- let clazz = qclazz.replace(/^.*\./, '');
678
- let rv = MessageClass[clazz] ? new MessageClass[clazz] : new Message();
679
- rv.__clazz__ = qclazz;
680
- rv._inflate(obj.data);
681
- return rv;
806
+ // node.js safe atob function
807
+ /**
808
+ * @private
809
+ * @param {string} a
810
+ */
811
+ function _atob(a){
812
+ if (isBrowser || isWebWorker) return window.atob(a);
813
+ else if (isJsDom || isNode) return Buffer.from(a, 'base64').toString('binary');
814
+ }
815
+
816
+ /* global global */
817
+
818
+
819
+ const DEFAULT_QUEUE_SIZE = 128; // max number of old unreceived messages to store
820
+ const DEFAULT_TIMEOUT$1 = 10000; // default timeout for requests in milliseconds
821
+
822
+ const GATEWAY_DEFAULTS = {
823
+ 'timeout': DEFAULT_TIMEOUT$1,
824
+ 'keepAlive' : true,
825
+ 'queueSize': DEFAULT_QUEUE_SIZE,
826
+ 'returnNullOnFailedResponse': true
827
+ };
828
+
829
+ let DEFAULT_URL;
830
+ let gObj = {};
831
+
832
+ /**
833
+ *
834
+ * @private
835
+ *
836
+ * Initializes the Gateway module. This function should be called before using the Gateway class.
837
+ * It sets up the default values for the Gateway and initializes the global object.
838
+ * It also sets up the default URL for the Gateway based on the environment (browser, Node.js, etc.).
839
+ * @returns {void}
840
+ */
841
+ function init(){
842
+ if (isBrowser || isWebWorker){
843
+ gObj = window;
844
+ Object.assign(GATEWAY_DEFAULTS, {
845
+ 'hostname': gObj.location.hostname,
846
+ 'port': gObj.location.port,
847
+ 'pathname' : '/ws/'
848
+ });
849
+ DEFAULT_URL = new URL('ws://localhost');
850
+ // Enable caching of Gateways in browser
851
+ if (typeof gObj.fjage === 'undefined') gObj.fjage = {};
852
+ if (typeof gObj.fjage.gateways == 'undefined') gObj.fjage.gateways = [];
853
+ } else if (isJsDom || isNode){
854
+ gObj = global;
855
+ Object.assign(GATEWAY_DEFAULTS, {
856
+ 'hostname': 'localhost',
857
+ 'port': '1100',
858
+ 'pathname': ''
859
+ });
860
+ DEFAULT_URL = new URL('tcp://localhost');
682
861
  }
683
862
  }
684
863
 
685
864
  /**
686
- * A gateway for connecting to a fjage master container. The new version of the constructor
687
- * uses an options object instead of individual parameters. The old version with
688
- *
689
- *
690
- * @class
691
- * @param {Object} opts
692
- * @param {string} [opts.hostname="localhost"] - hostname/ip address of the master container to connect to
693
- * @param {number} [opts.port=1100] - port number of the master container to connect to
694
- * @param {string} [opts.pathname=""] - path of the master container to connect to (for WebSockets)
695
- * @param {string} [opts.keepAlive=true] - try to reconnect if the connection is lost
696
- * @param {number} [opts.queueSize=128] - size of the queue of received messages that haven't been consumed yet
697
- * @param {number} [opts.timeout=1000] - timeout for fjage level messages in ms
698
- * @param {boolean} [opts.returnNullOnFailedResponse=true] - return null instead of throwing an error when a parameter is not found
699
- * @param {boolean} [opts.cancelPendingOnDisconnect=false] - cancel pending requests on disconnect
700
- */
865
+ * A gateway for connecting to a fjage master container. This class provides methods to
866
+ * send and receive messages, subscribe to topics, and manage connections to the master container.
867
+ * It can be used to connect to a fjage master container over WebSockets or TCP.
868
+ *
869
+ * @example <caption>Connects to the localhost:1100</caption>
870
+ * const gw = new Gateway({ hostname: 'localhost', port: 1100 });
871
+ *
872
+ * @example <caption>Connects to the origin</caption>
873
+ * const gw = new Gateway();
874
+ *
875
+ * @class
876
+ * @property {AgentID} aid - agent id of the gateway
877
+ * @property {boolean} connected - true if the gateway is connected to the master container
878
+ * @property {boolean} debug - true if debug messages should be logged to the console
879
+ *
880
+ * Constructor arguments:
881
+ * @param {Object} opts
882
+ * @param {string} [opts.hostname="localhost"] - hostname/ip address of the master container to connect to
883
+ * @param {number} [opts.port=1100] - port number of the master container to connect to
884
+ * @param {string} [opts.pathname=""] - path of the master container to connect to (for WebSockets)
885
+ * @param {boolean} [opts.keepAlive=true] - try to reconnect if the connection is lost
886
+ * @param {number} [opts.queueSize=128] - size of the _queue of received messages that haven't been consumed yet
887
+ * @param {number} [opts.timeout=10000] - timeout for fjage level messages in ms
888
+ * @param {boolean} [opts.returnNullOnFailedResponse=true] - return null instead of throwing an error when a parameter is not found
889
+ * @param {boolean} [opts.cancelPendingOnDisconnect=false] - cancel pending requests on disconnects
890
+ */
701
891
  class Gateway {
702
892
 
703
893
  constructor(opts = {}) {
@@ -713,14 +903,14 @@
713
903
  if (existing) return existing;
714
904
  this._timeout = opts.timeout; // timeout for fjage level messages (agentForService etc)
715
905
  this._keepAlive = opts.keepAlive; // reconnect if connection gets closed/errored
716
- this._queueSize = opts.queueSize; // size of queue
906
+ this._queueSize = opts.queueSize; // size of _queue
717
907
  this._returnNullOnFailedResponse = opts.returnNullOnFailedResponse; // null or error
718
908
  this._cancelPendingOnDisconnect = opts.cancelPendingOnDisconnect; // cancel pending requests on disconnect
719
- this.pending = {}; // msgid to callback mapping for pending requests to server
720
- this.subscriptions = {}; // hashset for all topics that are subscribed
721
- this.listeners = {}; // list of callbacks that want to listen to incoming messages
722
- this.eventListeners = {}; // external listeners wanting to listen internal events
723
- this.queue = []; // incoming message queue
909
+ this._pending_actions = {}; // msgid to callback mapping for pending actions
910
+ this._subscriptions = {}; // map for all topics that are subscribed
911
+ this._pending_receives = {}; // uuid to callbacks mapping for pending receives
912
+ this._eventListeners = {}; // external listeners wanting to listen internal events
913
+ this._queue = []; // incoming message _queue
724
914
  this.connected = false; // connection status
725
915
  this.debug = false; // debug info to be logged to console?
726
916
  this.aid = new AgentID('gateway-'+_guid(4)); // gateway agent name
@@ -729,14 +919,14 @@
729
919
  }
730
920
 
731
921
  /**
732
- * Sends an event to all registered listeners of the given type.
733
- * @private
734
- * @param {string} type - type of event
735
- * @param {Object|Message|string} val - value to be sent to the listeners
736
- */
922
+ * Sends an event to all registered listeners of the given type.
923
+ * @private
924
+ * @param {string} type - type of event
925
+ * @param {Object|Message|string} val - value to be sent to the listeners
926
+ */
737
927
  _sendEvent(type, val) {
738
- if (!Array.isArray(this.eventListeners[type])) return;
739
- this.eventListeners[type].forEach(l => {
928
+ if (!Array.isArray(this._eventListeners[type])) return;
929
+ this._eventListeners[type].forEach(l => {
740
930
  if (l && {}.toString.call(l) === '[object Function]'){
741
931
  try {
742
932
  l(val);
@@ -748,16 +938,16 @@
748
938
  }
749
939
 
750
940
  /**
751
- * Sends the message to all registered receivers.
752
- *
753
- * @private
754
- * @param {Message} msg
755
- * @returns {boolean} - true if the message was consumed by any listener
756
- */
941
+ * Sends the message to all registered receivers.
942
+ *
943
+ * @private
944
+ * @param {Message} msg
945
+ * @returns {boolean} - true if the message was consumed by any listener
946
+ */
757
947
  _sendReceivers(msg) {
758
- for (var lid in this.listeners){
948
+ for (var lid in this._pending_receives){
759
949
  try {
760
- if (this.listeners[lid] && this.listeners[lid](msg)) return true;
950
+ if (this._pending_receives[lid] && this._pending_receives[lid](msg)) return true;
761
951
  } catch (error) {
762
952
  console.warn('Error in listener : ' + error);
763
953
  }
@@ -767,59 +957,60 @@
767
957
 
768
958
 
769
959
  /**
770
- * @private
771
- * @param {string} data - stringfied JSON data received from the master container to be processed
772
- * @returns {void}
773
- */
960
+ * @private
961
+ * @param {string} data - stringfied JSON data received from the master container to be processed
962
+ * @returns {void}
963
+ */
774
964
  _onMsgRx(data) {
775
- var obj;
965
+ var jsonMsg;
776
966
  if (this.debug) console.log('< '+data);
777
967
  this._sendEvent('rx', data);
778
968
  try {
779
- obj = JSON.parse(data, _decodeBase64);
969
+ jsonMsg = new JSONMessage(data, this);
780
970
  }catch(e){
781
971
  return;
782
972
  }
783
- this._sendEvent('rxp', obj);
784
- if ('id' in obj && obj.id in this.pending) {
973
+ this._sendEvent('rxp', jsonMsg);
974
+ if (jsonMsg.id && jsonMsg.id in this._pending_actions) {
785
975
  // response to a pending request to master
786
- this.pending[obj.id](obj);
787
- delete this.pending[obj.id];
788
- } else if (obj.action == 'send') {
976
+ this._pending_actions[jsonMsg.id](jsonMsg);
977
+ delete this._pending_actions[jsonMsg.id];
978
+ } else if (jsonMsg.action == Actions.SEND) {
789
979
  // incoming message from master
790
- // @ts-ignore
791
- let msg = Message._deserialize(obj.message);
980
+ const msg = jsonMsg.message;
792
981
  if (!msg) return;
793
982
  this._sendEvent('rxmsg', msg);
794
- if ((msg.recipient == this.aid.toJSON() )|| this.subscriptions[msg.recipient]) {
983
+ if ((msg.recipient.toJSON() == this.aid.toJSON())|| this._subscriptions[msg.recipient.toJSON()]) {
795
984
  // send to any "message" listeners
796
985
  this._sendEvent('message', msg);
797
- // send message to receivers, if not consumed, add to queue
986
+ // send message to receivers, if not consumed, add to _queue
798
987
  if(!this._sendReceivers(msg)) {
799
- if (this.queue.length >= this._queueSize) this.queue.shift();
800
- this.queue.push(msg);
988
+ if (this._queue.length >= this._queueSize) this._queue.shift();
989
+ this._queue.push(msg);
801
990
  }
802
991
  }
803
992
  } else {
804
- // respond to standard requests that every container must
805
- let rsp = { id: obj.id, inResponseTo: obj.action };
806
- switch (obj.action) {
807
- case 'agents':
808
- rsp.agentIDs = [this.aid.getName()];
993
+ // respond to standard requests that every gateway must
994
+ let rsp = new JSONMessage();
995
+ rsp.id = jsonMsg.id;
996
+ rsp.inResponseTo = jsonMsg.action;
997
+ switch (jsonMsg.action) {
998
+ case 'agents':
999
+ rsp.agentIDs = [this.aid];
809
1000
  break;
810
- case 'containsAgent':
811
- rsp.answer = (obj.agentID == this.aid.getName());
1001
+ case 'containsAgent':
1002
+ rsp.answer = (jsonMsg.agentID.toJSON() == this.aid.toJSON());
812
1003
  break;
813
- case 'services':
1004
+ case 'services':
814
1005
  rsp.services = [];
815
1006
  break;
816
- case 'agentForService':
1007
+ case 'agentForService':
817
1008
  rsp.agentID = '';
818
1009
  break;
819
- case 'agentsForService':
1010
+ case 'agentsForService':
820
1011
  rsp.agentIDs = [];
821
1012
  break;
822
- default:
1013
+ default:
823
1014
  rsp = undefined;
824
1015
  }
825
1016
  if (rsp) this._msgTx(rsp);
@@ -827,49 +1018,56 @@
827
1018
  }
828
1019
 
829
1020
  /**
830
- * Sends a message out to the master container.
831
- * @private
832
- * @param {string|Object} s - JSON object (either stringified or not) to be sent to the master container
833
- * @returns {boolean} - true if the message was sent successfully
834
- */
835
- _msgTx(s) {
836
- if (typeof s != 'string' && !(s instanceof String)) s = JSON.stringify(s);
1021
+ * Sends a message out to the master container. This method is used for sending
1022
+ * fjage level actions that do not require a response, such as alive, wantMessages, etc.
1023
+ * @private
1024
+ * @param {JSONMessage} msg - JSONMessage to be sent to the master container
1025
+ * @returns {boolean} - true if the message was sent successfully
1026
+ */
1027
+ _msgTx(msg) {
1028
+ const s = msg.toJSON();
837
1029
  if(this.debug) console.log('> '+s);
838
1030
  this._sendEvent('tx', s);
839
1031
  return this.connector.write(s);
840
1032
  }
841
1033
 
842
1034
  /**
843
- * @private
844
- * @param {Object} rq - request to be sent to the master container as a JSON object
845
- * @returns {Promise<Object>} - a promise which returns the response from the master container
846
- */
847
- _msgTxRx(rq) {
848
- rq.id = _guid(8);
1035
+ * Send a message to the master container and wait for a response. This method is used for sending
1036
+ * fjage level actions that require a response, such as agentForService, agents, etc.
1037
+ * @private
1038
+ * @param {JSONMessage} rq - JSONMessage to be sent to the master container
1039
+ * @param {number} [timeout=opts.timeout] - timeout in milliseconds for the response
1040
+ * @returns {Promise<JSONMessage|null>} - a promise which returns the response from the master container
1041
+ */
1042
+ _msgTxRx(rq, timeout = this._timeout) {
1043
+ rq.id = UUID7.generate().toString();
849
1044
  return new Promise(resolve => {
850
- let timer = setTimeout(() => {
851
- delete this.pending[rq.id];
852
- if (this.debug) console.log('Receive Timeout : ' + JSON.stringify(rq));
853
- resolve();
854
- }, 8*this._timeout);
855
- this.pending[rq.id] = rsp => {
856
- clearTimeout(timer);
1045
+ let timer;
1046
+ if (timeout >= 0){
1047
+ timer = setTimeout(() => {
1048
+ delete this._pending_actions[rq.id];
1049
+ if (this.debug) console.log('Receive Timeout : ' + JSON.stringify(rq));
1050
+ resolve(null);
1051
+ }, timeout);
1052
+ }
1053
+ this._pending_actions[rq.id] = rsp => {
1054
+ if (timer) clearTimeout(timer);
857
1055
  resolve(rsp);
858
1056
  };
859
1057
  if (!this._msgTx.call(this,rq)) {
860
- clearTimeout(timer);
861
- delete this.pending[rq.id];
862
- if (this.debug) console.log('Transmit Timeout : ' + JSON.stringify(rq));
863
- resolve();
1058
+ if(timer) clearTimeout(timer);
1059
+ delete this._pending_actions[rq.id];
1060
+ if (this.debug) console.log('Transmit Failure : ' + JSON.stringify(rq));
1061
+ resolve(null);
864
1062
  }
865
1063
  });
866
1064
  }
867
1065
 
868
1066
  /**
869
- * @private
870
- * @param {URL} url - URL object of the master container to connect to
871
- * @returns {TCPConnector|WSConnector} - connector object to connect to the master container
872
- */
1067
+ * @private
1068
+ * @param {URL} url - URL object of the master container to connect to
1069
+ * @returns {TCPConnector|WSConnector} - connector object to connect to the master container
1070
+ */
873
1071
  _createConnector(url){
874
1072
  let conn;
875
1073
  if (url.protocol.startsWith('ws')){
@@ -907,12 +1105,12 @@
907
1105
  }
908
1106
 
909
1107
  /**
910
- * Checks if the object is a constructor.
911
- *
912
- * @private
913
- * @param {Object} value - an object to be checked if it is a constructor
914
- * @returns {boolean} - if the object is a constructor
915
- */
1108
+ * Checks if the object is a constructor.
1109
+ *
1110
+ * @private
1111
+ * @param {Object} value - an object to be checked if it is a constructor
1112
+ * @returns {boolean} - if the object is a constructor
1113
+ */
916
1114
  _isConstructor(value) {
917
1115
  try {
918
1116
  new new Proxy(value, {construct() { return {}; }});
@@ -923,12 +1121,12 @@
923
1121
  }
924
1122
 
925
1123
  /**
926
- * Matches a message with a filter.
927
- * @private
928
- * @param {string|Object|function} filter - filter to be matched
929
- * @param {Message} msg - message to be matched to the filter
930
- * @returns {boolean} - true if the message matches the filter
931
- */
1124
+ * Matches a message with a filter.
1125
+ * @private
1126
+ * @param {string|Object|function} filter - filter to be matched
1127
+ * @param {Message} msg - message to be matched to the filter
1128
+ * @returns {boolean} - true if the message matches the filter
1129
+ */
932
1130
  _matchMessage(filter, msg){
933
1131
  if (typeof filter == 'string' || filter instanceof String) {
934
1132
  return 'inReplyTo' in msg && msg.inReplyTo == filter;
@@ -949,24 +1147,24 @@
949
1147
  }
950
1148
 
951
1149
  /**
952
- * Gets the next message from the queue that matches the filter.
953
- * @private
954
- * @param {string|Object|function} filter - filter to be matched
955
- */
1150
+ * Gets the next message from the _queue that matches the filter.
1151
+ * @private
1152
+ * @param {string|Object|function} filter - filter to be matched
1153
+ */
956
1154
  _getMessageFromQueue(filter) {
957
- if (!this.queue.length) return;
958
- if (!filter) return this.queue.shift();
959
- let matchedMsg = this.queue.find( msg => this._matchMessage(filter, msg));
960
- if (matchedMsg) this.queue.splice(this.queue.indexOf(matchedMsg), 1);
1155
+ if (!this._queue.length) return;
1156
+ if (!filter) return this._queue.shift();
1157
+ let matchedMsg = this._queue.find( msg => this._matchMessage(filter, msg));
1158
+ if (matchedMsg) this._queue.splice(this._queue.indexOf(matchedMsg), 1);
961
1159
  return matchedMsg;
962
1160
  }
963
1161
 
964
1162
  /**
965
- * Gets a cached gateway object for the given URL (if it exists).
966
- * @private
967
- * @param {URL} url - URL object of the master container to connect to
968
- * @returns {Gateway|void} - gateway object for the given URL
969
- */
1163
+ * Gets a cached gateway object for the given URL (if it exists).
1164
+ * @private
1165
+ * @param {URL} url - URL object of the master container to connect to
1166
+ * @returns {Gateway|void} - gateway object for the given URL
1167
+ */
970
1168
  _getGWCache(url){
971
1169
  if (!gObj.fjage || !gObj.fjage.gateways) return null;
972
1170
  var f = gObj.fjage.gateways.filter(g => g.connector.url.toString() == url.toString());
@@ -975,20 +1173,20 @@
975
1173
  }
976
1174
 
977
1175
  /**
978
- * Adds a gateway object to the cache if it doesn't already exist.
979
- * @private
980
- * @param {Gateway} gw - gateway object to be added to the cache
981
- */
1176
+ * Adds a gateway object to the cache if it doesn't already exist.
1177
+ * @private
1178
+ * @param {Gateway} gw - gateway object to be added to the cache
1179
+ */
982
1180
  _addGWCache(gw){
983
1181
  if (!gObj.fjage || !gObj.fjage.gateways) return;
984
1182
  gObj.fjage.gateways.push(gw);
985
1183
  }
986
1184
 
987
1185
  /**
988
- * Removes a gateway object from the cache if it exists.
989
- * @private
990
- * @param {Gateway} gw - gateway object to be removed from the cache
991
- */
1186
+ * Removes a gateway object from the cache if it exists.
1187
+ * @private
1188
+ * @param {Gateway} gw - gateway object to be removed from the cache
1189
+ */
992
1190
  _removeGWCache(gw){
993
1191
  if (!gObj.fjage || !gObj.fjage.gateways) return;
994
1192
  var index = gObj.fjage.gateways.indexOf(gw);
@@ -997,105 +1195,105 @@
997
1195
 
998
1196
  /** @private */
999
1197
  _update_watch() {
1000
- let watch = Object.keys(this.subscriptions);
1001
- watch.push(this.aid.getName());
1002
- let rq = { action: 'wantsMessagesFor', agentIDs: watch };
1003
- this._msgTx(rq);
1198
+ let watch = Object.keys(this._subscriptions);
1199
+ watch.push(this.aid.toJSON());
1200
+ const jsonMsg = JSONMessage.createWantsMessagesFor(watch.map(id => AgentID.fromJSON(id)));
1201
+ this._msgTx(jsonMsg);
1004
1202
  }
1005
1203
 
1006
1204
  /**
1007
- * Add an event listener to listen to various events happening on this Gateway
1008
- *
1009
- * @param {string} type - type of event to be listened to
1010
- * @param {function} listener - new callback/function to be called when the event happens
1011
- * @returns {void}
1012
- */
1205
+ * Add an event listener to listen to various events happening on this Gateway
1206
+ *
1207
+ * @param {string} type - type of event to be listened to
1208
+ * @param {function} listener - new callback/function to be called when the event happens
1209
+ * @returns {void}
1210
+ */
1013
1211
  addEventListener(type, listener) {
1014
- if (!Array.isArray(this.eventListeners[type])){
1015
- this.eventListeners[type] = [];
1212
+ if (!Array.isArray(this._eventListeners[type])){
1213
+ this._eventListeners[type] = [];
1016
1214
  }
1017
- this.eventListeners[type].push(listener);
1215
+ this._eventListeners[type].push(listener);
1018
1216
  }
1019
1217
 
1020
1218
  /**
1021
- * Remove an event listener.
1022
- *
1023
- * @param {string} type - type of event the listener was for
1024
- * @param {function} listener - callback/function which was to be called when the event happens
1025
- * @returns {void}
1026
- */
1219
+ * Remove an event listener.
1220
+ *
1221
+ * @param {string} type - type of event the listener was for
1222
+ * @param {function} listener - callback/function which was to be called when the event happens
1223
+ * @returns {void}
1224
+ */
1027
1225
  removeEventListener(type, listener) {
1028
- if (!this.eventListeners[type]) return;
1029
- let ndx = this.eventListeners[type].indexOf(listener);
1030
- if (ndx >= 0) this.eventListeners[type].splice(ndx, 1);
1226
+ if (!this._eventListeners[type]) return;
1227
+ let ndx = this._eventListeners[type].indexOf(listener);
1228
+ if (ndx >= 0) this._eventListeners[type].splice(ndx, 1);
1031
1229
  }
1032
1230
 
1033
1231
  /**
1034
- * Add a new listener to listen to all {Message}s sent to this Gateway
1035
- *
1036
- * @param {function} listener - new callback/function to be called when a {Message} is received
1037
- * @returns {void}
1038
- */
1232
+ * Add a new listener to listen to all {Message}s sent to this Gateway
1233
+ *
1234
+ * @param {function} listener - new callback/function to be called when a {Message} is received
1235
+ * @returns {void}
1236
+ */
1039
1237
  addMessageListener(listener) {
1040
1238
  this.addEventListener('message',listener);
1041
1239
  }
1042
1240
 
1043
1241
  /**
1044
- * Remove a message listener.
1045
- *
1046
- * @param {function} listener - removes a previously registered listener/callback
1047
- * @returns {void}
1048
- */
1242
+ * Remove a message listener.
1243
+ *
1244
+ * @param {function} listener - removes a previously registered listener/callback
1245
+ * @returns {void}
1246
+ */
1049
1247
  removeMessageListener(listener) {
1050
1248
  this.removeEventListener('message', listener);
1051
1249
  }
1052
1250
 
1053
1251
  /**
1054
- * Add a new listener to get notified when the connection to master is created and terminated.
1055
- *
1056
- * @param {function} listener - new callback/function to be called connection to master is created and terminated
1057
- * @returns {void}
1058
- */
1252
+ * Add a new listener to get notified when the connection to master is created and terminated.
1253
+ *
1254
+ * @param {function} listener - new callback/function to be called connection to master is created and terminated
1255
+ * @returns {void}
1256
+ */
1059
1257
  addConnListener(listener) {
1060
1258
  this.addEventListener('conn', listener);
1061
1259
  }
1062
1260
 
1063
1261
  /**
1064
- * Remove a connection listener.
1065
- *
1066
- * @param {function} listener - removes a previously registered listener/callback
1067
- * @returns {void}
1068
- */
1262
+ * Remove a connection listener.
1263
+ *
1264
+ * @param {function} listener - removes a previously registered listener/callback
1265
+ * @returns {void}
1266
+ */
1069
1267
  removeConnListener(listener) {
1070
1268
  this.removeEventListener('conn', listener);
1071
1269
  }
1072
1270
 
1073
1271
  /**
1074
- * Gets the agent ID associated with the gateway.
1075
- *
1076
- * @returns {AgentID} - agent ID
1077
- */
1272
+ * Gets the agent ID associated with the gateway.
1273
+ *
1274
+ * @returns {AgentID} - agent ID
1275
+ */
1078
1276
  getAgentID() {
1079
1277
  return this.aid;
1080
1278
  }
1081
1279
 
1082
1280
  /**
1083
- * Get an AgentID for a given agent name.
1084
- *
1085
- * @param {string} name - name of agent
1086
- * @returns {AgentID} - AgentID for the given name
1087
- */
1281
+ * Get an AgentID for a given agent name.
1282
+ *
1283
+ * @param {string} name - name of agent
1284
+ * @returns {AgentID} - AgentID for the given name
1285
+ */
1088
1286
  agent(name) {
1089
1287
  return new AgentID(name, false, this);
1090
1288
  }
1091
1289
 
1092
1290
  /**
1093
- * Returns an object representing the named topic.
1094
- *
1095
- * @param {string|AgentID} topic - name of the topic or AgentID
1096
- * @param {string} [topic2] - name of the topic if the topic param is an AgentID
1097
- * @returns {AgentID} - object representing the topic
1098
- */
1291
+ * Returns an object representing the named topic.
1292
+ *
1293
+ * @param {string|AgentID} topic - name of the topic or AgentID
1294
+ * @param {string} [topic2] - name of the topic if the topic param is an AgentID
1295
+ * @returns {AgentID} - object representing the topic
1296
+ */
1099
1297
  topic(topic, topic2) {
1100
1298
  if (typeof topic == 'string' || topic instanceof String) return new AgentID(topic, true, this);
1101
1299
  if (topic instanceof AgentID) {
@@ -1105,143 +1303,139 @@
1105
1303
  }
1106
1304
 
1107
1305
  /**
1108
- * Subscribes the gateway to receive all messages sent to the given topic.
1109
- *
1110
- * @param {AgentID} topic - the topic to subscribe to
1111
- * @returns {boolean} - true if the subscription is successful, false otherwise
1112
- */
1306
+ * Subscribes the gateway to receive all messages sent to the given topic.
1307
+ *
1308
+ * @param {AgentID} topic - the topic to subscribe to
1309
+ * @returns {boolean} - true if the subscription is successful, false otherwise
1310
+ */
1113
1311
  subscribe(topic) {
1114
1312
  if (!topic.isTopic()) topic = new AgentID(topic.getName() + '__ntf', true, this);
1115
- this.subscriptions[topic.toJSON()] = true;
1313
+ this._subscriptions[topic.toJSON()] = true;
1116
1314
  this._update_watch();
1117
1315
  return true;
1118
1316
  }
1119
1317
 
1120
1318
  /**
1121
- * Unsubscribes the gateway from a given topic.
1122
- *
1123
- * @param {AgentID} topic - the topic to unsubscribe
1124
- * @returns {void}
1125
- */
1319
+ * Unsubscribes the gateway from a given topic.
1320
+ *
1321
+ * @param {AgentID} topic - the topic to unsubscribe
1322
+ * @returns {void}
1323
+ */
1126
1324
  unsubscribe(topic) {
1127
1325
  if (!topic.isTopic()) topic = new AgentID(topic.getName() + '__ntf', true, this);
1128
- delete this.subscriptions[topic.toJSON()];
1326
+ delete this._subscriptions[topic.toJSON()];
1129
1327
  this._update_watch();
1130
1328
  }
1131
1329
 
1132
1330
  /**
1133
- * Gets a list of all agents in the container.
1134
- * @returns {Promise<AgentID[]>} - a promise which returns an array of all agent ids when resolved
1135
- */
1136
- async agents() {
1137
- let rq = { action: 'agents' };
1138
- let rsp = await this._msgTxRx(rq);
1331
+ * Gets a list of all agents in the container.
1332
+ * @param {number} [timeout=opts.timeout] - timeout in milliseconds
1333
+ * @returns {Promise<AgentID[]>} - a promise which returns an array of all agent ids when resolved
1334
+ */
1335
+ async agents(timeout=this._timeout) {
1336
+ let jsonMsg = JSONMessage.createAgents();
1337
+ let rsp = await this._msgTxRx(jsonMsg, timeout);
1139
1338
  if (!rsp || !Array.isArray(rsp.agentIDs)) throw new Error('Unable to get agents');
1140
- return rsp.agentIDs.map(aid => new AgentID(aid, false, this));
1339
+ return rsp.agentIDs;
1141
1340
  }
1142
1341
 
1143
1342
  /**
1144
- * Check if an agent with a given name exists in the container.
1145
- *
1146
- * @param {AgentID|String} agentID - the agent id to check
1147
- * @returns {Promise<boolean>} - a promise which returns true if the agent exists when resolved
1148
- */
1149
- async containsAgent(agentID) {
1150
- let rq = { action: 'containsAgent', agentID: agentID instanceof AgentID ? agentID.getName() : agentID };
1151
- let rsp = await this._msgTxRx(rq);
1152
- if (!rsp) throw new Error('Unable to check if agent exists');
1343
+ * Check if an agent with a given name exists in the container.
1344
+ *
1345
+ * @param {AgentID|string} agentID - the agent id to check
1346
+ * @param {number} [timeout=opts.timeout] - timeout in milliseconds
1347
+ * @returns {Promise<boolean>} - a promise which returns true if the agent exists when resolved
1348
+ */
1349
+ async containsAgent(agentID, timeout=this._timeout) {
1350
+ let jsonMsg = JSONMessage.createContainsAgent(agentID instanceof AgentID ? agentID : new AgentID(agentID));
1351
+ let rsp = await this._msgTxRx(jsonMsg, timeout);
1352
+ if (!rsp) {
1353
+ if (this._returnNullOnFailedResponse) return null;
1354
+ else throw new Error('Unable to check if agent exists');
1355
+ }
1153
1356
  return !!rsp.answer;
1154
1357
  }
1155
1358
 
1156
1359
  /**
1157
- * Finds an agent that provides a named service. If multiple agents are registered
1158
- * to provide a given service, any of the agents' id may be returned.
1159
- *
1160
- * @param {string} service - the named service of interest
1161
- * @returns {Promise<?AgentID>} - a promise which returns an agent id for an agent that provides the service when resolved
1162
- */
1163
- async agentForService(service) {
1164
- let rq = { action: 'agentForService', service: service };
1165
- let rsp = await this._msgTxRx(rq);
1360
+ * Finds an agent that provides a named service. If multiple agents are registered
1361
+ * to provide a given service, any of the agents' id may be returned.
1362
+ *
1363
+ * @param {string} service - the named service of interest
1364
+ * @param {number} [timeout=opts.timeout] - timeout in milliseconds
1365
+ * @returns {Promise<?AgentID>} - a promise which returns an agent id for an agent that provides the service when resolved
1366
+ */
1367
+ async agentForService(service, timeout=this._timeout) {
1368
+ let jsonMsg = JSONMessage.createAgentForService(service);
1369
+ let rsp = await this._msgTxRx(jsonMsg, timeout);
1166
1370
  if (!rsp) {
1167
1371
  if (this._returnNullOnFailedResponse) return null;
1168
1372
  else throw new Error('Unable to get agent for service');
1169
1373
  }
1170
- if (!rsp.agentID) return null;
1171
- return new AgentID(rsp.agentID, false, this);
1374
+ return rsp.agentID;
1172
1375
  }
1173
-
1174
- /**
1175
- * Finds all agents that provides a named service.
1176
- *
1177
- * @param {string} service - the named service of interest
1178
- * @returns {Promise<?AgentID[]>} - a promise which returns an array of all agent ids that provides the service when resolved
1179
- */
1180
- async agentsForService(service) {
1181
- let rq = { action: 'agentsForService', service: service };
1182
- let rsp = await this._msgTxRx(rq);
1183
- let aids = [];
1376
+
1377
+ /**
1378
+ * Finds all agents that provides a named service.
1379
+ *
1380
+ * @param {string} service - the named service of interest
1381
+ * @param {number} [timeout=opts.timeout] - timeout in milliseconds
1382
+ * @returns {Promise<AgentID[]>} - a promise which returns an array of all agent ids that provides the service when resolved
1383
+ */
1384
+ async agentsForService(service, timeout=this._timeout) {
1385
+ let jsonMsg = JSONMessage.createAgentsForService(service);
1386
+ let rsp = await this._msgTxRx(jsonMsg, timeout);
1184
1387
  if (!rsp) {
1185
- if (this._returnNullOnFailedResponse) return aids;
1388
+ if (this._returnNullOnFailedResponse) return null;
1186
1389
  else throw new Error('Unable to get agents for service');
1187
1390
  }
1188
- if (!Array.isArray(rsp.agentIDs)) return aids;
1189
- for (var i = 0; i < rsp.agentIDs.length; i++)
1190
- aids.push(new AgentID(rsp.agentIDs[i], false, this));
1191
- return aids;
1391
+ return rsp.agentIDs || [];
1192
1392
  }
1193
1393
 
1194
1394
  /**
1195
- * Sends a message to the recipient indicated in the message. The recipient
1196
- * may be an agent or a topic.
1197
- *
1198
- * @param {Message} msg - message to be sent
1199
- * @returns {boolean} - if sending was successful
1200
- */
1395
+ * Sends a message to the recipient indicated in the message. The recipient
1396
+ * may be an agent or a topic.
1397
+ *
1398
+ * @param {Message} msg - message to be sent
1399
+ * @returns {boolean} - if sending was successful
1400
+ */
1201
1401
  send(msg) {
1202
- msg.sender = this.aid.toJSON();
1203
- if (msg.perf == '') {
1204
- if (msg.__clazz__.endsWith('Req')) msg.perf = Performative.REQUEST;
1205
- else msg.perf = Performative.INFORM;
1206
- }
1402
+ msg.sender = this.aid;
1207
1403
  this._sendEvent('txmsg', msg);
1208
- let rq = JSON.stringify({ action: 'send', relay: true, message: '###MSG###' });
1209
- // @ts-ignore
1210
- rq = rq.replace('"###MSG###"', msg._serialize());
1211
- return !!this._msgTx(rq);
1404
+ const jsonMsg = JSONMessage.createSend(msg, true);
1405
+ return !!this._msgTx(jsonMsg);
1212
1406
  }
1213
1407
 
1214
1408
  /**
1215
- * Flush the Gateway queue for all pending messages. This drops all the pending messages.
1216
- * @returns {void}
1217
- *
1218
- */
1409
+ * Flush the Gateway _queue for all pending messages. This drops all the pending messages.
1410
+ * @returns {void}
1411
+ *
1412
+ */
1219
1413
  flush() {
1220
- this.queue.length = 0;
1414
+ this._queue.length = 0;
1221
1415
  }
1222
1416
 
1223
1417
  /**
1224
- * Sends a request and waits for a response. This method returns a {Promise} which resolves when a response
1225
- * is received or if no response is received after the timeout.
1226
- *
1227
- * @param {Message} msg - message to send
1228
- * @param {number} [timeout=1000] - timeout in milliseconds
1229
- * @returns {Promise<Message|void>} - a promise which resolves with the received response message, null on timeout
1230
- */
1231
- async request(msg, timeout=1000) {
1418
+ * Sends a request and waits for a response. This method returns a {Promise} which resolves when a response
1419
+ * is received or if no response is received after the timeout.
1420
+ *
1421
+ * @param {Message} msg - message to send
1422
+ * @param {number} [timeout=opts.timeout] - timeout in milliseconds
1423
+ * @returns {Promise<Message|void>} - a promise which resolves with the received response message, null on timeout
1424
+ */
1425
+ async request(msg, timeout=this._timeout) {
1232
1426
  this.send(msg);
1233
1427
  return this.receive(msg, timeout);
1234
1428
  }
1235
1429
 
1236
1430
  /**
1237
- * Returns a response message received by the gateway. This method returns a {Promise} which resolves when
1238
- * a response is received or if no response is received after the timeout.
1239
- *
1240
- * @param {function|Message|typeof Message} filter - original message to which a response is expected, or a MessageClass of the type
1241
- * of message to match, or a closure to use to match against the message
1242
- * @param {number} [timeout=0] - timeout in milliseconds
1243
- * @returns {Promise<Message|void>} - received response message, null on timeout
1244
- */
1431
+ * Returns a response message received by the gateway. This method returns a {Promise} which resolves when
1432
+ * a response is received or if no response is received after the timeout.
1433
+ *
1434
+ * @param {function|Message|typeof Message} filter - original message to which a response is expected, or a MessageClass of the type
1435
+ * of message to match, or a closure to use to match against the message
1436
+ * @param {number} [timeout=0] - timeout in milliseconds
1437
+ * @returns {Promise<Message|void>} - received response message, null on timeout
1438
+ */
1245
1439
  async receive(filter, timeout=0) {
1246
1440
  return new Promise(resolve => {
1247
1441
  let msg = this._getMessageFromQueue.call(this,filter);
@@ -1254,22 +1448,22 @@
1254
1448
  resolve();
1255
1449
  return;
1256
1450
  }
1257
- let lid = _guid(8);
1451
+ let lid = UUID7.generate().toString();
1258
1452
  let timer;
1259
1453
  if (timeout > 0){
1260
1454
  timer = setTimeout(() => {
1261
- this.listeners[lid] && delete this.listeners[lid];
1455
+ this._pending_receives[lid] && delete this._pending_receives[lid];
1262
1456
  if (this.debug) console.log('Receive Timeout : ' + filter);
1263
1457
  resolve();
1264
1458
  }, timeout);
1265
1459
  }
1266
1460
  // listener for each pending receive
1267
- this.listeners[lid] = msg => {
1461
+ this._pending_receives[lid] = msg => {
1268
1462
  // skip if the message does not match the filter
1269
1463
  if (msg && !this._matchMessage(filter, msg)) return false;
1270
1464
  if(timer) clearTimeout(timer);
1271
1465
  // if the message matches the filter or is null, delete listener clear timer and resolve
1272
- this.listeners[lid] && delete this.listeners[lid];
1466
+ this._pending_receives[lid] && delete this._pending_receives[lid];
1273
1467
  resolve(msg);
1274
1468
  return true;
1275
1469
  };
@@ -1277,10 +1471,10 @@
1277
1471
  }
1278
1472
 
1279
1473
  /**
1280
- * Closes the gateway. The gateway functionality may not longer be accessed after
1281
- * this method is called.
1282
- * @returns {void}
1283
- */
1474
+ * Closes the gateway. The gateway functionality may not longer be accessed after
1475
+ * this method is called.
1476
+ * @returns {void}
1477
+ */
1284
1478
  close() {
1285
1479
  this.connector.close();
1286
1480
  this._removeGWCache(this);
@@ -1288,29 +1482,302 @@
1288
1482
 
1289
1483
  }
1290
1484
 
1485
+ const DEFAULT_TIMEOUT = 10000; // Default timeout for non-owned AgentIDs
1486
+
1487
+
1291
1488
  /**
1292
- * Services supported by fjage agents.
1293
- */
1294
- const Services = {
1295
- SHELL : 'org.arl.fjage.shell.Services.SHELL'
1296
- };
1489
+ * An identifier for an agent or a topic. This can be to send, receive messages, and set or get parameters
1490
+ * on an agent or topic on the fjåge master container.
1491
+ *
1492
+ * @class
1493
+ * @param {string} name - name of the agent
1494
+ * @param {boolean} [topic=false] - name of topic
1495
+ * @param {Gateway} [owner] - Gateway owner for this AgentID
1496
+ */
1497
+ class AgentID {
1498
+
1499
+ constructor(name, topic=false, owner) {
1500
+ this.name = name;
1501
+ this.topic = topic;
1502
+ this.owner = owner;
1503
+ this._timeout = owner ? owner._timeout : DEFAULT_TIMEOUT; // Default timeout if owner is not provided
1504
+ }
1505
+
1506
+ /**
1507
+ * Gets the name of the agent or topic.
1508
+ *
1509
+ * @returns {string} - name of agent or topic
1510
+ */
1511
+ getName() {
1512
+ return this.name;
1513
+ }
1514
+
1515
+ /**
1516
+ * Returns true if the agent id represents a topic.
1517
+ *
1518
+ * @returns {boolean} - true if the agent id represents a topic, false if it represents an agent
1519
+ */
1520
+ isTopic() {
1521
+ return this.topic;
1522
+ }
1523
+
1524
+ /**
1525
+ * Sends a message to the agent represented by this id.
1526
+ *
1527
+ * @param {Message} msg - message to send
1528
+ * @returns {void}
1529
+ */
1530
+ send(msg) {
1531
+ msg.recipient = this;
1532
+ if (this.owner) this.owner.send(msg);
1533
+ else throw new Error('Unowned AgentID cannot send messages');
1534
+ }
1535
+
1536
+ /**
1537
+ * Sends a request to the agent represented by this id and waits for a reponse.
1538
+ *
1539
+ * @param {Message} msg - request to send
1540
+ * @param {number} [timeout=owner.timeout] - timeout in milliseconds
1541
+ * @returns {Promise<Message>} - response
1542
+ */
1543
+ async request(msg, timeout=this._timeout) {
1544
+ msg.recipient = this;
1545
+ if (this.owner) return this.owner.request(msg, timeout);
1546
+ else throw new Error('Unowned AgentID cannot send messages');
1547
+ }
1548
+
1549
+ /**
1550
+ * Gets a string representation of the agent id.
1551
+ *
1552
+ * @returns {string} - string representation of the agent id
1553
+ */
1554
+ toString() {
1555
+ return this.toJSON() + ((this.owner && this.owner.connector) ? ` on ${this.owner.connector.url}` : '');
1556
+ }
1557
+
1558
+ /**
1559
+ * Gets a JSON string representation of the agent id.
1560
+ *
1561
+ * @returns {string} - JSON string representation of the agent id
1562
+ */
1563
+ toJSON() {
1564
+ return (this.topic ? '#' : '') + this.name;
1565
+ }
1566
+
1567
+ /**
1568
+ * Inflate the AgentID from a JSON string or object.
1569
+ *
1570
+ * @param {string} json - JSON string or object to be converted to an AgentID
1571
+ * @param {Gateway} [owner] - Gateway owner for this AgentID
1572
+ * @returns {AgentID} - AgentID created from the JSON string or object
1573
+ */
1574
+ static fromJSON(json, owner) {
1575
+ if (typeof json !== 'string') {
1576
+ throw new Error('Invalid JSON for AgentID');
1577
+ }
1578
+ json = json.trim();
1579
+ if (json.startsWith('#')) {
1580
+ return new AgentID(json.substring(1), true, owner);
1581
+ } else {
1582
+ return new AgentID(json, false, owner);
1583
+ }
1584
+ }
1585
+
1586
+ /**
1587
+ * Sets parameter(s) on the Agent referred to by this AgentID.
1588
+ *
1589
+ * @param {(string|string[])} params - parameters name(s) to be set
1590
+ * @param {(Object|Object[])} values - parameters value(s) to be set
1591
+ * @param {number} [index=-1] - index of parameter(s) to be set
1592
+ * @param {number} [timeout=owner.timeout] - timeout for the response
1593
+ * @returns {Promise<(Object|Object[])>} - a promise which returns the new value(s) of the parameters
1594
+ */
1595
+ async set (params, values, index=-1, timeout=this._timeout) {
1596
+ if (!params) return null;
1597
+ let msg = new ParameterReq();
1598
+ msg.recipient = this;
1599
+ if (Array.isArray(params)){
1600
+ if (params.length != values.length) throw new Error(`Parameters and values arrays must have the same length: ${params.length} != ${values.length}`);
1601
+ const clonedParams = params.slice(); // Clone the array to avoid side effects
1602
+ const clonedValues = values.slice(); // Clone the values array
1603
+ msg.param = clonedParams.shift();
1604
+ msg.value = clonedValues.shift();
1605
+ msg.requests = clonedParams.map((p, i) => {
1606
+ return {
1607
+ 'param': p,
1608
+ 'value': clonedValues[i]
1609
+ };
1610
+ });
1611
+ } else {
1612
+ msg.param = params;
1613
+ msg.value = values;
1614
+ }
1615
+ msg.index = Number.isInteger(index) ? index : -1;
1616
+ const rsp = await this.owner.request(msg, timeout);
1617
+ var ret = Array.isArray(params) ? new Array(params.length).fill(null) : null;
1618
+ if (!rsp || rsp.perf != Performative.INFORM || !rsp.param) {
1619
+ if (this.owner._returnNullOnFailedResponse) return ret;
1620
+ else throw new Error(`Unable to set ${this.name}.${params} to ${values}`);
1621
+ }
1622
+ if (Array.isArray(params)) {
1623
+ if (!rsp.values) rsp.values = {};
1624
+ if (rsp.param) rsp.values[rsp.param] = rsp.value;
1625
+ const rkeys = Object.keys(rsp.values);
1626
+ return params.map( p => {
1627
+ if (p.includes('.')) p = p.split('.').pop();
1628
+ let f = rkeys.find(k => (k.includes('.') ? k.split('.').pop() : k) == p);
1629
+ return f ? rsp.values[f] : undefined;
1630
+ });
1631
+ } else {
1632
+ return rsp.value;
1633
+ }
1634
+ }
1635
+
1636
+
1637
+ /**
1638
+ * Gets parameter(s) on the Agent referred to by this AgentID.
1639
+ *
1640
+ * @param {(string|string[])} params - parameters name(s) to be get, null implies get value of all parameters on the Agent
1641
+ * @param {number} [index=-1] - index of parameter(s) to be get
1642
+ * @param {number} [timeout=owner.timeout] - timeout for the response
1643
+ * @returns {Promise<(Object|Object[])>} - a promise which returns the value(s) of the parameters
1644
+ */
1645
+ async get(params, index=-1, timeout=this._timeout) {
1646
+ let msg = new ParameterReq();
1647
+ msg.recipient = this;
1648
+ if (params){
1649
+ if (Array.isArray(params)) {
1650
+ const clonedParams = params.slice(); // Clone the array to avoid side effects
1651
+ msg.param = clonedParams.shift();
1652
+ msg.requests = clonedParams.map(p => {return {'param': p};});
1653
+ }
1654
+ else msg.param = params;
1655
+ }
1656
+ msg.index = Number.isInteger(index) ? index : -1;
1657
+ const rsp = await this.owner.request(msg, timeout);
1658
+ var ret = Array.isArray(params) ? new Array(params.length).fill(null) : null;
1659
+ if (!rsp || rsp.perf != Performative.INFORM || !rsp.param) {
1660
+ if (this.owner._returnNullOnFailedResponse) return ret;
1661
+ else throw new Error(`Unable to get ${this.name}.${params}`);
1662
+ }
1663
+ // Request for listing of all parameters.
1664
+ if (!params) {
1665
+ if (!rsp.values) rsp.values = {};
1666
+ if (rsp.param) rsp.values[rsp.param] = rsp.value;
1667
+ return rsp.values;
1668
+ } else if (Array.isArray(params)) {
1669
+ if (!rsp.values) rsp.values = {};
1670
+ if (rsp.param) rsp.values[rsp.param] = rsp.value;
1671
+ const rkeys = Object.keys(rsp.values);
1672
+ return params.map( p => {
1673
+ if (p.includes('.')) p = p.split('.').pop();
1674
+ let f = rkeys.find(k => (k.includes('.') ? k.split('.').pop() : k) == p);
1675
+ return f ? rsp.values[f] : undefined;
1676
+ });
1677
+ } else {
1678
+ return rsp.value;
1679
+ }
1680
+ }
1681
+ }
1297
1682
 
1298
1683
  /**
1299
- * Creates a unqualified message class based on a fully qualified name.
1300
- * @param {string} name - fully qualified name of the message class to be created
1301
- * @param {typeof Message} [parent] - class of the parent MessageClass to inherit from
1302
- * @constructs Message
1303
- * @example
1304
- * const ParameterReq = MessageClass('org.arl.fjage.param.ParameterReq');
1305
- * let pReq = new ParameterReq()
1306
- */
1684
+ * Base class for messages transmitted by one agent to another. Creates an empty message.
1685
+ * @class
1686
+ *
1687
+ * @property {string} msgID - unique message ID
1688
+ * @property {Performative} perf - performative of the message
1689
+ * @property {AgentID} [sender] - AgentID of the sender of the message
1690
+ * @property {AgentID} [recipient] - AgentID of the recipient of the message
1691
+ * @property {string} [inReplyTo] - ID of the message to which this message is a response
1692
+ * @property {number} [sentAt] - timestamp when the message was sent
1693
+ */
1694
+ class Message {
1695
+
1696
+ /**
1697
+ * @param {Message} [inReplyToMsg] - message to which this message is a response
1698
+ * @param {Performative} [perf=Performative.INFORM] - performative of the message
1699
+ */
1700
+ constructor(inReplyToMsg, perf=Performative.INFORM) {
1701
+ this.__clazz__ = 'org.arl.fjage.Message';
1702
+ this.msgID = UUID7.generate().toString();
1703
+ this.perf = perf;
1704
+ this.sender = null;
1705
+ this.recipient = inReplyToMsg ? inReplyToMsg.sender : null;
1706
+ this.inReplyTo = inReplyToMsg ? inReplyToMsg.msgID : null;
1707
+ }
1708
+
1709
+ /**
1710
+ * Gets a string representation of the message.
1711
+ *
1712
+ * @returns {string} - string representation
1713
+ */
1714
+ toString() {
1715
+ let p = this.perf ? this.perf.toString() : 'MESSAGE';
1716
+ if (this.__clazz__ == 'org.arl.fjage.Message') return p;
1717
+ return p + ': ' + this.__clazz__.replace(/^.*\./, '');
1718
+ }
1719
+
1720
+ /** Convert a message into a object for JSON serialization.
1721
+ *
1722
+ * NOTE: we don't do any base64 encoding for TX as
1723
+ * we don't know what data type is intended
1724
+ *
1725
+ * @return {Object} - JSON string representation of the message
1726
+ */
1727
+ toJSON() {
1728
+ let props = {};
1729
+ for (let key in this) {
1730
+ if (key.startsWith('_')) continue; // skip private properties
1731
+ // @ts-ignore
1732
+ props[key] = this[key];
1733
+ }
1734
+ return { 'clazz': this.__clazz__, 'data': props };
1735
+ }
1736
+
1737
+
1738
+ /**
1739
+ * Create a message from a object parsed from the JSON representation of the message.
1740
+ *
1741
+ * @param {Object} jsonObj - Object containing all the properties of the message
1742
+ * @returns {Message} - A message created from the Object
1743
+ *
1744
+ */
1745
+ static fromJSON(jsonObj) {
1746
+ if (!( 'clazz' in jsonObj) || !( 'data' in jsonObj)) {
1747
+ throw new Error(`Invalid Object for Message : ${jsonObj}`);
1748
+ }
1749
+ let qclazz = jsonObj.clazz;
1750
+ let clazz = qclazz.replace(/^.*\./, '');
1751
+ let rv = MessageClass[clazz] ? new MessageClass[clazz] : new Message();
1752
+ rv.__clazz__ = qclazz;
1753
+ // copy all properties from the data object
1754
+ for (var key in jsonObj.data){
1755
+ if (key === 'sender' || key === 'recipient') {
1756
+ if (jsonObj.data[key] && typeof jsonObj.data[key] === 'string') {
1757
+ rv[key] = AgentID.fromJSON(jsonObj.data[key]);
1758
+ }
1759
+ } else rv[key] = jsonObj.data[key];
1760
+ }
1761
+ return rv;
1762
+ }
1763
+ }
1764
+
1765
+ /**
1766
+ * Creates a unqualified message class based on a fully qualified name.
1767
+ * @param {string} name - fully qualified name of the message class to be created
1768
+ * @param {typeof Message} [parent] - class of the parent MessageClass to inherit from
1769
+ * @constructs Message
1770
+ * @example
1771
+ * const ParameterReq = MessageClass('org.arl.fjage.param.ParameterReq');
1772
+ * let pReq = new ParameterReq()
1773
+ */
1307
1774
  function MessageClass(name, parent=Message) {
1308
1775
  let sname = name.replace(/^.*\./, '');
1309
1776
  if (MessageClass[sname]) return MessageClass[sname];
1310
1777
  let cls = class extends parent {
1311
1778
  /**
1312
- * @param {{ [x: string]: any; }} params
1313
- */
1779
+ * @param {{ [x: string]: any; }} params
1780
+ */
1314
1781
  constructor(params) {
1315
1782
  super();
1316
1783
  this.__clazz__ = name;
@@ -1328,134 +1795,67 @@
1328
1795
  return cls;
1329
1796
  }
1330
1797
 
1331
- ////// private utilities
1332
-
1333
- // generate random ID with length 4*len characters
1334
- /** @private */
1335
- function _guid(len) {
1336
- function s4() {
1337
- return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
1338
- }
1339
- let s = s4();
1340
- for (var i = 0; i < len-1; i++)
1341
- s += s4();
1342
- return s;
1343
- }
1344
-
1345
- // convert from base 64 to array
1346
- /** @private */
1347
- function _b64toArray(base64, dtype, littleEndian=true) {
1348
- let s = gObj.atob(base64);
1349
- let len = s.length;
1350
- let bytes = new Uint8Array(len);
1351
- for (var i = 0; i < len; i++)
1352
- bytes[i] = s.charCodeAt(i);
1353
- let rv = [];
1354
- let view = new DataView(bytes.buffer);
1355
- switch (dtype) {
1356
- case '[B': // byte array
1357
- for (i = 0; i < len; i++)
1358
- rv.push(view.getUint8(i));
1359
- break;
1360
- case '[S': // short array
1361
- for (i = 0; i < len; i+=2)
1362
- rv.push(view.getInt16(i, littleEndian));
1363
- break;
1364
- case '[I': // integer array
1365
- for (i = 0; i < len; i+=4)
1366
- rv.push(view.getInt32(i, littleEndian));
1367
- break;
1368
- case '[J': // long array
1369
- for (i = 0; i < len; i+=8)
1370
- rv.push(view.getBigInt64(i, littleEndian));
1371
- break;
1372
- case '[F': // float array
1373
- for (i = 0; i < len; i+=4)
1374
- rv.push(view.getFloat32(i, littleEndian));
1375
- break;
1376
- case '[D': // double array
1377
- for (i = 0; i < len; i+=8)
1378
- rv.push(view.getFloat64(i, littleEndian));
1379
- break;
1380
- default:
1381
- return;
1382
- }
1383
- return rv;
1384
- }
1385
-
1386
- // base 64 JSON decoder
1387
- /** @private */
1388
- function _decodeBase64(k, d) {
1389
- if (d === null) {
1390
- return null;
1391
- }
1392
- if (typeof d == 'object' && 'clazz' in d) {
1393
- let clazz = d.clazz;
1394
- if (clazz.startsWith('[') && clazz.length == 2 && 'data' in d) {
1395
- let x = _b64toArray(d.data, d.clazz);
1396
- if (x) d = x;
1397
- }
1398
- }
1399
- return d;
1400
- }
1401
-
1402
- ////// global
1403
-
1404
- const GATEWAY_DEFAULTS = {};
1405
-
1406
- /** @type {Window & globalThis & Object} */
1407
- let gObj = {};
1408
- let DEFAULT_URL;
1409
- if (isBrowser || isWebWorker){
1410
- gObj = window;
1411
- Object.assign(GATEWAY_DEFAULTS, {
1412
- 'hostname': gObj.location.hostname,
1413
- 'port': gObj.location.port,
1414
- 'pathname' : '/ws/',
1415
- 'timeout': 1000,
1416
- 'keepAlive' : true,
1417
- 'queueSize': DEFAULT_QUEUE_SIZE,
1418
- 'returnNullOnFailedResponse': true,
1419
- 'cancelPendingOnDisconnect': false
1420
- });
1421
- DEFAULT_URL = new URL('ws://localhost');
1422
- // Enable caching of Gateways
1423
- if (typeof gObj.fjage === 'undefined') gObj.fjage = {};
1424
- if (typeof gObj.fjage.gateways == 'undefined') gObj.fjage.gateways = [];
1425
- } else if (isJsDom || isNode){
1426
- gObj = global;
1427
- Object.assign(GATEWAY_DEFAULTS, {
1428
- 'hostname': 'localhost',
1429
- 'port': '1100',
1430
- 'pathname': '',
1431
- 'timeout': 1000,
1432
- 'keepAlive' : true,
1433
- 'queueSize': DEFAULT_QUEUE_SIZE,
1434
- 'returnNullOnFailedResponse': true,
1435
- 'cancelPendingOnDisconnect': false
1436
- });
1437
- DEFAULT_URL = new URL('tcp://localhost');
1438
- gObj.atob = a => Buffer.from(a, 'base64').toString('binary');
1439
- }
1798
+ /**
1799
+ * @typedef {Object} ParameterReq.Entry
1800
+ * @property {string} param - parameter name
1801
+ * @property {Object} value - parameter value
1802
+ * @exports ParameterReq.Entry
1803
+ */
1440
1804
 
1441
1805
  /**
1442
- * @typedef {Object} ParameterReq.Entry
1443
- * @property {string} param - parameter name
1444
- * @property {Object} value - parameter value
1445
- * @exports ParameterReq.Entry
1446
- */
1806
+ * A message that requests one or more parameters of an agent.
1807
+ *
1808
+ * @example <caption>Setting a parameter myAgent.x to 42</caption>
1809
+ * let req = new ParameterReq({
1810
+ * recipient: myAgentId,
1811
+ * param: 'x',
1812
+ * value: 42
1813
+ * });
1814
+ *
1815
+ * @example <caption>Getting the value of myAgent.x</caption>
1816
+ * let req = new ParameterReq({
1817
+ * recipient: myAgentId,
1818
+ * param: 'x'
1819
+ * });
1820
+ *
1821
+ * @typedef {Message} ParameterReq
1822
+ * @property {string} param - parameters name to be get/set if only a single parameter is to be get/set
1823
+ * @property {Object} value - parameters value to be set if only a single parameter is to be set
1824
+ * @property {Array<ParameterReq.Entry>} requests - a list of multiple parameters to be get/set
1825
+ * @property {number} [index=-1] - index of parameter(s) to be set*
1826
+ * @exports ParameterReq
1827
+ */
1828
+ const ParameterReq = MessageClass('org.arl.fjage.param.ParameterReq');
1447
1829
 
1830
+ /**
1831
+ * A message that is a response to a {@link ParameterReq} message.
1832
+ *
1833
+ * @example <caption>Receiving a parameter from myAgent</caption>
1834
+ * let rsp = gw.receive(ParameterRsp)
1835
+ * rsp.sender // = myAgentId; sender of the message
1836
+ * rsp.param // = 'x'; parameter name that was get/set
1837
+ * rsp.value // = 42; value of the parameter that was set
1838
+ * rsp.readonly // = [false]; indicates if the parameter is read-only
1839
+ *
1840
+ *
1841
+ * @typedef {Message} ParameterRsp
1842
+ * @property {string} param - parameters name if only a single parameter value was requested
1843
+ * @property {Object} value - parameters value if only a single parameter was requested
1844
+ * @property {Map<string, Object>} values - a map of multiple parameter names and their values if multiple parameters were requested
1845
+ * @property {Array<boolean>} readonly - a list of booleans indicating if the parameters are read-only
1846
+ * @property {number} [index=-1] - index of parameter(s) being returned
1847
+ * @exports ParameterReq
1848
+ */
1849
+ MessageClass('org.arl.fjage.param.ParameterRsp');
1448
1850
 
1449
1851
  /**
1450
- * A message that requests one or more parameters of an agent.
1451
- * @typedef {Message} ParameterReq
1452
- * @property {string} param - parameters name to be get/set if only a single parameter is to be get/set
1453
- * @property {Object} value - parameters value to be set if only a single parameter is to be set
1454
- * @property {Array<ParameterReq.Entry>} requests - a list of multiple parameters to be get/set
1455
- * @property {number} [index=-1] - index of parameter(s) to be set
1456
- * @exports ParameterReq
1457
- */
1458
- const ParameterReq = MessageClass('org.arl.fjage.param.ParameterReq');
1852
+ * Services supported by fjage agents.
1853
+ */
1854
+ const Services = {
1855
+ SHELL : 'org.arl.fjage.shell.Services.SHELL'
1856
+ };
1857
+
1858
+ init();
1459
1859
 
1460
1860
  const DatagramReq$1 = MessageClass('org.arl.unet.DatagramReq');
1461
1861
  const DatagramNtf$1 = MessageClass('org.arl.unet.DatagramNtf');
@@ -2110,34 +2510,31 @@
2110
2510
  /**
2111
2511
  * Gets an AgentID providing a specified service for low-level access to UnetStack
2112
2512
  * @param {string} svc - the named service of interest
2113
- * @param {Boolean} caching - if the AgentID should cache parameters
2114
2513
  * @returns {Promise<?AgentID>} - a promise which returns an {@link AgentID} that provides the service when resolved
2115
2514
  */
2116
- async agentForService(svc, caching=true) {
2515
+ async agentForService(svc) {
2117
2516
  if (this.gw == null) return null;
2118
- return await this.gw.agentForService(svc, caching);
2517
+ return await this.gw.agentForService(svc);
2119
2518
  }
2120
2519
 
2121
2520
  /**
2122
2521
  *
2123
2522
  * @param {string} svc - the named service of interest
2124
- * @param {Boolean} caching - if the AgentID should cache parameters
2125
2523
  * @returns {Promise<AgentID[]>} - a promise which returns an array of {@link AgentID|AgentIDs} that provides the service when resolved
2126
2524
  */
2127
- async agentsForService(svc, caching=true) {
2525
+ async agentsForService(svc) {
2128
2526
  if (this.gw == null) return null;
2129
- return await this.gw.agentsForService(svc, caching``);
2527
+ return await this.gw.agentsForService(svc);
2130
2528
  }
2131
2529
 
2132
2530
  /**
2133
2531
  * Gets a named AgentID for low-level access to UnetStack.
2134
2532
  * @param {string} name - name of agent
2135
- * @param {Boolean} caching - if the AgentID should cache parameters
2136
2533
  * @returns {AgentID} - AgentID for the given name
2137
2534
  */
2138
- agent(name, caching=true) {
2535
+ agent(name) {
2139
2536
  if (this.gw == null) return null;
2140
- return this.gw.agent(name, caching);
2537
+ return this.gw.agent(name);
2141
2538
  }
2142
2539
 
2143
2540
  /**