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