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/esm/unet.js
CHANGED
|
@@ -1,31 +1,160 @@
|
|
|
1
|
-
/* unet.js
|
|
1
|
+
/* unet.js v5.0.1 2025-09-10T10:45:09.128Z */
|
|
2
2
|
|
|
3
|
-
/* fjage.js v2.
|
|
3
|
+
/* fjage.js v2.2.2 */
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
*
|
|
20
|
-
*
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
166
|
+
* @class
|
|
167
|
+
* @ignore
|
|
168
|
+
*/
|
|
40
169
|
class TCPConnector {
|
|
41
170
|
|
|
42
171
|
/**
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
292
|
+
* @callback TCPConnectorReadCallback
|
|
293
|
+
* @ignore
|
|
294
|
+
* @param {string} s - incoming message string
|
|
295
|
+
*/
|
|
167
296
|
|
|
168
297
|
/**
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
178
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
353
|
+
* @class
|
|
354
|
+
* @ignore
|
|
355
|
+
*/
|
|
227
356
|
class WSConnector {
|
|
228
357
|
|
|
229
358
|
/**
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
304
|
-
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
449
|
+
* @callback WSConnectorReadCallback
|
|
450
|
+
* @ignore
|
|
451
|
+
* @param {string} s - incoming message string
|
|
452
|
+
*/
|
|
324
453
|
|
|
325
454
|
/**
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
336
|
-
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
|
|
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
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
const DEFAULT_QUEUE_SIZE = 128; // max number of old unreceived messages to store
|
|
504
|
+
/* global Buffer */
|
|
379
505
|
|
|
380
506
|
/**
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
-
|
|
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
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
if (
|
|
551
|
-
|
|
552
|
-
|
|
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
|
-
|
|
555
|
-
if (
|
|
556
|
-
|
|
557
|
-
if (
|
|
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
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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
|
-
|
|
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
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
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
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
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
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
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
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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
|
|
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.
|
|
716
|
-
this.
|
|
717
|
-
this.
|
|
718
|
-
this.
|
|
719
|
-
this.
|
|
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
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
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.
|
|
735
|
-
this.
|
|
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
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
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.
|
|
944
|
+
for (var lid in this._pending_receives){
|
|
755
945
|
try {
|
|
756
|
-
if (this.
|
|
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
|
-
|
|
767
|
-
|
|
768
|
-
|
|
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
|
|
961
|
+
var jsonMsg;
|
|
772
962
|
if (this.debug) console.log('< '+data);
|
|
773
963
|
this._sendEvent('rx', data);
|
|
774
964
|
try {
|
|
775
|
-
|
|
965
|
+
jsonMsg = new JSONMessage(data, this);
|
|
776
966
|
}catch(e){
|
|
777
967
|
return;
|
|
778
968
|
}
|
|
779
|
-
this._sendEvent('rxp',
|
|
780
|
-
if (
|
|
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.
|
|
783
|
-
delete this.
|
|
784
|
-
} else if (
|
|
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
|
-
|
|
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()
|
|
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
|
|
982
|
+
// send message to receivers, if not consumed, add to _queue
|
|
794
983
|
if(!this._sendReceivers(msg)) {
|
|
795
|
-
if (this.
|
|
796
|
-
this.
|
|
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
|
|
801
|
-
let rsp =
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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
|
-
|
|
807
|
-
rsp.answer = (
|
|
997
|
+
case 'containsAgent':
|
|
998
|
+
rsp.answer = (jsonMsg.agentID.toJSON() == this.aid.toJSON());
|
|
808
999
|
break;
|
|
809
|
-
|
|
1000
|
+
case 'services':
|
|
810
1001
|
rsp.services = [];
|
|
811
1002
|
break;
|
|
812
|
-
|
|
1003
|
+
case 'agentForService':
|
|
813
1004
|
rsp.agentID = '';
|
|
814
1005
|
break;
|
|
815
|
-
|
|
1006
|
+
case 'agentsForService':
|
|
816
1007
|
rsp.agentIDs = [];
|
|
817
1008
|
break;
|
|
818
|
-
|
|
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
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
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
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
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
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
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.
|
|
858
|
-
if (this.debug) console.log('Transmit
|
|
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
|
-
|
|
866
|
-
|
|
867
|
-
|
|
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
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
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
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
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
|
-
|
|
949
|
-
|
|
950
|
-
|
|
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.
|
|
954
|
-
if (!filter) return this.
|
|
955
|
-
let matchedMsg = this.
|
|
956
|
-
if (matchedMsg) this.
|
|
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
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
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
|
-
|
|
975
|
-
|
|
976
|
-
|
|
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
|
-
|
|
985
|
-
|
|
986
|
-
|
|
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.
|
|
997
|
-
watch.push(this.aid.
|
|
998
|
-
|
|
999
|
-
this._msgTx(
|
|
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
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
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.
|
|
1011
|
-
this.
|
|
1208
|
+
if (!Array.isArray(this._eventListeners[type])){
|
|
1209
|
+
this._eventListeners[type] = [];
|
|
1012
1210
|
}
|
|
1013
|
-
this.
|
|
1211
|
+
this._eventListeners[type].push(listener);
|
|
1014
1212
|
}
|
|
1015
1213
|
|
|
1016
1214
|
/**
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
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.
|
|
1025
|
-
let ndx = this.
|
|
1026
|
-
if (ndx >= 0) this.
|
|
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
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
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
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
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
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
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
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
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
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
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
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
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
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
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
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
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.
|
|
1309
|
+
this._subscriptions[topic.toJSON()] = true;
|
|
1112
1310
|
this._update_watch();
|
|
1113
1311
|
return true;
|
|
1114
1312
|
}
|
|
1115
1313
|
|
|
1116
1314
|
/**
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
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.
|
|
1322
|
+
delete this._subscriptions[topic.toJSON()];
|
|
1125
1323
|
this._update_watch();
|
|
1126
1324
|
}
|
|
1127
1325
|
|
|
1128
1326
|
/**
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
let
|
|
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
|
|
1335
|
+
return rsp.agentIDs;
|
|
1137
1336
|
}
|
|
1138
1337
|
|
|
1139
1338
|
/**
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
let
|
|
1148
|
-
|
|
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
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
let
|
|
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
|
-
|
|
1167
|
-
return new AgentID(rsp.agentID, false, this);
|
|
1370
|
+
return rsp.agentID;
|
|
1168
1371
|
}
|
|
1169
|
-
|
|
1170
|
-
/**
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
let
|
|
1179
|
-
let
|
|
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
|
|
1384
|
+
if (this._returnNullOnFailedResponse) return null;
|
|
1182
1385
|
else throw new Error('Unable to get agents for service');
|
|
1183
1386
|
}
|
|
1184
|
-
|
|
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
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
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
|
|
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
|
-
|
|
1205
|
-
|
|
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
|
-
|
|
1212
|
-
|
|
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.
|
|
1410
|
+
this._queue.length = 0;
|
|
1217
1411
|
}
|
|
1218
1412
|
|
|
1219
1413
|
/**
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
async request(msg, timeout=
|
|
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
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
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 =
|
|
1447
|
+
let lid = UUID7.generate().toString();
|
|
1254
1448
|
let timer;
|
|
1255
1449
|
if (timeout > 0){
|
|
1256
1450
|
timer = setTimeout(() => {
|
|
1257
|
-
this.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
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
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
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
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
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
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
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
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
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
|
|
2511
|
+
async agentForService(svc) {
|
|
2113
2512
|
if (this.gw == null) return null;
|
|
2114
|
-
return await this.gw.agentForService(svc
|
|
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
|
|
2521
|
+
async agentsForService(svc) {
|
|
2124
2522
|
if (this.gw == null) return null;
|
|
2125
|
-
return await this.gw.agentsForService(svc
|
|
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
|
|
2531
|
+
agent(name) {
|
|
2135
2532
|
if (this.gw == null) return null;
|
|
2136
|
-
return this.gw.agent(name
|
|
2533
|
+
return this.gw.agent(name);
|
|
2137
2534
|
}
|
|
2138
2535
|
|
|
2139
2536
|
/**
|