@yuneta/gobj-js 7.26.25 → 7.26.27

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.
@@ -979,11 +979,14 @@ function value_has_shape(type, value) {
979
979
  if (DTP_IS_NUMBER(type)) return typeof value === "number" || typeof value === "boolean";
980
980
  return true;
981
981
  }
982
+ function is_int64_number(value) {
983
+ return value >= -0x8000000000000000 && value < 0x8000000000000000;
984
+ }
982
985
  function shape_of_value(value) {
983
986
  if (is_array(value)) return "an array";
984
987
  if (is_object(value)) return "an object";
985
988
  if (is_string(value)) return REFUSED_TEXT;
986
- if (typeof value === "number") return Number.isInteger(value) && Math.abs(value) <= 0x8000000000000000 ? "an integer" : "a real";
989
+ if (typeof value === "number") return Number.isInteger(value) && is_int64_number(value) ? "an integer" : "a real";
987
990
  if (typeof value === "boolean") return "a boolean";
988
991
  return "null";
989
992
  }
@@ -1004,7 +1007,7 @@ function number_as_type(type, value) {
1004
1007
  value: value ? 1 : 0,
1005
1008
  refused: null
1006
1009
  };
1007
- if (!(value >= -0x8000000000000000 && value <= 0x8000000000000000)) return {
1010
+ if (!is_int64_number(value)) return {
1008
1011
  value: void 0,
1009
1012
  refused: "a number out of its range"
1010
1013
  };
@@ -1069,8 +1072,13 @@ function text2parameter(type, s, from_kw) {
1069
1072
  value: n !== 0n,
1070
1073
  refused: null
1071
1074
  };
1075
+ const v = Number(n);
1076
+ if (!is_int64_number(v)) return {
1077
+ value: void 0,
1078
+ refused: "a number out of its range"
1079
+ };
1072
1080
  return {
1073
- value: Number(n),
1081
+ value: v,
1074
1082
  refused: null
1075
1083
  };
1076
1084
  }
@@ -1129,6 +1137,16 @@ function command_parameter_from_text(type, text) {
1129
1137
  * undefined, or an empty text given to a number).
1130
1138
  ***************************************************************************/
1131
1139
  function command_parameter_from_json(type, value) {
1140
+ try {
1141
+ return parameter_from_json(type, value);
1142
+ } catch (e) {
1143
+ return {
1144
+ value: void 0,
1145
+ refused: NO_JSON_TEXT
1146
+ };
1147
+ }
1148
+ }
1149
+ function parameter_from_json(type, value) {
1132
1150
  if (value === void 0 || value === null) {
1133
1151
  if (DTP_IS_JSON(type) && value === null) return {
1134
1152
  value: null,
@@ -1187,31 +1205,63 @@ var TYPES_READ = [
1187
1205
  data_type_t.DTP_LIST,
1188
1206
  data_type_t.DTP_DICT
1189
1207
  ];
1190
- var LEAVE_NODE = Symbol("leave");
1191
- function is_json_value(value) {
1192
- const on_path = /* @__PURE__ */ new Set();
1193
- const stack = [value];
1194
- while (stack.length) {
1195
- const v = stack.pop();
1196
- if (v === LEAVE_NODE) {
1197
- on_path.delete(stack.pop());
1198
- continue;
1208
+ function is_plain_object$1(v) {
1209
+ const proto = Object.getPrototypeOf(v);
1210
+ if (proto === Object.prototype || proto === null) return true;
1211
+ if (Object.getPrototypeOf(proto) !== null || !Object.prototype.hasOwnProperty.call(proto, "constructor")) return false;
1212
+ const ctor = proto.constructor;
1213
+ return typeof ctor === "function" && ctor.name === "Object";
1214
+ }
1215
+ function json_scalar_or_container(v) {
1216
+ switch (typeof v) {
1217
+ case "string":
1218
+ case "boolean": return true;
1219
+ case "number": return Number.isFinite(v);
1220
+ case "object": return v === null ? true : null;
1221
+ default: return false;
1222
+ }
1223
+ }
1224
+ function is_json_node(value) {
1225
+ const top = json_scalar_or_container(value);
1226
+ if (top !== null) return top;
1227
+ const values = [value];
1228
+ const levels = [1];
1229
+ while (values.length) {
1230
+ const v = values.pop();
1231
+ const level = levels.pop() + 1;
1232
+ let children;
1233
+ if (Array.isArray(v)) {
1234
+ if (typeof v.toJSON === "function") return false;
1235
+ children = v;
1236
+ } else {
1237
+ if (!is_plain_object$1(v) || typeof v.toJSON === "function") return false;
1238
+ children = [];
1239
+ for (const key of Object.keys(v)) {
1240
+ const x = v[key];
1241
+ if (x === void 0 || typeof x === "function" || typeof x === "symbol") continue;
1242
+ children.push(x);
1243
+ }
1199
1244
  }
1200
- if (typeof v === "bigint") return false;
1201
- if (v === null || typeof v !== "object") continue;
1202
- if (on_path.has(v)) return false;
1203
- const array = Array.isArray(v);
1204
- if (!array) {
1205
- const proto = Object.getPrototypeOf(v);
1206
- if (proto !== Object.prototype && proto !== null) return false;
1245
+ if (children.length > 0 && level > JSON_MAX_DEPTH) return false;
1246
+ for (let i = 0; i < children.length; i++) {
1247
+ const x = children[i];
1248
+ const r = json_scalar_or_container(x);
1249
+ if (r === false) return false;
1250
+ if (r === null) {
1251
+ values.push(x);
1252
+ levels.push(level);
1253
+ }
1207
1254
  }
1208
- on_path.add(v);
1209
- stack.push(v, LEAVE_NODE);
1210
- if (array) for (let i = 0; i < v.length; i++) stack.push(v[i]);
1211
- else for (const key of Object.keys(v)) stack.push(v[key]);
1212
1255
  }
1213
1256
  return true;
1214
1257
  }
1258
+ function is_json_value(value) {
1259
+ try {
1260
+ return is_json_node(value);
1261
+ } catch (e) {
1262
+ return false;
1263
+ }
1264
+ }
1215
1265
  var NO_JSON_TEXT = "a value with no json text";
1216
1266
  function json_text_of(value) {
1217
1267
  if (!is_json_value(value)) return;
@@ -2342,11 +2392,13 @@ function rc_walk_by_list(iter_, walk_type, cb_walking, user_data, user_data2) {
2342
2392
  let child;
2343
2393
  if (walk_type & walk_type_t.WALK_LAST2FIRST) for (let i = iter.length; i > 0; i--) {
2344
2394
  child = iter[i - 1];
2395
+ if (gobj_is_destroying(child)) continue;
2345
2396
  let ret = walk_ret(cb_walking(child, user_data, user_data2));
2346
2397
  if (ret < 0) return ret;
2347
2398
  }
2348
2399
  else for (let i = 0; i < iter.length; i++) {
2349
2400
  child = iter[i];
2401
+ if (gobj_is_destroying(child)) continue;
2350
2402
  let ret = walk_ret(cb_walking(child, user_data, user_data2));
2351
2403
  if (ret < 0) return ret;
2352
2404
  }
@@ -2361,18 +2413,21 @@ function rc_walk_by_list(iter_, walk_type, cb_walking, user_data, user_data2) {
2361
2413
  * or
2362
2414
  * WALK_LAST2FIRST
2363
2415
  ***************************************************************/
2364
- function rc_walk_by_level(iter, walk_type, cb_walking, user_data, user_data2) {
2416
+ function rc_walk_by_level(iter_, walk_type, cb_walking, user_data, user_data2) {
2417
+ let iter = [...iter_];
2365
2418
  let ret = rc_walk_by_list(iter, walk_type, cb_walking, user_data, user_data2);
2366
2419
  if (ret < 0) return ret;
2367
2420
  let child;
2368
2421
  if (walk_type & walk_type_t.WALK_LAST2FIRST) for (let i = iter.length; i > 0; i--) {
2369
2422
  child = iter[i - 1];
2370
- ret = rc_walk_by_level([...child.dl_children], walk_type, cb_walking, user_data, user_data2);
2423
+ if (gobj_is_destroying(child)) continue;
2424
+ ret = rc_walk_by_level(child.dl_children, walk_type, cb_walking, user_data, user_data2);
2371
2425
  if (ret < 0) return ret;
2372
2426
  }
2373
2427
  else for (let i = 0; i < iter.length; i++) {
2374
2428
  child = iter[i];
2375
- ret = rc_walk_by_level([...child.dl_children], walk_type, cb_walking, user_data, user_data2);
2429
+ if (gobj_is_destroying(child)) continue;
2430
+ ret = rc_walk_by_level(child.dl_children, walk_type, cb_walking, user_data, user_data2);
2376
2431
  if (ret < 0) return ret;
2377
2432
  }
2378
2433
  return ret;
@@ -2392,23 +2447,27 @@ function rc_walk_by_level(iter, walk_type, cb_walking, user_data, user_data2) {
2392
2447
  * or
2393
2448
  * WALK_LAST2FIRST
2394
2449
  ***************************************************************/
2395
- function rc_walk_by_tree(iter, walk_type, cb_walking, user_data, user_data2) {
2396
- if (walk_type & walk_type_t.WALK_BYLEVEL) return rc_walk_by_level(iter, walk_type, cb_walking, user_data, user_data2);
2450
+ function rc_walk_by_tree(iter_, walk_type, cb_walking, user_data, user_data2) {
2451
+ if (walk_type & walk_type_t.WALK_BYLEVEL) return rc_walk_by_level(iter_, walk_type, cb_walking, user_data, user_data2);
2452
+ let iter = [...iter_];
2397
2453
  let ret;
2398
2454
  let child;
2399
2455
  if (walk_type & walk_type_t.WALK_BOTTOM2TOP) for (let i = iter.length; i > 0; i--) {
2400
2456
  child = iter[i - 1];
2401
- ret = rc_walk_by_tree([...child.dl_children], walk_type, cb_walking, user_data, user_data2);
2457
+ if (gobj_is_destroying(child)) continue;
2458
+ ret = rc_walk_by_tree(child.dl_children, walk_type, cb_walking, user_data, user_data2);
2402
2459
  if (ret < 0) return ret;
2460
+ if (gobj_is_destroying(child)) continue;
2403
2461
  ret = walk_ret(cb_walking(child, user_data, user_data2));
2404
2462
  if (ret < 0) return ret;
2405
2463
  }
2406
2464
  else for (let i = 0; i < iter.length; i++) {
2407
2465
  child = iter[i];
2466
+ if (gobj_is_destroying(child)) continue;
2408
2467
  ret = walk_ret(cb_walking(child, user_data, user_data2));
2409
2468
  if (ret < 0) return ret;
2410
- else if (ret === 0) {
2411
- ret = rc_walk_by_tree([...child.dl_children], walk_type, cb_walking, user_data, user_data2);
2469
+ else if (ret === 0 && !gobj_is_destroying(child)) {
2470
+ ret = rc_walk_by_tree(child.dl_children, walk_type, cb_walking, user_data, user_data2);
2412
2471
  if (ret < 0) return ret;
2413
2472
  }
2414
2473
  }
@@ -4725,6 +4784,7 @@ function mark_inline_rules_of_now(str, marks) {
4725
4784
  mark_labels(str, marks);
4726
4785
  }
4727
4786
  var MASK_MAX_DECODINGS = 2;
4787
+ var VIEW_CHUNK = 8192;
4728
4788
  function encoded_ascii_at(str, p) {
4729
4789
  if (p + 3 > str.length || str.charCodeAt(p) !== 37) return -1;
4730
4790
  const hi = hex_digit_value(str.charCodeAt(p + 1));
@@ -4735,28 +4795,30 @@ function encoded_ascii_at(str, p) {
4735
4795
  }
4736
4796
  function mark_decoded_view(str, marks, decodings) {
4737
4797
  if (decodings >= MASK_MAX_DECODINGS) return;
4738
- const pieces = [];
4739
- let last = 0;
4740
- let p = str.indexOf("%");
4741
- while (p >= 0) {
4742
- const c = encoded_ascii_at(str, p);
4798
+ let first = str.indexOf("%");
4799
+ while (first >= 0 && encoded_ascii_at(str, first) < 0) first = str.indexOf("%", first + 1);
4800
+ if (first < 0) return;
4801
+ const codes = new Uint16Array(str.length - first);
4802
+ let n = 0;
4803
+ for (let p = first; p < str.length; p++) {
4804
+ const code = str.charCodeAt(p);
4805
+ const c = code === 37 ? encoded_ascii_at(str, p) : -1;
4743
4806
  if (c >= 0) {
4744
- pieces.push(str.substring(last, p), String.fromCharCode(c));
4745
- last = p + 3;
4746
- p = str.indexOf("%", last);
4747
- } else p = str.indexOf("%", p + 1);
4748
- }
4749
- if (pieces.length === 0) return;
4750
- pieces.push(str.substring(last));
4751
- const view = pieces.join("");
4807
+ codes[n++] = c;
4808
+ p += 2;
4809
+ } else codes[n++] = code;
4810
+ }
4811
+ const chunks = [str.substring(0, first)];
4812
+ for (let k = 0; k < n; k += VIEW_CHUNK) chunks.push(String.fromCharCode.apply(null, codes.subarray(k, Math.min(n, k + VIEW_CHUNK))));
4813
+ const view = chunks.join("");
4752
4814
  const view_marks = new Uint8Array(view.length);
4753
4815
  mark_inline_rules_of_now(view, view_marks);
4754
4816
  mark_decoded_view(view, view_marks, decodings + 1);
4755
4817
  let q = 0;
4756
4818
  for (let i = 0; i < view.length; i++) {
4757
- const n = encoded_ascii_at(str, q) >= 0 ? 3 : 1;
4758
- if (view_marks[i]) mark_range(marks, q, q + n);
4759
- q += n;
4819
+ const n_from = encoded_ascii_at(str, q) >= 0 ? 3 : 1;
4820
+ if (view_marks[i]) mark_range(marks, q, q + n_from);
4821
+ q += n_from;
4760
4822
  }
4761
4823
  }
4762
4824
  function mark_inline_secrets(str, marks) {
@@ -5990,21 +6052,88 @@ function kw_clone_by_not_keys(gobj, kw, keys, verbose) {
5990
6052
  return kw_clone;
5991
6053
  }
5992
6054
  /********************************************
6055
+ * The browser's localStorage, or null when there is none (outside a
6056
+ * browser).
6057
+ *
6058
+ * With the site's data blocked (Chrome with cookies blocked, a sandboxed
6059
+ * iframe), READING the window.localStorage property already throws a
6060
+ * SecurityError, not only its getItem(): every app reads its locale at
6061
+ * start, so the app did not start. Such a store is answered as one whose
6062
+ * every call throws that error, so each helper fails as it fails for any
6063
+ * store that refuses it: a read gives the default, a write -1.
6064
+ ********************************************/
6065
+ function local_store() {
6066
+ if (typeof window === "undefined") return null;
6067
+ let store;
6068
+ try {
6069
+ store = window.localStorage;
6070
+ } catch (e) {
6071
+ const refuse = () => {
6072
+ throw e;
6073
+ };
6074
+ return {
6075
+ getItem: refuse,
6076
+ setItem: refuse,
6077
+ removeItem: refuse
6078
+ };
6079
+ }
6080
+ return store || null;
6081
+ }
6082
+ /********************************************
6083
+ * A failure of the store that would repeat on every read is warned
6084
+ * once per key, until a read of the key goes well again.
6085
+ ********************************************/
6086
+ var local_storage_warned = /* @__PURE__ */ new Set();
6087
+ function warn_local_storage_once(key, msg) {
6088
+ if (local_storage_warned.has(key)) return;
6089
+ local_storage_warned.add(key);
6090
+ log_warning(msg);
6091
+ }
6092
+ /********************************************
5993
6093
  * Get a local attribute
6094
+ *
6095
+ * The json the entry holds, or `default_value` when there is none
6096
+ * (written there too with `create`). A json null is no value, as
6097
+ * json_absent() in C: the default. An entry that is not json (a
6098
+ * corrupt or foreign write) is a WARNING and is REMOVED, so the
6099
+ * next read finds no entry: it used to console.warn on every read,
6100
+ * for ever; a new corrupt entry is a new WARNING. Its text is never
6101
+ * shown (JSON.parse() quotes it in its error, and an entry can hold a
6102
+ * token). A store that cannot be read (blocked) is warned once per
6103
+ * key, and the default is NOT written there. Outside a browser: the
6104
+ * default.
5994
6105
  ********************************************/
5995
6106
  function kw_get_local_storage_value(key, default_value, create = false) {
5996
- if (typeof window === "undefined" || !window.localStorage) return;
6107
+ const store = local_store();
6108
+ if (!store) return default_value;
6109
+ const where = `kw_get_local_storage_value(): localStorage key "${key}"`;
6110
+ let text;
5997
6111
  try {
5998
- let value = window.localStorage.getItem(key);
5999
- if (value === null) {
6000
- if (create) kw_set_local_storage_value(key, default_value);
6001
- return default_value;
6002
- }
6003
- return JSON.parse(value);
6112
+ text = store.getItem(key);
6004
6113
  } catch (e) {
6005
- window.console.warn(`Error reading localStorage key "${key}":`, e);
6114
+ warn_local_storage_once(key, `${where} cannot be read (storage blocked?): the default is used`);
6006
6115
  return default_value;
6007
6116
  }
6117
+ let value = null;
6118
+ if (text !== null) try {
6119
+ value = JSON.parse(text);
6120
+ } catch (e) {
6121
+ value = null;
6122
+ try {
6123
+ store.removeItem(key);
6124
+ log_warning(`${where} does not hold json: removed, the default is used`);
6125
+ local_storage_warned.delete(key);
6126
+ } catch (e2) {
6127
+ warn_local_storage_once(key, `${where} does not hold json and cannot be removed: the default is used`);
6128
+ }
6129
+ text = void 0;
6130
+ }
6131
+ if (text !== void 0) local_storage_warned.delete(key);
6132
+ if (value === null) {
6133
+ if (create) kw_set_local_storage_value(key, default_value);
6134
+ return default_value;
6135
+ }
6136
+ return value;
6008
6137
  }
6009
6138
  /********************************************
6010
6139
  * Save a local attribute
@@ -6015,15 +6144,26 @@ function kw_get_local_storage_value(key, default_value, create = false) {
6015
6144
  * reported success on a store that had rejected the write: a full or blocked
6016
6145
  * localStorage (quota, private mode) silently discarded the change while the
6017
6146
  * UI showed it as saved.
6147
+ *
6148
+ * A value with no json text is refused with an ERROR, as a typed writer
6149
+ * refuses it (json_text_of()): a function was stored as the text
6150
+ * "undefined", and a NaN, a Date or a Map as a text that reads back as
6151
+ * another value. Checked in Node.js too, where there is no store.
6018
6152
  ********************************************/
6019
6153
  function kw_set_local_storage_value(key, value) {
6020
- if (typeof window === "undefined" || !window.localStorage) return 0;
6021
- if (!key || value === void 0) {
6022
- log_error(`kw_set_local_storage_value(): invalid key or value: key=${key}`);
6154
+ if (!key) {
6155
+ log_error(`kw_set_local_storage_value(): invalid key: key=${key}`);
6023
6156
  return -1;
6024
6157
  }
6158
+ const text = json_text_of(value);
6159
+ if (text === void 0) {
6160
+ log_error(`kw_set_local_storage_value(): cannot save "${key}": a value with no json text`);
6161
+ return -1;
6162
+ }
6163
+ const store = local_store();
6164
+ if (!store) return 0;
6025
6165
  try {
6026
- window.localStorage.setItem(key, JSON.stringify(value));
6166
+ store.setItem(key, text);
6027
6167
  return 0;
6028
6168
  } catch (e) {
6029
6169
  log_error(`kw_set_local_storage_value(): cannot save "${key}" (storage full or blocked?): ${e && e.message ? e.message : e}`);
@@ -6034,15 +6174,16 @@ function kw_set_local_storage_value(key, value) {
6034
6174
  * Remove local attribute
6035
6175
  ********************************************/
6036
6176
  function kw_remove_local_storage_value(key) {
6037
- if (typeof window === "undefined" || !window.localStorage) return;
6177
+ const store = local_store();
6178
+ if (!store) return;
6038
6179
  if (!key) {
6039
- window.console.warn(`Invalid key for localStorage removal: ${key}`);
6180
+ log_error(`kw_remove_local_storage_value(): invalid key: key=${key}`);
6040
6181
  return;
6041
6182
  }
6042
6183
  try {
6043
- window.localStorage.removeItem(key);
6184
+ store.removeItem(key);
6044
6185
  } catch (e) {
6045
- window.console.warn(`Error removing localStorage key "${key}":`, e);
6186
+ log_warning(`kw_remove_local_storage_value(): cannot remove "${key}" (storage blocked?): ${e && e.message ? e.message : e}`);
6046
6187
  }
6047
6188
  }
6048
6189
  /*************************************************************
@@ -7235,11 +7376,21 @@ function index_in_list(list, elm) {
7235
7376
  ************************************************************/
7236
7377
  function node_uuid() {
7237
7378
  let uuid = "";
7238
- if (typeof window !== "undefined" && window.localStorage) {
7239
- uuid = window.localStorage.getItem("host_uuid");
7379
+ const store = local_store();
7380
+ if (store) {
7381
+ try {
7382
+ uuid = store.getItem("host_uuid");
7383
+ } catch (e) {
7384
+ uuid = null;
7385
+ warn_local_storage_once("host_uuid", "node_uuid(): localStorage cannot be read (storage blocked?): a uuid of this page only");
7386
+ }
7240
7387
  if (!uuid && window.crypto?.randomUUID) {
7241
7388
  uuid = window.crypto.randomUUID();
7242
- window.localStorage.setItem("host_uuid", uuid);
7389
+ try {
7390
+ store.setItem("host_uuid", uuid);
7391
+ } catch (e) {
7392
+ warn_local_storage_once("host_uuid", "node_uuid(): localStorage cannot be written (storage full or blocked?): a uuid of this page only");
7393
+ }
7243
7394
  }
7244
7395
  } else if (typeof globalThis !== "undefined" && globalThis.crypto?.randomUUID) uuid = globalThis.crypto.randomUUID();
7245
7396
  else uuid = "00000000-0000-0000-0000-000000000000";
@@ -8528,7 +8679,7 @@ var attrs_table = [
8528
8679
  SDATA(data_type_t.DTP_STRING, "cert_pem", sdata_flag_t.SDF_PERSIST, "", "SSL server certification, PEM str format"),
8529
8680
  SDATA(data_type_t.DTP_JSON, "extra_info", sdata_flag_t.SDF_RD, "{}", "dict data set by user, added to the identity card msg."),
8530
8681
  SDATA(data_type_t.DTP_LIST, "required_services", sdata_flag_t.SDF_RD, "[]", "Services this LINK requires (identity card). Empty = the yuno's `required_services`, as before. A yuno with several links to DIFFERENT backends must set it per link: the yuno-wide list is the union, so every backend is told the service names of all the others."),
8531
- SDATA(data_type_t.DTP_INTEGER, "timeout_retry", sdata_flag_t.SDF_RD, "5000", "First reconnect delay (ms). It BACKS OFF from here, doubling up to timeout_retry_max, and resets when a session is reached"),
8682
+ SDATA(data_type_t.DTP_INTEGER, "timeout_retry", sdata_flag_t.SDF_RD, "5000", "First reconnect delay (ms), at least 1000. It BACKS OFF from here, doubling up to timeout_retry_max, and resets when a session is reached"),
8532
8683
  SDATA(data_type_t.DTP_INTEGER, "timeout_retry_max", sdata_flag_t.SDF_RD, "60000", "Cap of the reconnect backoff (ms). Set it equal to timeout_retry for the old fixed-interval behaviour"),
8533
8684
  SDATA(data_type_t.DTP_INTEGER, "timeout_idack", sdata_flag_t.SDF_RD, "5000", "timeout waiting idAck"),
8534
8685
  SDATA(data_type_t.DTP_POINTER, "subscriber", 0, 0, "subscriber of output-events. If null then subscriber is the parent"),
@@ -8552,7 +8703,9 @@ var PRIVATE_DATA = {
8552
8703
  websocket: null,
8553
8704
  inside_on_open: false,
8554
8705
  retry_ms: 0,
8555
- retry_ms_before_session: 0
8706
+ retry_ms_before_session: 0,
8707
+ session_since: 0,
8708
+ retry_floor_warned: false
8556
8709
  };
8557
8710
  var __gclass__ = null;
8558
8711
  /******************************
@@ -8811,15 +8964,22 @@ function close_websocket(gobj, code = 1e3, reason = "") {
8811
8964
  * protocol error, throws there), so it is 4002 -- and the server
8812
8965
  * echoes it, so EV_ON_CLOSE carries it.
8813
8966
  *
8814
- * And a session the server broke was not a session reached: the
8815
- * backoff the ack reset is put back, or a server that breaks every
8967
+ * And a session the server broke at once was not a session reached:
8968
+ * the backoff the ack reset is put back, or a server that breaks every
8816
8969
  * session is reconnected at the first delay for ever, with an
8817
- * EV_ON_OPEN and an EV_ON_CLOSE in the app per round.
8970
+ * EV_ON_OPEN and an EV_ON_CLOSE in the app per round. A session that
8971
+ * outlived the longest delay of the backoff (`timeout_retry_max`) was
8972
+ * served: its fault starts the backoff over, as a clean end does, or
8973
+ * one foreign message hours later is waited for with a delay escalated
8974
+ * by failures long gone.
8818
8975
  ***************************************************************/
8819
8976
  var CLOSE_PEER_FAULT = 4002;
8820
8977
  function close_for_peer_fault(gobj, reason) {
8821
8978
  let priv = gobj.priv;
8822
- if (gobj_current_state(gobj) === "ST_SESSION") priv.retry_ms = priv.retry_ms_before_session;
8979
+ if (gobj_current_state(gobj) === "ST_SESSION") {
8980
+ if (performance.now() - priv.session_since < retry_bounds(gobj).max) priv.retry_ms = priv.retry_ms_before_session;
8981
+ priv.retry_ms_before_session = 0;
8982
+ }
8823
8983
  close_websocket(gobj, CLOSE_PEER_FAULT, reason);
8824
8984
  }
8825
8985
  /***************************************************************
@@ -9089,6 +9249,32 @@ function ac_on_close(gobj, event, kw, src) {
9089
9249
  return 0;
9090
9250
  }
9091
9251
  /***************************************************************
9252
+ * The first delay of the backoff and its cap.
9253
+ *
9254
+ * A first delay below RETRY_FLOOR_MS is raised to it, with a WARNING
9255
+ * once: `timeout_retry` 0 never left 0 (0 doubled is 0), so a server
9256
+ * that closed before the ack was reconnected in a loop with no wait,
9257
+ * hundreds of connections a second.
9258
+ ***************************************************************/
9259
+ var RETRY_FLOOR_MS = 1e3;
9260
+ function retry_bounds(gobj) {
9261
+ let priv = gobj.priv;
9262
+ let base = gobj_read_integer_attr(gobj, "timeout_retry");
9263
+ let max = gobj_read_integer_attr(gobj, "timeout_retry_max");
9264
+ if (!(base >= RETRY_FLOOR_MS)) {
9265
+ if (!priv.retry_floor_warned) {
9266
+ priv.retry_floor_warned = true;
9267
+ log_warning(`${gobj_short_name(gobj)}: timeout_retry ${base} ms is below ${RETRY_FLOOR_MS} ms: ${RETRY_FLOOR_MS} ms is used`);
9268
+ }
9269
+ base = RETRY_FLOOR_MS;
9270
+ }
9271
+ if (!(max >= base)) max = base;
9272
+ return {
9273
+ base,
9274
+ max
9275
+ };
9276
+ }
9277
+ /***************************************************************
9092
9278
  * How long to wait before the next reconnect attempt.
9093
9279
  *
9094
9280
  * The delay used to be a constant (`timeout_retry`, 5s), for ever. A
@@ -9109,9 +9295,7 @@ function ac_on_close(gobj, event, kw, src) {
9109
9295
  ***************************************************************/
9110
9296
  function next_retry(gobj) {
9111
9297
  let priv = gobj.priv;
9112
- let base = gobj_read_integer_attr(gobj, "timeout_retry");
9113
- let max = gobj_read_integer_attr(gobj, "timeout_retry_max");
9114
- if (max < base) max = base;
9298
+ const { base, max } = retry_bounds(gobj);
9115
9299
  priv.retry_ms = priv.retry_ms ? Math.min(priv.retry_ms * 2, max) : base;
9116
9300
  let jitter = priv.retry_ms * .2;
9117
9301
  let ms = priv.retry_ms - jitter + Math.random() * 2 * jitter;
@@ -9157,6 +9341,7 @@ function ac_identity_card_ack(gobj, event, kw, src) {
9157
9341
  priv.inside_on_open = true;
9158
9342
  priv.retry_ms_before_session = priv.retry_ms;
9159
9343
  priv.retry_ms = 0;
9344
+ priv.session_since = performance.now();
9160
9345
  if (!priv.inform_on_close) {
9161
9346
  priv.inform_on_close = true;
9162
9347
  gobj_publish_event(gobj, "EV_ON_OPEN", {
@@ -9254,16 +9439,30 @@ function ac_on_message(gobj, event, kw, src) {
9254
9439
  return 0;
9255
9440
  }
9256
9441
  /***************************************************************************
9442
+ * The server asking this client for stats or a command. The C client
9443
+ * serves them (gobj_stats() / gobj_command() of the service named, and
9444
+ * an answer); a browser client does not run what its server asks, so it
9445
+ * is refused: a WARNING naming what was asked, masked (a command line
9446
+ * can carry a secret), and nothing else -- the protocol allows the ask,
9447
+ * so the session goes on. Up to 7.26.25 it was dropped without a trace.
9448
+ ***************************************************************************/
9449
+ function refuse_asked_by_server(gobj, event, kw, key) {
9450
+ const asked = kw && typeof kw[key] === "string" ? kw[key] : "";
9451
+ const service = kw && typeof kw.service === "string" ? kw.service : "";
9452
+ log_warning(`${gobj_short_name(gobj)}: ${event} not served by this client, DROPPED: ${key.replace(/_/g, "")} '${peer_text_shown(asked, PEER_NAME_SHOWN)}', service '${peer_text_shown(service, PEER_NAME_SHOWN)}'`);
9453
+ return -1;
9454
+ }
9455
+ /***************************************************************************
9257
9456
  * remote asking for stats
9258
9457
  ***************************************************************************/
9259
9458
  function ac_mt_stats(gobj, event, kw, src) {
9260
- return 0;
9459
+ return refuse_asked_by_server(gobj, event, kw, "__stats__");
9261
9460
  }
9262
9461
  /***************************************************************************
9263
9462
  * remote asking for command
9264
9463
  ***************************************************************************/
9265
9464
  function ac_mt_command(gobj, event, kw, src) {
9266
- return 0;
9465
+ return refuse_asked_by_server(gobj, event, kw, "__command__");
9267
9466
  }
9268
9467
  /***************************
9269
9468
  * FSM
@@ -9594,6 +9793,7 @@ exports.json_object_update = json_object_update;
9594
9793
  exports.json_object_update_existing = json_object_update_existing;
9595
9794
  exports.json_object_update_missing = json_object_update_missing;
9596
9795
  exports.json_size = json_size;
9796
+ exports.json_text_of = json_text_of;
9597
9797
  exports.jwt2json = jwt2json;
9598
9798
  exports.jwtDecode = jwtDecode;
9599
9799
  exports.kw_clone_by_keys = kw_clone_by_keys;