unetjs 4.0.2 → 5.0.0

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