@yuneta/gobj-js 7.26.26 → 7.26.28

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
 
@@ -457,7 +460,10 @@ gobj_create_volatil(name, gclass_name, attrs, parent)
457
460
  gobj_create_pure_child(name, gclass_name, attrs, parent)
458
461
 
459
462
  // Start / Stop
460
- gobj_start(gobj) // calls mt_start
463
+ gobj_start(gobj) // calls mt_start; -1 with an ERROR if a
464
+ // SDF_REQUIRED attr is empty, as in C
465
+ // (BREAKING in 7.26.28: "" string, empty_json()
466
+ // list/dict/json, null pointer)
461
467
  gobj_stop(gobj) // calls mt_stop
462
468
  gobj_start_children(gobj)
463
469
  gobj_stop_children(gobj)
@@ -596,6 +602,9 @@ gobj_search_path(gobj, path)
596
602
 
597
603
  gobj_walk_gobj_children(gobj, walk_type, cb_walking, user_data, user_data2)
598
604
  gobj_walk_gobj_children_tree(gobj, walk_type, cb_walking, user_data, user_data2)
605
+ // A callback may destroy or move (gobj_change_parent()) any gobj: a
606
+ // destroyed one is never handed, a moved one is handed where the walk
607
+ // finds it now, and no gobj is handed twice in one walk (7.26.28).
599
608
  ```
600
609
 
601
610
  ### Persistence
@@ -723,13 +732,30 @@ kw_clone_by_not_keys(gobj, kw, keys, verbose)
723
732
 
724
733
  // Local storage helpers (browser localStorage, no "store" parameter)
725
734
  kw_get_local_storage_value(key, default_value, create) // create=false
726
- kw_set_local_storage_value(key, value)
735
+ kw_set_local_storage_value(key, value) // 0, or -1 logged
727
736
  kw_remove_local_storage_value(key)
737
+ // BREAKING in 7.26.26: kw_get_local_storage_value() answers default_value
738
+ // where it answered null (an absent entry, a json null) or undefined (no
739
+ // browser), and an entry that is not json is REMOVED, with one WARNING
740
+ // that never shows its text. A store the browser blocks (cookies blocked,
741
+ // a sandboxed iframe: reading window.localStorage throws) answers the
742
+ // default, warned once per key, and a write to it is -1 (7.26.27: before,
743
+ // every helper and node_uuid() threw). A value with no json text is
744
+ // refused by the writer, -1 with an ERROR, as a typed writer refuses it
745
+ // (7.26.27; a function was stored as the text "undefined"):
746
+ // kw_set_local_storage_value("k", () => 1) → -1
747
+ // kw_set_local_storage_value("k", {w: NaN}) → -1 (it was {"w":null})
748
+ // kw_set_local_storage_value("k", [1, , 3]) → -1 (a hole: [1,null,3])
749
+ // A full store (read, not written) with `create`: the default, warned
750
+ // once per key (7.26.28: an ERROR on every read).
728
751
 
729
752
  // Misc utilities
730
753
  current_timestamp() // ISO string
731
754
  get_now() // Date object
732
- node_uuid() // generate UUID
755
+ node_uuid() // uuid of this browser, the SAME for the
756
+ // life of the page (kept in localStorage
757
+ // "host_uuid"; a page uuid if the store
758
+ // cannot keep it; never null: 7.26.28)
733
759
  parseBoolean(v) // coerce to boolean
734
760
  empty_string(s) // true if null/undefined/""
735
761
  str_in_list(list, str, ci) // case-insensitive optional
@@ -1091,7 +1117,11 @@ count as reached: the backoff goes on from where it was (7.26.25), so a server
1091
1117
  that breaks every session is not reconnected at the first delay for ever -- unless
1092
1118
  the session outlived `timeout_retry_max`: a session that long was served, and its
1093
1119
  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`.
1120
+ `timeout_retry`. The BASE of the first delay is **at least 1000 ms** (7.26.27;
1121
+ the ±20% jitter puts the first delay itself between 800 and 1200 ms): a lower
1122
+ `timeout_retry` is raised to 1000 with one WARNING, because 0 never doubled
1123
+ away from 0 and a server that closes before the ack was reconnected in a loop
1124
+ with no wait, hundreds of times a second.
1095
1125
 
1096
1126
  **A refused identity card** (`result < 0` in its ack) publishes `EV_ON_ID_NAK`
1097
1127
  and closes the channel with 1000; no session was reached, so the close is then
@@ -1949,6 +1949,31 @@ function gobj_destroy_children(gobj) {
1949
1949
  }
1950
1950
  }
1951
1951
  /************************************************************
1952
+ * The SDF_REQUIRED attributes with no value, or null when there
1953
+ * is none: what is empty is asked type by type, as in C.
1954
+ ************************************************************/
1955
+ function gobj_check_required_attrs(gobj) {
1956
+ const hs = gobj_hsdata(gobj);
1957
+ const missing = [];
1958
+ if (!is_array(gobj.gclass.attrs_table)) return null;
1959
+ for (const it of gobj.gclass.attrs_table) {
1960
+ if (!it.name || !(it.flag & sdata_flag_t.SDF_REQUIRED)) continue;
1961
+ const value = hs[it.name];
1962
+ switch (it.type) {
1963
+ case data_type_t.DTP_STRING:
1964
+ if (!is_string(value) || empty_string(value)) missing.push(it.name);
1965
+ break;
1966
+ case data_type_t.DTP_LIST:
1967
+ case data_type_t.DTP_DICT:
1968
+ case data_type_t.DTP_JSON:
1969
+ if (empty_json(value)) missing.push(it.name);
1970
+ break;
1971
+ case data_type_t.DTP_POINTER: if (!value) missing.push(it.name);
1972
+ }
1973
+ }
1974
+ return missing.length ? missing : null;
1975
+ }
1976
+ /************************************************************
1952
1977
  *
1953
1978
  ************************************************************/
1954
1979
  function gobj_start(gobj) {
@@ -1964,6 +1989,11 @@ function gobj_start(gobj) {
1964
1989
  log_error(`${gobj_short_name(gobj)}: GObj DISABLED`);
1965
1990
  return -1;
1966
1991
  }
1992
+ const required_attrs = gobj_check_required_attrs(gobj);
1993
+ if (required_attrs) {
1994
+ log_error(`${gobj_short_name(gobj)}: Cannot start without all required attributes: ` + JSON.stringify(required_attrs));
1995
+ return -1;
1996
+ }
1967
1997
  if (__trace_gobj_start_stop__(gobj)) trace_machine(sprintf("⏺ ⏺ start: %s", gobj_full_name(gobj)));
1968
1998
  gobj.running = true;
1969
1999
  let ret = 0;
@@ -2350,7 +2380,7 @@ function gobj_walk_gobj_children(gobj, walk_type, cb_walking, user_data, user_da
2350
2380
  log_error(`gobj NULL, destroying, or bad type`);
2351
2381
  return 0;
2352
2382
  }
2353
- return rc_walk_by_list(gobj.dl_children, walk_type, cb_walking, user_data, user_data2);
2383
+ return rc_walk_by_list(gobj, /* @__PURE__ */ new Set(), walk_type, cb_walking, user_data, user_data2);
2354
2384
  }
2355
2385
  /***************************************************************************
2356
2386
  *
@@ -2360,7 +2390,7 @@ function gobj_walk_gobj_children_tree(gobj, walk_type, cb_walking, user_data, us
2360
2390
  log_error(`gobj NULL, destroying, or bad type`);
2361
2391
  return 0;
2362
2392
  }
2363
- return rc_walk_by_tree(gobj.dl_children, walk_type, cb_walking, user_data, user_data2);
2393
+ return rc_walk_by_tree(gobj, /* @__PURE__ */ new Set(), walk_type, cb_walking, user_data, user_data2);
2364
2394
  }
2365
2395
  /***************************************************************
2366
2396
  * Walking
@@ -2387,16 +2417,33 @@ function walk_ret(ret) {
2387
2417
  log_error(`walk callback must return a NUMBER (-1 stop, 0 descend, >0 skip children), got ${typeof ret}: taken as 0`);
2388
2418
  return 0;
2389
2419
  }
2390
- function rc_walk_by_list(iter_, walk_type, cb_walking, user_data, user_data2) {
2391
- let iter = [...iter_];
2420
+ /***************************************************************
2421
+ * Whether a walk steps over this gobj of the list of `owner`.
2422
+ *
2423
+ * Each list is walked over a COPY of it, because a callback may destroy
2424
+ * any gobj of the list, the current one included, or move one with
2425
+ * gobj_change_parent(). One it destroyed is stepped over, as the C walk
2426
+ * does; one it moved is no longer in this list, and is handed where the
2427
+ * walk finds it now (7.26.27 handed it here AND there); and a gobj is
2428
+ * never handed twice in one walk (`seen`), wherever it was moved.
2429
+ ***************************************************************/
2430
+ function walk_steps_over(child, owner, seen) {
2431
+ return gobj_is_destroying(child) || child.parent !== owner || seen.has(child);
2432
+ }
2433
+ function rc_walk_by_list(owner, seen, walk_type, cb_walking, user_data, user_data2) {
2434
+ let iter = [...owner.dl_children];
2392
2435
  let child;
2393
2436
  if (walk_type & walk_type_t.WALK_LAST2FIRST) for (let i = iter.length; i > 0; i--) {
2394
2437
  child = iter[i - 1];
2438
+ if (walk_steps_over(child, owner, seen)) continue;
2439
+ seen.add(child);
2395
2440
  let ret = walk_ret(cb_walking(child, user_data, user_data2));
2396
2441
  if (ret < 0) return ret;
2397
2442
  }
2398
2443
  else for (let i = 0; i < iter.length; i++) {
2399
2444
  child = iter[i];
2445
+ if (walk_steps_over(child, owner, seen)) continue;
2446
+ seen.add(child);
2400
2447
  let ret = walk_ret(cb_walking(child, user_data, user_data2));
2401
2448
  if (ret < 0) return ret;
2402
2449
  }
@@ -2411,21 +2458,23 @@ function rc_walk_by_list(iter_, walk_type, cb_walking, user_data, user_data2) {
2411
2458
  * or
2412
2459
  * WALK_LAST2FIRST
2413
2460
  ***************************************************************/
2414
- function rc_walk_by_level(iter, walk_type, cb_walking, user_data, user_data2) {
2415
- let ret = rc_walk_by_list(iter, walk_type, cb_walking, user_data, user_data2);
2416
- if (ret < 0) return ret;
2417
- let child;
2418
- if (walk_type & walk_type_t.WALK_LAST2FIRST) for (let i = iter.length; i > 0; i--) {
2419
- child = iter[i - 1];
2420
- ret = rc_walk_by_level([...child.dl_children], walk_type, cb_walking, user_data, user_data2);
2461
+ function rc_walk_by_level(owner, seen, walk_type, cb_walking, user_data, user_data2) {
2462
+ let iter = [...owner.dl_children];
2463
+ if (walk_type & walk_type_t.WALK_LAST2FIRST) iter.reverse();
2464
+ let handed = [];
2465
+ for (const child of iter) {
2466
+ if (walk_steps_over(child, owner, seen)) continue;
2467
+ seen.add(child);
2468
+ handed.push(child);
2469
+ let ret = walk_ret(cb_walking(child, user_data, user_data2));
2421
2470
  if (ret < 0) return ret;
2422
2471
  }
2423
- else for (let i = 0; i < iter.length; i++) {
2424
- child = iter[i];
2425
- ret = rc_walk_by_level([...child.dl_children], walk_type, cb_walking, user_data, user_data2);
2472
+ for (const child of handed) {
2473
+ if (gobj_is_destroying(child)) continue;
2474
+ let ret = rc_walk_by_level(child, seen, walk_type, cb_walking, user_data, user_data2);
2426
2475
  if (ret < 0) return ret;
2427
2476
  }
2428
- return ret;
2477
+ return 0;
2429
2478
  }
2430
2479
  /***************************************************************
2431
2480
  * Walking
@@ -2442,23 +2491,29 @@ function rc_walk_by_level(iter, walk_type, cb_walking, user_data, user_data2) {
2442
2491
  * or
2443
2492
  * WALK_LAST2FIRST
2444
2493
  ***************************************************************/
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);
2494
+ function rc_walk_by_tree(owner, seen, walk_type, cb_walking, user_data, user_data2) {
2495
+ if (walk_type & walk_type_t.WALK_BYLEVEL) return rc_walk_by_level(owner, seen, walk_type, cb_walking, user_data, user_data2);
2496
+ let iter = [...owner.dl_children];
2447
2497
  let ret;
2448
2498
  let child;
2449
2499
  if (walk_type & walk_type_t.WALK_BOTTOM2TOP) for (let i = iter.length; i > 0; i--) {
2450
2500
  child = iter[i - 1];
2451
- ret = rc_walk_by_tree([...child.dl_children], walk_type, cb_walking, user_data, user_data2);
2501
+ if (walk_steps_over(child, owner, seen)) continue;
2502
+ ret = rc_walk_by_tree(child, seen, walk_type, cb_walking, user_data, user_data2);
2452
2503
  if (ret < 0) return ret;
2504
+ if (gobj_is_destroying(child)) continue;
2505
+ seen.add(child);
2453
2506
  ret = walk_ret(cb_walking(child, user_data, user_data2));
2454
2507
  if (ret < 0) return ret;
2455
2508
  }
2456
2509
  else for (let i = 0; i < iter.length; i++) {
2457
2510
  child = iter[i];
2511
+ if (walk_steps_over(child, owner, seen)) continue;
2512
+ seen.add(child);
2458
2513
  ret = walk_ret(cb_walking(child, user_data, user_data2));
2459
2514
  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);
2515
+ else if (ret === 0 && !gobj_is_destroying(child)) {
2516
+ ret = rc_walk_by_tree(child, seen, walk_type, cb_walking, user_data, user_data2);
2462
2517
  if (ret < 0) return ret;
2463
2518
  }
2464
2519
  }
@@ -6043,44 +6098,99 @@ function kw_clone_by_not_keys(gobj, kw, keys, verbose) {
6043
6098
  return kw_clone;
6044
6099
  }
6045
6100
  /********************************************
6101
+ * The browser's localStorage, or null when there is none (outside a
6102
+ * browser).
6103
+ *
6104
+ * With the site's data blocked (Chrome with cookies blocked, a sandboxed
6105
+ * iframe), READING the window.localStorage property already throws a
6106
+ * SecurityError, not only its getItem(): every app reads its locale at
6107
+ * start, so the app did not start. Such a store is answered as one whose
6108
+ * every call throws that error, so each helper fails as it fails for any
6109
+ * store that refuses it: a read gives the default, a write -1.
6110
+ ********************************************/
6111
+ function local_store() {
6112
+ if (typeof window === "undefined") return null;
6113
+ let store;
6114
+ try {
6115
+ store = window.localStorage;
6116
+ } catch (e) {
6117
+ const refuse = () => {
6118
+ throw e;
6119
+ };
6120
+ return {
6121
+ getItem: refuse,
6122
+ setItem: refuse,
6123
+ removeItem: refuse
6124
+ };
6125
+ }
6126
+ return store || null;
6127
+ }
6128
+ /********************************************
6129
+ * A failure of the store that would repeat on every read is warned
6130
+ * once per key, until a read of the key goes well again.
6131
+ ********************************************/
6132
+ var local_storage_warned = /* @__PURE__ */ new Set();
6133
+ function warn_local_storage_once(key, msg) {
6134
+ if (local_storage_warned.has(key)) return;
6135
+ local_storage_warned.add(key);
6136
+ log_warning(msg);
6137
+ }
6138
+ /********************************************
6046
6139
  * Get a local attribute
6047
6140
  *
6048
6141
  * The json the entry holds, or `default_value` when there is none
6049
6142
  * (written there too with `create`). A json null is no value, as
6050
6143
  * 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
6144
+ * corrupt or foreign write) is a WARNING and is REMOVED, so the
6052
6145
  * 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.
6146
+ * for ever; a new corrupt entry is a new WARNING. Its text is never
6147
+ * shown (JSON.parse() quotes it in its error, and an entry can hold a
6148
+ * token). A store that cannot be read (blocked) is warned once per
6149
+ * key, and the default is NOT written there; one that cannot be
6150
+ * written (full) is warned once per key too: with `create` it was an
6151
+ * ERROR on every read, the default tried again each time. A key is
6152
+ * warned again after a read that goes well. Outside a browser: the
6153
+ * default.
6056
6154
  ********************************************/
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
6155
  function kw_get_local_storage_value(key, default_value, create = false) {
6064
- if (typeof window === "undefined" || !window.localStorage) return default_value;
6156
+ const store = local_store();
6157
+ if (!store) return default_value;
6158
+ const where = `kw_get_local_storage_value(): localStorage key "${key}"`;
6065
6159
  let text;
6066
6160
  try {
6067
- text = window.localStorage.getItem(key);
6161
+ text = store.getItem(key);
6068
6162
  } catch (e) {
6069
- warn_local_storage_once(key, "cannot be read (storage blocked?): the default is used");
6163
+ warn_local_storage_once(key, `${where} cannot be read (storage blocked?): the default is used`);
6070
6164
  return default_value;
6071
6165
  }
6166
+ let went_well = true;
6072
6167
  let value = null;
6073
6168
  if (text !== null) try {
6074
6169
  value = JSON.parse(text);
6075
6170
  } 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
6171
  value = null;
6172
+ try {
6173
+ store.removeItem(key);
6174
+ log_warning(`${where} does not hold json: removed, the default is used`);
6175
+ } catch (e2) {
6176
+ went_well = false;
6177
+ warn_local_storage_once(key, `${where} does not hold json and cannot be removed: the default is used`);
6178
+ }
6079
6179
  }
6080
6180
  if (value === null) {
6081
- if (create) kw_set_local_storage_value(key, default_value);
6082
- return default_value;
6181
+ value = default_value;
6182
+ if (create) {
6183
+ const default_text = json_text_of(default_value);
6184
+ if (default_text === void 0) log_error(`${where}: cannot write the default: a value with no json text`);
6185
+ else try {
6186
+ store.setItem(key, default_text);
6187
+ } catch (e) {
6188
+ went_well = false;
6189
+ warn_local_storage_once(key, `${where} cannot be written (storage full or blocked?): the default is used, not kept`);
6190
+ }
6191
+ }
6083
6192
  }
6193
+ if (went_well) local_storage_warned.delete(key);
6084
6194
  return value;
6085
6195
  }
6086
6196
  /********************************************
@@ -6092,15 +6202,26 @@ function kw_get_local_storage_value(key, default_value, create = false) {
6092
6202
  * reported success on a store that had rejected the write: a full or blocked
6093
6203
  * localStorage (quota, private mode) silently discarded the change while the
6094
6204
  * UI showed it as saved.
6205
+ *
6206
+ * A value with no json text is refused with an ERROR, as a typed writer
6207
+ * refuses it (json_text_of()): a function was stored as the text
6208
+ * "undefined", and a NaN, a Date or a Map as a text that reads back as
6209
+ * another value. Checked in Node.js too, where there is no store.
6095
6210
  ********************************************/
6096
6211
  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}`);
6212
+ if (!key) {
6213
+ log_error(`kw_set_local_storage_value(): invalid key: key=${key}`);
6214
+ return -1;
6215
+ }
6216
+ const text = json_text_of(value);
6217
+ if (text === void 0) {
6218
+ log_error(`kw_set_local_storage_value(): cannot save "${key}": a value with no json text`);
6100
6219
  return -1;
6101
6220
  }
6221
+ const store = local_store();
6222
+ if (!store) return 0;
6102
6223
  try {
6103
- window.localStorage.setItem(key, JSON.stringify(value));
6224
+ store.setItem(key, text);
6104
6225
  return 0;
6105
6226
  } catch (e) {
6106
6227
  log_error(`kw_set_local_storage_value(): cannot save "${key}" (storage full or blocked?): ${e && e.message ? e.message : e}`);
@@ -6111,15 +6232,16 @@ function kw_set_local_storage_value(key, value) {
6111
6232
  * Remove local attribute
6112
6233
  ********************************************/
6113
6234
  function kw_remove_local_storage_value(key) {
6114
- if (typeof window === "undefined" || !window.localStorage) return;
6235
+ const store = local_store();
6236
+ if (!store) return;
6115
6237
  if (!key) {
6116
- window.console.warn(`Invalid key for localStorage removal: ${key}`);
6238
+ log_error(`kw_remove_local_storage_value(): invalid key: key=${key}`);
6117
6239
  return;
6118
6240
  }
6119
6241
  try {
6120
- window.localStorage.removeItem(key);
6242
+ store.removeItem(key);
6121
6243
  } catch (e) {
6122
- window.console.warn(`Error removing localStorage key "${key}":`, e);
6244
+ log_warning(`kw_remove_local_storage_value(): cannot remove "${key}" (storage blocked?): ${e && e.message ? e.message : e}`);
6123
6245
  }
6124
6246
  }
6125
6247
  /*************************************************************
@@ -7308,19 +7430,62 @@ function index_in_list(list, elm) {
7308
7430
  return -1;
7309
7431
  }
7310
7432
  /************************************************************
7433
+ * A new uuid v4 (RFC 9562).
7311
7434
  *
7435
+ * crypto.randomUUID() exists only in a secure context, so a page
7436
+ * served over http without TLS has none; crypto.getRandomValues()
7437
+ * exists there too. With no crypto at all, Math.random(): not fit
7438
+ * for a secret, and a uuid that names a node is not one.
7312
7439
  ************************************************************/
7440
+ function new_uuid() {
7441
+ const c = typeof window !== "undefined" && window.crypto || globalThis.crypto;
7442
+ if (c && typeof c.randomUUID === "function") return c.randomUUID();
7443
+ const b = /* @__PURE__ */ new Uint8Array(16);
7444
+ if (c && typeof c.getRandomValues === "function") c.getRandomValues(b);
7445
+ else for (let i = 0; i < b.length; i++) b[i] = Math.floor(Math.random() * 256);
7446
+ b[6] = b[6] & 15 | 64;
7447
+ b[8] = b[8] & 63 | 128;
7448
+ const h = Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
7449
+ return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
7450
+ }
7451
+ /************************************************************
7452
+ * The uuid of this node: ONE for the life of the page.
7453
+ *
7454
+ * The one the store keeps ("host_uuid"), made and written there when
7455
+ * there is none. When the store cannot keep it (blocked, full, no
7456
+ * localStorage, outside a browser) the one this page made, warned
7457
+ * once. It goes in the node_uuid attribute of the yuno, the id of the
7458
+ * identity card and the `host` of every message the client sends, so
7459
+ * the first answer is kept and answered again: 7.26.27 made a new
7460
+ * uuid on every call when the store could not keep it, and answered
7461
+ * null without crypto.randomUUID().
7462
+ ************************************************************/
7463
+ var uuid_of_this_page = null;
7313
7464
  function node_uuid() {
7314
- let uuid = "";
7315
- if (typeof window !== "undefined" && window.localStorage) {
7316
- uuid = window.localStorage.getItem("host_uuid");
7317
- if (!uuid && window.crypto?.randomUUID) {
7318
- uuid = window.crypto.randomUUID();
7319
- window.localStorage.setItem("host_uuid", uuid);
7465
+ if (uuid_of_this_page) return uuid_of_this_page;
7466
+ const store = local_store();
7467
+ if (!store) {
7468
+ uuid_of_this_page = new_uuid();
7469
+ return uuid_of_this_page;
7470
+ }
7471
+ let uuid = null;
7472
+ try {
7473
+ uuid = store.getItem("host_uuid");
7474
+ } catch (e) {
7475
+ warn_local_storage_once("host_uuid", "node_uuid(): localStorage cannot be read (storage blocked?): a uuid of this page only");
7476
+ uuid_of_this_page = new_uuid();
7477
+ return uuid_of_this_page;
7478
+ }
7479
+ if (!uuid) {
7480
+ uuid = new_uuid();
7481
+ try {
7482
+ store.setItem("host_uuid", uuid);
7483
+ } catch (e) {
7484
+ warn_local_storage_once("host_uuid", "node_uuid(): localStorage cannot be written (storage full or blocked?): a uuid of this page only");
7320
7485
  }
7321
- } else if (typeof globalThis !== "undefined" && globalThis.crypto?.randomUUID) uuid = globalThis.crypto.randomUUID();
7322
- else uuid = "00000000-0000-0000-0000-000000000000";
7323
- return uuid;
7486
+ }
7487
+ uuid_of_this_page = uuid;
7488
+ return uuid_of_this_page;
7324
7489
  }
7325
7490
  /************************************************************
7326
7491
  * Build a cleaned name
@@ -8605,7 +8770,7 @@ var attrs_table = [
8605
8770
  SDATA(data_type_t.DTP_STRING, "cert_pem", sdata_flag_t.SDF_PERSIST, "", "SSL server certification, PEM str format"),
8606
8771
  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
8772
  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"),
8773
+ SDATA(data_type_t.DTP_INTEGER, "timeout_retry", sdata_flag_t.SDF_RD, "5000", "Base of the first reconnect delay (ms, ±20% jitter), at least 1000. It BACKS OFF from here, doubling up to timeout_retry_max, and resets when a session is reached"),
8609
8774
  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
8775
  SDATA(data_type_t.DTP_INTEGER, "timeout_idack", sdata_flag_t.SDF_RD, "5000", "timeout waiting idAck"),
8611
8776
  SDATA(data_type_t.DTP_POINTER, "subscriber", 0, 0, "subscriber of output-events. If null then subscriber is the parent"),
@@ -8630,7 +8795,8 @@ var PRIVATE_DATA = {
8630
8795
  inside_on_open: false,
8631
8796
  retry_ms: 0,
8632
8797
  retry_ms_before_session: 0,
8633
- session_since: 0
8798
+ session_since: 0,
8799
+ retry_floor_warned: false
8634
8800
  };
8635
8801
  var __gclass__ = null;
8636
8802
  /******************************
@@ -8902,7 +9068,7 @@ var CLOSE_PEER_FAULT = 4002;
8902
9068
  function close_for_peer_fault(gobj, reason) {
8903
9069
  let priv = gobj.priv;
8904
9070
  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;
9071
+ if (performance.now() - priv.session_since < retry_bounds(gobj).max) priv.retry_ms = priv.retry_ms_before_session;
8906
9072
  priv.retry_ms_before_session = 0;
8907
9073
  }
8908
9074
  close_websocket(gobj, CLOSE_PEER_FAULT, reason);
@@ -9174,6 +9340,32 @@ function ac_on_close(gobj, event, kw, src) {
9174
9340
  return 0;
9175
9341
  }
9176
9342
  /***************************************************************
9343
+ * The first delay of the backoff and its cap.
9344
+ *
9345
+ * A first delay below RETRY_FLOOR_MS is raised to it, with a WARNING
9346
+ * once: `timeout_retry` 0 never left 0 (0 doubled is 0), so a server
9347
+ * that closed before the ack was reconnected in a loop with no wait,
9348
+ * hundreds of connections a second.
9349
+ ***************************************************************/
9350
+ var RETRY_FLOOR_MS = 1e3;
9351
+ function retry_bounds(gobj) {
9352
+ let priv = gobj.priv;
9353
+ let base = gobj_read_integer_attr(gobj, "timeout_retry");
9354
+ let max = gobj_read_integer_attr(gobj, "timeout_retry_max");
9355
+ if (!(base >= RETRY_FLOOR_MS)) {
9356
+ if (!priv.retry_floor_warned) {
9357
+ priv.retry_floor_warned = true;
9358
+ log_warning(`${gobj_short_name(gobj)}: timeout_retry ${base} ms is below ${RETRY_FLOOR_MS} ms: ${RETRY_FLOOR_MS} ms is used`);
9359
+ }
9360
+ base = RETRY_FLOOR_MS;
9361
+ }
9362
+ if (!(max >= base)) max = base;
9363
+ return {
9364
+ base,
9365
+ max
9366
+ };
9367
+ }
9368
+ /***************************************************************
9177
9369
  * How long to wait before the next reconnect attempt.
9178
9370
  *
9179
9371
  * The delay used to be a constant (`timeout_retry`, 5s), for ever. A
@@ -9194,9 +9386,7 @@ function ac_on_close(gobj, event, kw, src) {
9194
9386
  ***************************************************************/
9195
9387
  function next_retry(gobj) {
9196
9388
  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;
9389
+ const { base, max } = retry_bounds(gobj);
9200
9390
  priv.retry_ms = priv.retry_ms ? Math.min(priv.retry_ms * 2, max) : base;
9201
9391
  let jitter = priv.retry_ms * .2;
9202
9392
  let ms = priv.retry_ms - jitter + Math.random() * 2 * jitter;
@@ -9694,6 +9884,7 @@ exports.json_object_update = json_object_update;
9694
9884
  exports.json_object_update_existing = json_object_update_existing;
9695
9885
  exports.json_object_update_missing = json_object_update_missing;
9696
9886
  exports.json_size = json_size;
9887
+ exports.json_text_of = json_text_of;
9697
9888
  exports.jwt2json = jwt2json;
9698
9889
  exports.jwtDecode = jwtDecode;
9699
9890
  exports.kw_clone_by_keys = kw_clone_by_keys;