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