unetjs 4.0.2 → 5.0.1

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