@yuneta/gobj-js 7.26.26 → 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.
package/README.md CHANGED
@@ -308,10 +308,12 @@ string, a `DTP_DICT` or a `DTP_LIST` (7.26.25): a cycle anywhere inside, a
308
308
  of a class. They were written, and burst (or turned into `"{}"`) when the value
309
309
  was serialized, far from the write. And so is a value whose text would say
310
310
  another value (7.26.26): an object with a `toJSON()`, `NaN` or `Infinity` inside
311
- (the text says `null`; jansson holds no such real), a function, `undefined` or a
312
- symbol inside an array (the text says `null`), and a value nested deeper than
313
- 2048 levels (a scalar is a level too), which C would not read back and which
314
- bursts `JSON.stringify()` a little further on. A getter or a `Proxy` trap that
311
+ (the text says `null`; jansson holds no such real), a function, `undefined`, a
312
+ symbol or a hole inside an array (`[1, , 3]`: the text says `null`), and a value
313
+ nested deeper than 2048 levels (a scalar is a level too), which C would not read
314
+ back and which bursts `JSON.stringify()` a little further on. Every version
315
+ before 7.26.26 wrote them, so this is a BREAKING change for a caller that wrote
316
+ one. A getter or a `Proxy` trap that
315
317
  throws is a refusal too, never an exception (7.26.26). A key whose value is
316
318
  `undefined`, a function or a symbol is dropped, as `JSON.stringify()` drops it:
317
319
  an absent key. A plain object of another realm (a `vm` context, an iframe) is a
@@ -324,6 +326,7 @@ gobj_write_attr(gobj, "settings", new Map([["a", 1]])); // -1, ERROR "a valu
324
326
  gobj_write_str_attr(gobj, "name", new Map()); // -1: it was the text "{}"
325
327
  gobj_write_attr(gobj, "settings", {ratio: NaN}); // -1: it was the text {"ratio":null}
326
328
  gobj_write_attr(gobj, "items", [1, undefined]); // -1: it was the text [1,null]
329
+ gobj_write_attr(gobj, "items", [1, , 3]); // -1: a hole, it was the text [1,null,3]
327
330
  gobj_write_str_attr(gobj, "name", NaN); // -1: it was the text "null"
328
331
  ```
329
332
 
@@ -723,8 +726,20 @@ kw_clone_by_not_keys(gobj, kw, keys, verbose)
723
726
 
724
727
  // Local storage helpers (browser localStorage, no "store" parameter)
725
728
  kw_get_local_storage_value(key, default_value, create) // create=false
726
- kw_set_local_storage_value(key, value)
729
+ kw_set_local_storage_value(key, value) // 0, or -1 logged
727
730
  kw_remove_local_storage_value(key)
731
+ // BREAKING in 7.26.26: kw_get_local_storage_value() answers default_value
732
+ // where it answered null (an absent entry, a json null) or undefined (no
733
+ // browser), and an entry that is not json is REMOVED, with one WARNING
734
+ // that never shows its text. A store the browser blocks (cookies blocked,
735
+ // a sandboxed iframe: reading window.localStorage throws) answers the
736
+ // default, warned once per key, and a write to it is -1 (7.26.27: before,
737
+ // every helper and node_uuid() threw). A value with no json text is
738
+ // refused by the writer, -1 with an ERROR, as a typed writer refuses it
739
+ // (7.26.27; a function was stored as the text "undefined"):
740
+ // kw_set_local_storage_value("k", () => 1) → -1
741
+ // kw_set_local_storage_value("k", {w: NaN}) → -1 (it was {"w":null})
742
+ // kw_set_local_storage_value("k", [1, , 3]) → -1 (a hole: [1,null,3])
728
743
 
729
744
  // Misc utilities
730
745
  current_timestamp() // ISO string
@@ -1091,7 +1106,10 @@ count as reached: the backoff goes on from where it was (7.26.25), so a server
1091
1106
  that breaks every session is not reconnected at the first delay for ever -- unless
1092
1107
  the session outlived `timeout_retry_max`: a session that long was served, and its
1093
1108
  fault starts the backoff over, as a clean end does (7.26.26). For the old fixed-interval behaviour set `timeout_retry_max` equal to
1094
- `timeout_retry`.
1109
+ `timeout_retry`. The first delay is **at least 1000 ms** (7.26.27): a lower
1110
+ `timeout_retry` is raised to 1000 with one WARNING, because 0 never doubled
1111
+ away from 0 and a server that closes before the ack was reconnected in a loop
1112
+ with no wait, hundreds of times a second.
1095
1113
 
1096
1114
  **A refused identity card** (`result < 0` in its ack) publishes `EV_ON_ID_NAK`
1097
1115
  and closes the channel with 1000; no session was reached, so the close is then
@@ -2392,11 +2392,13 @@ function rc_walk_by_list(iter_, walk_type, cb_walking, user_data, user_data2) {
2392
2392
  let child;
2393
2393
  if (walk_type & walk_type_t.WALK_LAST2FIRST) for (let i = iter.length; i > 0; i--) {
2394
2394
  child = iter[i - 1];
2395
+ if (gobj_is_destroying(child)) continue;
2395
2396
  let ret = walk_ret(cb_walking(child, user_data, user_data2));
2396
2397
  if (ret < 0) return ret;
2397
2398
  }
2398
2399
  else for (let i = 0; i < iter.length; i++) {
2399
2400
  child = iter[i];
2401
+ if (gobj_is_destroying(child)) continue;
2400
2402
  let ret = walk_ret(cb_walking(child, user_data, user_data2));
2401
2403
  if (ret < 0) return ret;
2402
2404
  }
@@ -2411,18 +2413,21 @@ function rc_walk_by_list(iter_, walk_type, cb_walking, user_data, user_data2) {
2411
2413
  * or
2412
2414
  * WALK_LAST2FIRST
2413
2415
  ***************************************************************/
2414
- 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_];
2415
2418
  let ret = rc_walk_by_list(iter, walk_type, cb_walking, user_data, user_data2);
2416
2419
  if (ret < 0) return ret;
2417
2420
  let child;
2418
2421
  if (walk_type & walk_type_t.WALK_LAST2FIRST) for (let i = iter.length; i > 0; i--) {
2419
2422
  child = iter[i - 1];
2420
- 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);
2421
2425
  if (ret < 0) return ret;
2422
2426
  }
2423
2427
  else for (let i = 0; i < iter.length; i++) {
2424
2428
  child = iter[i];
2425
- 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);
2426
2431
  if (ret < 0) return ret;
2427
2432
  }
2428
2433
  return ret;
@@ -2442,23 +2447,27 @@ function rc_walk_by_level(iter, walk_type, cb_walking, user_data, user_data2) {
2442
2447
  * or
2443
2448
  * WALK_LAST2FIRST
2444
2449
  ***************************************************************/
2445
- function rc_walk_by_tree(iter, walk_type, cb_walking, user_data, user_data2) {
2446
- 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_];
2447
2453
  let ret;
2448
2454
  let child;
2449
2455
  if (walk_type & walk_type_t.WALK_BOTTOM2TOP) for (let i = iter.length; i > 0; i--) {
2450
2456
  child = iter[i - 1];
2451
- 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);
2452
2459
  if (ret < 0) return ret;
2460
+ if (gobj_is_destroying(child)) continue;
2453
2461
  ret = walk_ret(cb_walking(child, user_data, user_data2));
2454
2462
  if (ret < 0) return ret;
2455
2463
  }
2456
2464
  else for (let i = 0; i < iter.length; i++) {
2457
2465
  child = iter[i];
2466
+ if (gobj_is_destroying(child)) continue;
2458
2467
  ret = walk_ret(cb_walking(child, user_data, user_data2));
2459
2468
  if (ret < 0) return ret;
2460
- else if (ret === 0) {
2461
- 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);
2462
2471
  if (ret < 0) return ret;
2463
2472
  }
2464
2473
  }
@@ -6043,40 +6052,83 @@ function kw_clone_by_not_keys(gobj, kw, keys, verbose) {
6043
6052
  return kw_clone;
6044
6053
  }
6045
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
+ /********************************************
6046
6093
  * Get a local attribute
6047
6094
  *
6048
6095
  * The json the entry holds, or `default_value` when there is none
6049
6096
  * (written there too with `create`). A json null is no value, as
6050
6097
  * json_absent() in C: the default. An entry that is not json (a
6051
- * corrupt or foreign write) is one WARNING and is REMOVED, so the
6098
+ * corrupt or foreign write) is a WARNING and is REMOVED, so the
6052
6099
  * next read finds no entry: it used to console.warn on every read,
6053
- * for ever. Its text is never shown (JSON.parse() quotes it in its
6054
- * error, and an entry can hold a token). A store that cannot be read
6055
- * is warned once per key. Outside a browser: the default.
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.
6056
6105
  ********************************************/
6057
- var local_storage_warned = /* @__PURE__ */ new Set();
6058
- function warn_local_storage_once(key, what) {
6059
- if (local_storage_warned.has(key)) return;
6060
- local_storage_warned.add(key);
6061
- log_warning(`kw_get_local_storage_value(): localStorage key "${key}" ${what}`);
6062
- }
6063
6106
  function kw_get_local_storage_value(key, default_value, create = false) {
6064
- if (typeof window === "undefined" || !window.localStorage) return default_value;
6107
+ const store = local_store();
6108
+ if (!store) return default_value;
6109
+ const where = `kw_get_local_storage_value(): localStorage key "${key}"`;
6065
6110
  let text;
6066
6111
  try {
6067
- text = window.localStorage.getItem(key);
6112
+ text = store.getItem(key);
6068
6113
  } catch (e) {
6069
- warn_local_storage_once(key, "cannot be read (storage blocked?): the default is used");
6114
+ warn_local_storage_once(key, `${where} cannot be read (storage blocked?): the default is used`);
6070
6115
  return default_value;
6071
6116
  }
6072
6117
  let value = null;
6073
6118
  if (text !== null) try {
6074
6119
  value = JSON.parse(text);
6075
6120
  } catch (e) {
6076
- warn_local_storage_once(key, "does not hold json: removed, the default is used");
6077
- kw_remove_local_storage_value(key);
6078
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;
6079
6130
  }
6131
+ if (text !== void 0) local_storage_warned.delete(key);
6080
6132
  if (value === null) {
6081
6133
  if (create) kw_set_local_storage_value(key, default_value);
6082
6134
  return default_value;
@@ -6092,15 +6144,26 @@ function kw_get_local_storage_value(key, default_value, create = false) {
6092
6144
  * reported success on a store that had rejected the write: a full or blocked
6093
6145
  * localStorage (quota, private mode) silently discarded the change while the
6094
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.
6095
6152
  ********************************************/
6096
6153
  function kw_set_local_storage_value(key, value) {
6097
- if (typeof window === "undefined" || !window.localStorage) return 0;
6098
- if (!key || value === void 0) {
6099
- 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}`);
6100
6156
  return -1;
6101
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;
6102
6165
  try {
6103
- window.localStorage.setItem(key, JSON.stringify(value));
6166
+ store.setItem(key, text);
6104
6167
  return 0;
6105
6168
  } catch (e) {
6106
6169
  log_error(`kw_set_local_storage_value(): cannot save "${key}" (storage full or blocked?): ${e && e.message ? e.message : e}`);
@@ -6111,15 +6174,16 @@ function kw_set_local_storage_value(key, value) {
6111
6174
  * Remove local attribute
6112
6175
  ********************************************/
6113
6176
  function kw_remove_local_storage_value(key) {
6114
- if (typeof window === "undefined" || !window.localStorage) return;
6177
+ const store = local_store();
6178
+ if (!store) return;
6115
6179
  if (!key) {
6116
- window.console.warn(`Invalid key for localStorage removal: ${key}`);
6180
+ log_error(`kw_remove_local_storage_value(): invalid key: key=${key}`);
6117
6181
  return;
6118
6182
  }
6119
6183
  try {
6120
- window.localStorage.removeItem(key);
6184
+ store.removeItem(key);
6121
6185
  } catch (e) {
6122
- 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}`);
6123
6187
  }
6124
6188
  }
6125
6189
  /*************************************************************
@@ -7312,11 +7376,21 @@ function index_in_list(list, elm) {
7312
7376
  ************************************************************/
7313
7377
  function node_uuid() {
7314
7378
  let uuid = "";
7315
- if (typeof window !== "undefined" && window.localStorage) {
7316
- 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
+ }
7317
7387
  if (!uuid && window.crypto?.randomUUID) {
7318
7388
  uuid = window.crypto.randomUUID();
7319
- 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
+ }
7320
7394
  }
7321
7395
  } else if (typeof globalThis !== "undefined" && globalThis.crypto?.randomUUID) uuid = globalThis.crypto.randomUUID();
7322
7396
  else uuid = "00000000-0000-0000-0000-000000000000";
@@ -8605,7 +8679,7 @@ var attrs_table = [
8605
8679
  SDATA(data_type_t.DTP_STRING, "cert_pem", sdata_flag_t.SDF_PERSIST, "", "SSL server certification, PEM str format"),
8606
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."),
8607
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."),
8608
- 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"),
8609
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"),
8610
8684
  SDATA(data_type_t.DTP_INTEGER, "timeout_idack", sdata_flag_t.SDF_RD, "5000", "timeout waiting idAck"),
8611
8685
  SDATA(data_type_t.DTP_POINTER, "subscriber", 0, 0, "subscriber of output-events. If null then subscriber is the parent"),
@@ -8630,7 +8704,8 @@ var PRIVATE_DATA = {
8630
8704
  inside_on_open: false,
8631
8705
  retry_ms: 0,
8632
8706
  retry_ms_before_session: 0,
8633
- session_since: 0
8707
+ session_since: 0,
8708
+ retry_floor_warned: false
8634
8709
  };
8635
8710
  var __gclass__ = null;
8636
8711
  /******************************
@@ -8902,7 +8977,7 @@ var CLOSE_PEER_FAULT = 4002;
8902
8977
  function close_for_peer_fault(gobj, reason) {
8903
8978
  let priv = gobj.priv;
8904
8979
  if (gobj_current_state(gobj) === "ST_SESSION") {
8905
- if (performance.now() - priv.session_since < Math.max(gobj_read_integer_attr(gobj, "timeout_retry"), gobj_read_integer_attr(gobj, "timeout_retry_max"))) priv.retry_ms = priv.retry_ms_before_session;
8980
+ if (performance.now() - priv.session_since < retry_bounds(gobj).max) priv.retry_ms = priv.retry_ms_before_session;
8906
8981
  priv.retry_ms_before_session = 0;
8907
8982
  }
8908
8983
  close_websocket(gobj, CLOSE_PEER_FAULT, reason);
@@ -9174,6 +9249,32 @@ function ac_on_close(gobj, event, kw, src) {
9174
9249
  return 0;
9175
9250
  }
9176
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
+ /***************************************************************
9177
9278
  * How long to wait before the next reconnect attempt.
9178
9279
  *
9179
9280
  * The delay used to be a constant (`timeout_retry`, 5s), for ever. A
@@ -9194,9 +9295,7 @@ function ac_on_close(gobj, event, kw, src) {
9194
9295
  ***************************************************************/
9195
9296
  function next_retry(gobj) {
9196
9297
  let priv = gobj.priv;
9197
- let base = gobj_read_integer_attr(gobj, "timeout_retry");
9198
- let max = gobj_read_integer_attr(gobj, "timeout_retry_max");
9199
- if (max < base) max = base;
9298
+ const { base, max } = retry_bounds(gobj);
9200
9299
  priv.retry_ms = priv.retry_ms ? Math.min(priv.retry_ms * 2, max) : base;
9201
9300
  let jitter = priv.retry_ms * .2;
9202
9301
  let ms = priv.retry_ms - jitter + Math.random() * 2 * jitter;
@@ -9694,6 +9793,7 @@ exports.json_object_update = json_object_update;
9694
9793
  exports.json_object_update_existing = json_object_update_existing;
9695
9794
  exports.json_object_update_missing = json_object_update_missing;
9696
9795
  exports.json_size = json_size;
9796
+ exports.json_text_of = json_text_of;
9697
9797
  exports.jwt2json = jwt2json;
9698
9798
  exports.jwtDecode = jwtDecode;
9699
9799
  exports.kw_clone_by_keys = kw_clone_by_keys;