jsgar 4.8.0 → 4.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/gar.umd.js +77 -21
- package/gar.js +77 -21
- package/package.json +4 -1
package/dist/gar.umd.js
CHANGED
|
@@ -200,6 +200,7 @@
|
|
|
200
200
|
this.websocket = null;
|
|
201
201
|
this.messageQueue = [];
|
|
202
202
|
this.connected = false;
|
|
203
|
+
this.connectedCallback = null;
|
|
203
204
|
this.reconnectDelay = 5000; // Milliseconds
|
|
204
205
|
this.user = user;
|
|
205
206
|
this.working_namespace = working_namespace;
|
|
@@ -383,18 +384,18 @@
|
|
|
383
384
|
});
|
|
384
385
|
|
|
385
386
|
this.websocket = await connectionPromise;
|
|
386
|
-
this.
|
|
387
|
+
this._setConnected(true);
|
|
387
388
|
this.log('INFO', `Connected to WebSocket server at ${this.wsEndpoint} using gar-protocol`);
|
|
388
389
|
|
|
389
390
|
this.websocket.onclose = () => {
|
|
390
391
|
this.log('WARNING', 'WebSocket connection closed.');
|
|
391
|
-
this.
|
|
392
|
+
this._setConnected(false);
|
|
392
393
|
this.websocket = null;
|
|
393
394
|
this._reconnect();
|
|
394
395
|
};
|
|
395
396
|
this.websocket.onerror = (error) => {
|
|
396
397
|
this.log('ERROR', `WebSocket error: ${error.message || 'Unknown error'}`);
|
|
397
|
-
this.
|
|
398
|
+
this._setConnected(false);
|
|
398
399
|
this.websocket = null;
|
|
399
400
|
};
|
|
400
401
|
|
|
@@ -402,7 +403,7 @@
|
|
|
402
403
|
this._receiveMessages();
|
|
403
404
|
|
|
404
405
|
} catch (e) {
|
|
405
|
-
this.
|
|
406
|
+
this._setConnected(false);
|
|
406
407
|
this.websocket = null;
|
|
407
408
|
// 401 is a permanent auth rejection — retrying won't help
|
|
408
409
|
if (e.message && e.message.includes('401')) {
|
|
@@ -454,7 +455,7 @@
|
|
|
454
455
|
}
|
|
455
456
|
this.log('INFO', 'Done sending messages.');
|
|
456
457
|
this.stop();
|
|
457
|
-
this.
|
|
458
|
+
this._setConnected(false);
|
|
458
459
|
}
|
|
459
460
|
|
|
460
461
|
/**
|
|
@@ -467,6 +468,34 @@
|
|
|
467
468
|
};
|
|
468
469
|
}
|
|
469
470
|
|
|
471
|
+
/**
|
|
472
|
+
* Register a callback fired when the WS `connected` flag transitions.
|
|
473
|
+
* @param {Function} handler - Callback receiving the new boolean value
|
|
474
|
+
*/
|
|
475
|
+
registerConnectedHandler(handler) {
|
|
476
|
+
this.connectedCallback = handler;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Clear the connected handler.
|
|
481
|
+
*/
|
|
482
|
+
clearConnectedHandler() {
|
|
483
|
+
this.connectedCallback = null;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Internal setter that fires connectedCallback on state transitions.
|
|
488
|
+
* @param {boolean} value
|
|
489
|
+
*/
|
|
490
|
+
_setConnected(value) {
|
|
491
|
+
if (this.connected === value) return;
|
|
492
|
+
this.connected = value;
|
|
493
|
+
if (this.connectedCallback) {
|
|
494
|
+
try { this.connectedCallback(value); }
|
|
495
|
+
catch (e) { this.log('ERROR', `connectedCallback threw: ${e.message}`); }
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
470
499
|
/**
|
|
471
500
|
* Register a callback handler for a specific message type.
|
|
472
501
|
* @param {string} messageType - Type of message
|
|
@@ -802,7 +831,7 @@
|
|
|
802
831
|
* Stop the client without sending pending messages.
|
|
803
832
|
*/
|
|
804
833
|
halt() {
|
|
805
|
-
this.
|
|
834
|
+
this._setConnected(false);
|
|
806
835
|
this.stop();
|
|
807
836
|
}
|
|
808
837
|
|
|
@@ -1072,9 +1101,18 @@
|
|
|
1072
1101
|
this.log('INFO', 'Received Logoff from server');
|
|
1073
1102
|
this.stop();
|
|
1074
1103
|
} else if (msgType === 'Error') {
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1104
|
+
// proto.gar's `error` struct carries `recoverable: bool`. Recoverable errors mean
|
|
1105
|
+
// the server processed the failing message (e.g. a publish that violated some
|
|
1106
|
+
// server-side check), logged it, and kept the connection alive. We dispatch the
|
|
1107
|
+
// message to the user's error handler but don't tear down the client. Non-recoverable
|
|
1108
|
+
// errors are followed by a server disconnect; we set exit_code + stop() so callers
|
|
1109
|
+
// (CLI tools, the html_view UI) see the failure.
|
|
1110
|
+
const recoverable = !!message.value.recoverable;
|
|
1111
|
+
this.log(recoverable ? 'WARNING' : 'ERROR', `GAR ${message.value.message}`);
|
|
1112
|
+
if (!recoverable) {
|
|
1113
|
+
this.exitCode = 1;
|
|
1114
|
+
this.stop();
|
|
1115
|
+
}
|
|
1078
1116
|
}
|
|
1079
1117
|
|
|
1080
1118
|
this.checkHeartbeat();
|
|
@@ -1358,28 +1396,25 @@
|
|
|
1358
1396
|
}
|
|
1359
1397
|
|
|
1360
1398
|
/**
|
|
1361
|
-
* Introduce a new key if not already known and return local key ID.
|
|
1399
|
+
* Introduce a new key if not already known and return local key ID. className is required;
|
|
1400
|
+
* a bare introduction with no class_list can't be routed by upstream proxies.
|
|
1362
1401
|
* @param {string} name - Key name
|
|
1363
|
-
* @param {string|Array<string
|
|
1402
|
+
* @param {string|Array<string>} className - Class name(s)
|
|
1364
1403
|
* @returns {number} Local key ID
|
|
1365
1404
|
*/
|
|
1366
|
-
getAndPossiblyIntroduceKeyId(name, className
|
|
1405
|
+
getAndPossiblyIntroduceKeyId(name, className) {
|
|
1367
1406
|
const existingId = this.localKeyMap.get(name);
|
|
1368
1407
|
if (existingId !== undefined && this.invalidatedKeyIds.has(existingId)) {
|
|
1369
1408
|
this.invalidatedKeyIds.delete(existingId);
|
|
1370
1409
|
this.localKeyMap.delete(name);
|
|
1371
1410
|
}
|
|
1372
1411
|
if (!this.localKeyMap.has(name)) {
|
|
1412
|
+
if (!className) throw new Error(`getAndPossiblyIntroduceKeyId: className required to introduce key "${name}"`);
|
|
1373
1413
|
const keyId = this.localKeyCounter++;
|
|
1374
1414
|
this.localKeyMap.set(name, keyId);
|
|
1375
1415
|
const value = { key_id: keyId, name };
|
|
1376
|
-
if (className)
|
|
1377
|
-
|
|
1378
|
-
value.class_list = className.split(/\s+/).filter(Boolean);
|
|
1379
|
-
} else if (Array.isArray(className)) {
|
|
1380
|
-
value.class_list = className;
|
|
1381
|
-
}
|
|
1382
|
-
}
|
|
1416
|
+
if (typeof className === 'string') value.class_list = className.split(/\s+/).filter(Boolean);
|
|
1417
|
+
else if (Array.isArray(className)) value.class_list = className;
|
|
1383
1418
|
this.sendMessage({ message_type: 'KeyIntroduction', value });
|
|
1384
1419
|
}
|
|
1385
1420
|
return this.localKeyMap.get(name);
|
|
@@ -1421,11 +1456,24 @@
|
|
|
1421
1456
|
/**
|
|
1422
1457
|
* Delete a key by name if it exists in the localKeyMap. Safe to call multiple times.
|
|
1423
1458
|
* @param {string} keyName - Key name
|
|
1459
|
+
* @param {string|null} [className=null] - If set, removes the key from this class only via
|
|
1460
|
+
* a KeyIntroduction with deleted_class set; the key may still exist on the server in
|
|
1461
|
+
* other classes and the localKeyMap entry is preserved. If unset, sends DeleteKey to
|
|
1462
|
+
* remove the key entirely and forgets the local id so a future
|
|
1463
|
+
* getAndPossiblyIntroduceKeyId(keyName) re-introduces with a fresh id.
|
|
1424
1464
|
*/
|
|
1425
|
-
deleteKey(keyName) {
|
|
1465
|
+
deleteKey(keyName, className = null) {
|
|
1426
1466
|
const keyId = this.localKeyMap.get(keyName);
|
|
1427
|
-
if (keyId)
|
|
1467
|
+
if (keyId === undefined) return;
|
|
1468
|
+
if (className) {
|
|
1469
|
+
this.sendMessage({
|
|
1470
|
+
message_type: 'KeyIntroduction',
|
|
1471
|
+
value: { key_id: keyId, name: keyName, deleted_class: className },
|
|
1472
|
+
});
|
|
1473
|
+
} else {
|
|
1428
1474
|
this.publishDeleteKey(keyId);
|
|
1475
|
+
// Forget the local id so future re-introduction allocates a new one.
|
|
1476
|
+
this.localKeyMap.delete(keyName);
|
|
1429
1477
|
}
|
|
1430
1478
|
}
|
|
1431
1479
|
|
|
@@ -1469,6 +1517,10 @@
|
|
|
1469
1517
|
* @param {any} value - JSON-serializable value
|
|
1470
1518
|
*/
|
|
1471
1519
|
publishRecordWithIds(keyId, topicId, value) {
|
|
1520
|
+
if (value === undefined) {
|
|
1521
|
+
this.log('ERROR', `publishRecordWithIds(${keyId}, ${topicId}): refusing to publish undefined — JSON.stringify would drop the "value" field and the server would reject with "Missing record 'value' field"`);
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1472
1524
|
const updateMsg = {
|
|
1473
1525
|
message_type: 'JSONRecordUpdate',
|
|
1474
1526
|
value: {
|
|
@@ -1487,6 +1539,10 @@
|
|
|
1487
1539
|
* @param {string|null} [className=null] - Class name
|
|
1488
1540
|
*/
|
|
1489
1541
|
publishRecord(keyName, topicName, value, className = null) {
|
|
1542
|
+
if (value === undefined) {
|
|
1543
|
+
this.log('ERROR', `publishRecord(${keyName}, ${topicName}): refusing to publish undefined — JSON.stringify would drop the "value" field and the server would reject with "Missing record 'value' field"`);
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1490
1546
|
const keyId = this.getAndPossiblyIntroduceKeyId(keyName, className);
|
|
1491
1547
|
const topicId = this.getAndPossiblyIntroduceTopicId(topicName);
|
|
1492
1548
|
this.publishRecordWithIds(keyId, topicId, value);
|
package/gar.js
CHANGED
|
@@ -194,6 +194,7 @@ class GARClient {
|
|
|
194
194
|
this.websocket = null;
|
|
195
195
|
this.messageQueue = [];
|
|
196
196
|
this.connected = false;
|
|
197
|
+
this.connectedCallback = null;
|
|
197
198
|
this.reconnectDelay = 5000; // Milliseconds
|
|
198
199
|
this.user = user;
|
|
199
200
|
this.working_namespace = working_namespace;
|
|
@@ -377,18 +378,18 @@ class GARClient {
|
|
|
377
378
|
});
|
|
378
379
|
|
|
379
380
|
this.websocket = await connectionPromise;
|
|
380
|
-
this.
|
|
381
|
+
this._setConnected(true);
|
|
381
382
|
this.log('INFO', `Connected to WebSocket server at ${this.wsEndpoint} using gar-protocol`);
|
|
382
383
|
|
|
383
384
|
this.websocket.onclose = () => {
|
|
384
385
|
this.log('WARNING', 'WebSocket connection closed.');
|
|
385
|
-
this.
|
|
386
|
+
this._setConnected(false);
|
|
386
387
|
this.websocket = null;
|
|
387
388
|
this._reconnect();
|
|
388
389
|
};
|
|
389
390
|
this.websocket.onerror = (error) => {
|
|
390
391
|
this.log('ERROR', `WebSocket error: ${error.message || 'Unknown error'}`);
|
|
391
|
-
this.
|
|
392
|
+
this._setConnected(false);
|
|
392
393
|
this.websocket = null;
|
|
393
394
|
};
|
|
394
395
|
|
|
@@ -396,7 +397,7 @@ class GARClient {
|
|
|
396
397
|
this._receiveMessages();
|
|
397
398
|
|
|
398
399
|
} catch (e) {
|
|
399
|
-
this.
|
|
400
|
+
this._setConnected(false);
|
|
400
401
|
this.websocket = null;
|
|
401
402
|
// 401 is a permanent auth rejection — retrying won't help
|
|
402
403
|
if (e.message && e.message.includes('401')) {
|
|
@@ -448,7 +449,7 @@ class GARClient {
|
|
|
448
449
|
}
|
|
449
450
|
this.log('INFO', 'Done sending messages.');
|
|
450
451
|
this.stop()
|
|
451
|
-
this.
|
|
452
|
+
this._setConnected(false);
|
|
452
453
|
}
|
|
453
454
|
|
|
454
455
|
/**
|
|
@@ -461,6 +462,34 @@ class GARClient {
|
|
|
461
462
|
};
|
|
462
463
|
}
|
|
463
464
|
|
|
465
|
+
/**
|
|
466
|
+
* Register a callback fired when the WS `connected` flag transitions.
|
|
467
|
+
* @param {Function} handler - Callback receiving the new boolean value
|
|
468
|
+
*/
|
|
469
|
+
registerConnectedHandler(handler) {
|
|
470
|
+
this.connectedCallback = handler;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Clear the connected handler.
|
|
475
|
+
*/
|
|
476
|
+
clearConnectedHandler() {
|
|
477
|
+
this.connectedCallback = null;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Internal setter that fires connectedCallback on state transitions.
|
|
482
|
+
* @param {boolean} value
|
|
483
|
+
*/
|
|
484
|
+
_setConnected(value) {
|
|
485
|
+
if (this.connected === value) return;
|
|
486
|
+
this.connected = value;
|
|
487
|
+
if (this.connectedCallback) {
|
|
488
|
+
try { this.connectedCallback(value); }
|
|
489
|
+
catch (e) { this.log('ERROR', `connectedCallback threw: ${e.message}`); }
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
464
493
|
/**
|
|
465
494
|
* Register a callback handler for a specific message type.
|
|
466
495
|
* @param {string} messageType - Type of message
|
|
@@ -796,7 +825,7 @@ class GARClient {
|
|
|
796
825
|
* Stop the client without sending pending messages.
|
|
797
826
|
*/
|
|
798
827
|
halt() {
|
|
799
|
-
this.
|
|
828
|
+
this._setConnected(false);
|
|
800
829
|
this.stop();
|
|
801
830
|
}
|
|
802
831
|
|
|
@@ -1066,9 +1095,18 @@ class GARClient {
|
|
|
1066
1095
|
this.log('INFO', 'Received Logoff from server');
|
|
1067
1096
|
this.stop();
|
|
1068
1097
|
} else if (msgType === 'Error') {
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1098
|
+
// proto.gar's `error` struct carries `recoverable: bool`. Recoverable errors mean
|
|
1099
|
+
// the server processed the failing message (e.g. a publish that violated some
|
|
1100
|
+
// server-side check), logged it, and kept the connection alive. We dispatch the
|
|
1101
|
+
// message to the user's error handler but don't tear down the client. Non-recoverable
|
|
1102
|
+
// errors are followed by a server disconnect; we set exit_code + stop() so callers
|
|
1103
|
+
// (CLI tools, the html_view UI) see the failure.
|
|
1104
|
+
const recoverable = !!message.value.recoverable;
|
|
1105
|
+
this.log(recoverable ? 'WARNING' : 'ERROR', `GAR ${message.value.message}`);
|
|
1106
|
+
if (!recoverable) {
|
|
1107
|
+
this.exitCode = 1;
|
|
1108
|
+
this.stop();
|
|
1109
|
+
}
|
|
1072
1110
|
}
|
|
1073
1111
|
|
|
1074
1112
|
this.checkHeartbeat();
|
|
@@ -1352,28 +1390,25 @@ class GARClient {
|
|
|
1352
1390
|
}
|
|
1353
1391
|
|
|
1354
1392
|
/**
|
|
1355
|
-
* Introduce a new key if not already known and return local key ID.
|
|
1393
|
+
* Introduce a new key if not already known and return local key ID. className is required;
|
|
1394
|
+
* a bare introduction with no class_list can't be routed by upstream proxies.
|
|
1356
1395
|
* @param {string} name - Key name
|
|
1357
|
-
* @param {string|Array<string
|
|
1396
|
+
* @param {string|Array<string>} className - Class name(s)
|
|
1358
1397
|
* @returns {number} Local key ID
|
|
1359
1398
|
*/
|
|
1360
|
-
getAndPossiblyIntroduceKeyId(name, className
|
|
1399
|
+
getAndPossiblyIntroduceKeyId(name, className) {
|
|
1361
1400
|
const existingId = this.localKeyMap.get(name);
|
|
1362
1401
|
if (existingId !== undefined && this.invalidatedKeyIds.has(existingId)) {
|
|
1363
1402
|
this.invalidatedKeyIds.delete(existingId);
|
|
1364
1403
|
this.localKeyMap.delete(name);
|
|
1365
1404
|
}
|
|
1366
1405
|
if (!this.localKeyMap.has(name)) {
|
|
1406
|
+
if (!className) throw new Error(`getAndPossiblyIntroduceKeyId: className required to introduce key "${name}"`);
|
|
1367
1407
|
const keyId = this.localKeyCounter++;
|
|
1368
1408
|
this.localKeyMap.set(name, keyId);
|
|
1369
1409
|
const value = { key_id: keyId, name };
|
|
1370
|
-
if (className)
|
|
1371
|
-
|
|
1372
|
-
value.class_list = className.split(/\s+/).filter(Boolean);
|
|
1373
|
-
} else if (Array.isArray(className)) {
|
|
1374
|
-
value.class_list = className;
|
|
1375
|
-
}
|
|
1376
|
-
}
|
|
1410
|
+
if (typeof className === 'string') value.class_list = className.split(/\s+/).filter(Boolean);
|
|
1411
|
+
else if (Array.isArray(className)) value.class_list = className;
|
|
1377
1412
|
this.sendMessage({ message_type: 'KeyIntroduction', value });
|
|
1378
1413
|
}
|
|
1379
1414
|
return this.localKeyMap.get(name);
|
|
@@ -1415,11 +1450,24 @@ class GARClient {
|
|
|
1415
1450
|
/**
|
|
1416
1451
|
* Delete a key by name if it exists in the localKeyMap. Safe to call multiple times.
|
|
1417
1452
|
* @param {string} keyName - Key name
|
|
1453
|
+
* @param {string|null} [className=null] - If set, removes the key from this class only via
|
|
1454
|
+
* a KeyIntroduction with deleted_class set; the key may still exist on the server in
|
|
1455
|
+
* other classes and the localKeyMap entry is preserved. If unset, sends DeleteKey to
|
|
1456
|
+
* remove the key entirely and forgets the local id so a future
|
|
1457
|
+
* getAndPossiblyIntroduceKeyId(keyName) re-introduces with a fresh id.
|
|
1418
1458
|
*/
|
|
1419
|
-
deleteKey(keyName) {
|
|
1459
|
+
deleteKey(keyName, className = null) {
|
|
1420
1460
|
const keyId = this.localKeyMap.get(keyName);
|
|
1421
|
-
if (keyId)
|
|
1461
|
+
if (keyId === undefined) return;
|
|
1462
|
+
if (className) {
|
|
1463
|
+
this.sendMessage({
|
|
1464
|
+
message_type: 'KeyIntroduction',
|
|
1465
|
+
value: { key_id: keyId, name: keyName, deleted_class: className },
|
|
1466
|
+
});
|
|
1467
|
+
} else {
|
|
1422
1468
|
this.publishDeleteKey(keyId);
|
|
1469
|
+
// Forget the local id so future re-introduction allocates a new one.
|
|
1470
|
+
this.localKeyMap.delete(keyName);
|
|
1423
1471
|
}
|
|
1424
1472
|
}
|
|
1425
1473
|
|
|
@@ -1463,6 +1511,10 @@ class GARClient {
|
|
|
1463
1511
|
* @param {any} value - JSON-serializable value
|
|
1464
1512
|
*/
|
|
1465
1513
|
publishRecordWithIds(keyId, topicId, value) {
|
|
1514
|
+
if (value === undefined) {
|
|
1515
|
+
this.log('ERROR', `publishRecordWithIds(${keyId}, ${topicId}): refusing to publish undefined — JSON.stringify would drop the "value" field and the server would reject with "Missing record 'value' field"`);
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1466
1518
|
const updateMsg = {
|
|
1467
1519
|
message_type: 'JSONRecordUpdate',
|
|
1468
1520
|
value: {
|
|
@@ -1481,6 +1533,10 @@ class GARClient {
|
|
|
1481
1533
|
* @param {string|null} [className=null] - Class name
|
|
1482
1534
|
*/
|
|
1483
1535
|
publishRecord(keyName, topicName, value, className = null) {
|
|
1536
|
+
if (value === undefined) {
|
|
1537
|
+
this.log('ERROR', `publishRecord(${keyName}, ${topicName}): refusing to publish undefined — JSON.stringify would drop the "value" field and the server would reject with "Missing record 'value' field"`);
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
1484
1540
|
const keyId = this.getAndPossiblyIntroduceKeyId(keyName, className);
|
|
1485
1541
|
const topicId = this.getAndPossiblyIntroduceTopicId(topicName);
|
|
1486
1542
|
this.publishRecordWithIds(keyId, topicId, value);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jsgar",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.9.1",
|
|
4
4
|
"description": "A Javascript client for the GAR protocol",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/gar.umd.js",
|
|
@@ -36,8 +36,11 @@
|
|
|
36
36
|
"@eslint/json": "^0.13.2",
|
|
37
37
|
"@rollup/plugin-commonjs": "^24.0.0",
|
|
38
38
|
"@rollup/plugin-node-resolve": "^15.0.0",
|
|
39
|
+
"ag-grid-community": "^34.1.1",
|
|
40
|
+
"ag-grid-enterprise": "^34.1.1",
|
|
39
41
|
"eslint": "^9.39.4",
|
|
40
42
|
"globals": "^16.0.0",
|
|
43
|
+
"playwright": "^1.50.0",
|
|
41
44
|
"rollup": "^3.0.0"
|
|
42
45
|
}
|
|
43
46
|
}
|