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.
Files changed (3) hide show
  1. package/dist/gar.umd.js +77 -21
  2. package/gar.js +77 -21
  3. 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.connected = true;
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.connected = false;
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.connected = false;
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.connected = false;
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.connected = false;
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.connected = false;
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
- this.log('ERROR', `GAR ${message.value.message}`);
1076
- this.exitCode = 1;
1077
- this.stop();
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>|null} [className=null] - Class name(s)
1402
+ * @param {string|Array<string>} className - Class name(s)
1364
1403
  * @returns {number} Local key ID
1365
1404
  */
1366
- getAndPossiblyIntroduceKeyId(name, className = null) {
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
- if (typeof className === 'string') {
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.connected = true;
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.connected = false;
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.connected = false;
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.connected = false;
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.connected = false;
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.connected = false;
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
- this.log('ERROR', `GAR ${message.value.message}`);
1070
- this.exitCode = 1;
1071
- this.stop();
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>|null} [className=null] - Class name(s)
1396
+ * @param {string|Array<string>} className - Class name(s)
1358
1397
  * @returns {number} Local key ID
1359
1398
  */
1360
- getAndPossiblyIntroduceKeyId(name, className = null) {
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
- if (typeof className === 'string') {
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.8.0",
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
  }