jsgar 4.13.0 → 4.13.3

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 +57 -5
  2. package/gar.js +57 -5
  3. package/package.json +3 -1
package/dist/gar.umd.js CHANGED
@@ -852,6 +852,31 @@
852
852
  */
853
853
  sendMessage(message) {
854
854
  this.messageQueue.push(message);
855
+ // Drain synchronously when possible: background tabs throttle timers — the polling
856
+ // drain loop (and any setInterval sender) can stretch from 100ms to minutes — but
857
+ // synchronous sends from an event or enqueue are never throttled.
858
+ this._flushQueue();
859
+ }
860
+
861
+ /**
862
+ * Synchronously send everything sendable. Safe alongside the polling loop (same JS
863
+ * thread); leaves the null shutdown sentinel for the loop's exit handling.
864
+ */
865
+ _flushQueue() {
866
+ while (this.connected && this.messageQueue.length > 0 && this._isSocketOpen()) {
867
+ if (this.messageQueue[0] === null) return;
868
+ const message = this.messageQueue.shift();
869
+ try {
870
+ this.websocket.send(JSON.stringify(message));
871
+ if (this.logEveryMessage) {
872
+ this.log('INFO', `Sent: ${JSON.stringify(message)}`);
873
+ }
874
+ } catch (e) {
875
+ this.log('WARNING', `Error sending message: ${e.message}`);
876
+ this.halt();
877
+ return;
878
+ }
879
+ }
855
880
  }
856
881
 
857
882
  /**
@@ -979,6 +1004,13 @@
979
1004
  this.invalidatedKeyIds.add(message.value.client_introduced_key_id);
980
1005
  } else if (msgType === 'Heartbeat') {
981
1006
  this.lastHeartbeatTime = Date.now() / 1000;
1007
+ // Echo our own heartbeat on the server's cadence. A hidden tab's throttled timers
1008
+ // can silence the periodic sender far past the server's timeout while this
1009
+ // (unthrottled) receive path keeps delivering — the server then rightly declares
1010
+ // us dead. Event-driven liveness: receive -> reply, flushed synchronously above.
1011
+ if (this.running) {
1012
+ this.sendMessage({ message_type: 'Heartbeat', value: { u_milliseconds: Date.now() } });
1013
+ }
982
1014
  if (this._initialGracePeriod) {
983
1015
  // Resolve first-heartbeat waiters and end the initial grace window
984
1016
  if (this._firstHeartbeatResolve) {
@@ -1147,7 +1179,9 @@
1147
1179
  * @param {Object} opts - Named subscription options (fields below).
1148
1180
  * @param {string} opts.name - Subscription name
1149
1181
  * @param {string} [opts.subscriptionMode='Streaming'] - Subscription mode
1150
- * @param {string|Array<string>|null} [keyName=null] - Key name(s)
1182
+ * @param {string|Array<string>|Array<number>|null} [keyName=null] - Key name(s), matched
1183
+ * exactly via a synthesized key_filter alternation (keys are NOT created by
1184
+ * subscribing); an array of integers passes through as the wire key_id_list
1151
1185
  * @param {string|Array<string>|null} [topicName=null] - Topic name(s)
1152
1186
  * @param {string|Array<string>|null} [className=null] - Class name(s)
1153
1187
  * @param {string|null} [keyFilter=null] - Key filter regex (cannot use with keyName)
@@ -1226,16 +1260,24 @@
1226
1260
  referencingClasses = referencingClassList;
1227
1261
  }
1228
1262
 
1229
- let singleClass = Array.isArray(classList) && classList.length === 1 ? classList[0] : null;
1230
-
1231
- // Convert keyName to array
1263
+ // Convert keyName to array. Subscribing must NOT create keys: names are no longer
1264
+ // introduced to build a key_id_list (the old behavior materialized every named key on
1265
+ // the server). Names become an exact-match alternation key_filter instead — the server
1266
+ // recognizes the literal-alternation shape and builds a trie, skipping the full
1267
+ // regex->DFA compilation. An array of integers passes through as key_id_list verbatim
1268
+ // (the caller already holds real key ids).
1232
1269
  let keyNames = [];
1233
1270
  if (typeof keyName === 'string') {
1234
1271
  keyNames = [keyName];
1235
1272
  } else if (Array.isArray(keyName)) {
1236
1273
  keyNames = keyName;
1237
1274
  }
1238
- const keyIdList = keyNames.map(x => this.getAndPossiblyIntroduceKeyId(x, singleClass));
1275
+ let keyIdList = [];
1276
+ if (keyNames.length > 0 && keyNames.every(Number.isInteger)) {
1277
+ keyIdList = keyNames;
1278
+ } else if (keyNames.length > 0) {
1279
+ keyFilter = keyNames.map(GARClient.escapeRegex).join('|');
1280
+ }
1239
1281
 
1240
1282
  // Convert topicName to array
1241
1283
  let topicNames = [];
@@ -1817,6 +1859,16 @@
1817
1859
  });
1818
1860
  }
1819
1861
 
1862
+ /**
1863
+ * Escape TRS-regex metacharacters so `name` matches literally (TRS regexes match the
1864
+ * whole string, so an escaped name is an exact-match alternative in a key_filter).
1865
+ * @param {string} name
1866
+ * @returns {string}
1867
+ */
1868
+ static escapeRegex(name) {
1869
+ return String(name).replace(/[\\.^$|?*+()[\]{}]/g, '\\$&');
1870
+ }
1871
+
1820
1872
  static _generateUUID() {
1821
1873
  const bytes = new Uint8Array(16);
1822
1874
  if (typeof globalThis.crypto !== 'undefined' && globalThis.crypto.getRandomValues)
package/gar.js CHANGED
@@ -845,6 +845,31 @@ class GARClient {
845
845
  */
846
846
  sendMessage(message) {
847
847
  this.messageQueue.push(message);
848
+ // Drain synchronously when possible: background tabs throttle timers — the polling
849
+ // drain loop (and any setInterval sender) can stretch from 100ms to minutes — but
850
+ // synchronous sends from an event or enqueue are never throttled.
851
+ this._flushQueue();
852
+ }
853
+
854
+ /**
855
+ * Synchronously send everything sendable. Safe alongside the polling loop (same JS
856
+ * thread); leaves the null shutdown sentinel for the loop's exit handling.
857
+ */
858
+ _flushQueue() {
859
+ while (this.connected && this.messageQueue.length > 0 && this._isSocketOpen()) {
860
+ if (this.messageQueue[0] === null) return;
861
+ const message = this.messageQueue.shift();
862
+ try {
863
+ this.websocket.send(JSON.stringify(message));
864
+ if (this.logEveryMessage) {
865
+ this.log('INFO', `Sent: ${JSON.stringify(message)}`);
866
+ }
867
+ } catch (e) {
868
+ this.log('WARNING', `Error sending message: ${e.message}`);
869
+ this.halt();
870
+ return;
871
+ }
872
+ }
848
873
  }
849
874
 
850
875
  /**
@@ -972,6 +997,13 @@ class GARClient {
972
997
  this.invalidatedKeyIds.add(message.value.client_introduced_key_id);
973
998
  } else if (msgType === 'Heartbeat') {
974
999
  this.lastHeartbeatTime = Date.now() / 1000;
1000
+ // Echo our own heartbeat on the server's cadence. A hidden tab's throttled timers
1001
+ // can silence the periodic sender far past the server's timeout while this
1002
+ // (unthrottled) receive path keeps delivering — the server then rightly declares
1003
+ // us dead. Event-driven liveness: receive -> reply, flushed synchronously above.
1004
+ if (this.running) {
1005
+ this.sendMessage({ message_type: 'Heartbeat', value: { u_milliseconds: Date.now() } });
1006
+ }
975
1007
  if (this._initialGracePeriod) {
976
1008
  // Resolve first-heartbeat waiters and end the initial grace window
977
1009
  if (this._firstHeartbeatResolve) {
@@ -1140,7 +1172,9 @@ class GARClient {
1140
1172
  * @param {Object} opts - Named subscription options (fields below).
1141
1173
  * @param {string} opts.name - Subscription name
1142
1174
  * @param {string} [opts.subscriptionMode='Streaming'] - Subscription mode
1143
- * @param {string|Array<string>|null} [keyName=null] - Key name(s)
1175
+ * @param {string|Array<string>|Array<number>|null} [keyName=null] - Key name(s), matched
1176
+ * exactly via a synthesized key_filter alternation (keys are NOT created by
1177
+ * subscribing); an array of integers passes through as the wire key_id_list
1144
1178
  * @param {string|Array<string>|null} [topicName=null] - Topic name(s)
1145
1179
  * @param {string|Array<string>|null} [className=null] - Class name(s)
1146
1180
  * @param {string|null} [keyFilter=null] - Key filter regex (cannot use with keyName)
@@ -1219,16 +1253,24 @@ class GARClient {
1219
1253
  referencingClasses = referencingClassList;
1220
1254
  }
1221
1255
 
1222
- let singleClass = Array.isArray(classList) && classList.length === 1 ? classList[0] : null;
1223
-
1224
- // Convert keyName to array
1256
+ // Convert keyName to array. Subscribing must NOT create keys: names are no longer
1257
+ // introduced to build a key_id_list (the old behavior materialized every named key on
1258
+ // the server). Names become an exact-match alternation key_filter instead — the server
1259
+ // recognizes the literal-alternation shape and builds a trie, skipping the full
1260
+ // regex->DFA compilation. An array of integers passes through as key_id_list verbatim
1261
+ // (the caller already holds real key ids).
1225
1262
  let keyNames = [];
1226
1263
  if (typeof keyName === 'string') {
1227
1264
  keyNames = [keyName];
1228
1265
  } else if (Array.isArray(keyName)) {
1229
1266
  keyNames = keyName;
1230
1267
  }
1231
- const keyIdList = keyNames.map(x => this.getAndPossiblyIntroduceKeyId(x, singleClass));
1268
+ let keyIdList = [];
1269
+ if (keyNames.length > 0 && keyNames.every(Number.isInteger)) {
1270
+ keyIdList = keyNames;
1271
+ } else if (keyNames.length > 0) {
1272
+ keyFilter = keyNames.map(GARClient.escapeRegex).join('|');
1273
+ }
1232
1274
 
1233
1275
  // Convert topicName to array
1234
1276
  let topicNames = [];
@@ -1810,6 +1852,16 @@ class GARClient {
1810
1852
  });
1811
1853
  }
1812
1854
 
1855
+ /**
1856
+ * Escape TRS-regex metacharacters so `name` matches literally (TRS regexes match the
1857
+ * whole string, so an escaped name is an exact-match alternative in a key_filter).
1858
+ * @param {string} name
1859
+ * @returns {string}
1860
+ */
1861
+ static escapeRegex(name) {
1862
+ return String(name).replace(/[\\.^$|?*+()[\]{}]/g, '\\$&');
1863
+ }
1864
+
1813
1865
  static _generateUUID() {
1814
1866
  const bytes = new Uint8Array(16);
1815
1867
  if (typeof globalThis.crypto !== 'undefined' && globalThis.crypto.getRandomValues)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsgar",
3
- "version": "4.13.0",
3
+ "version": "4.13.3",
4
4
  "description": "A Javascript client for the GAR protocol",
5
5
  "type": "module",
6
6
  "main": "dist/gar.umd.js",
@@ -36,6 +36,8 @@
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
+ "@xterm/addon-fit": "^0.10.0",
40
+ "@xterm/xterm": "^5.5.0",
39
41
  "ag-grid-community": "^34.1.1",
40
42
  "ag-grid-enterprise": "^34.1.1",
41
43
  "eslint": "^9.39.4",