@yuneta/gobj-js 7.26.27 → 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
@@ -460,7 +460,10 @@ gobj_create_volatil(name, gclass_name, attrs, parent)
460
460
  gobj_create_pure_child(name, gclass_name, attrs, parent)
461
461
 
462
462
  // Start / Stop
463
- 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)
464
467
  gobj_stop(gobj) // calls mt_stop
465
468
  gobj_start_children(gobj)
466
469
  gobj_stop_children(gobj)
@@ -599,6 +602,9 @@ gobj_search_path(gobj, path)
599
602
 
600
603
  gobj_walk_gobj_children(gobj, walk_type, cb_walking, user_data, user_data2)
601
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).
602
608
  ```
603
609
 
604
610
  ### Persistence
@@ -740,11 +746,16 @@ kw_remove_local_storage_value(key)
740
746
  // kw_set_local_storage_value("k", () => 1) → -1
741
747
  // kw_set_local_storage_value("k", {w: NaN}) → -1 (it was {"w":null})
742
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).
743
751
 
744
752
  // Misc utilities
745
753
  current_timestamp() // ISO string
746
754
  get_now() // Date object
747
- 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)
748
759
  parseBoolean(v) // coerce to boolean
749
760
  empty_string(s) // true if null/undefined/""
750
761
  str_in_list(list, str, ci) // case-insensitive optional
@@ -1106,7 +1117,8 @@ count as reached: the backoff goes on from where it was (7.26.25), so a server
1106
1117
  that breaks every session is not reconnected at the first delay for ever -- unless
1107
1118
  the session outlived `timeout_retry_max`: a session that long was served, and its
1108
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
1109
- `timeout_retry`. The first delay is **at least 1000 ms** (7.26.27): a lower
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
1110
1122
  `timeout_retry` is raised to 1000 with one WARNING, because 0 never doubled
1111
1123
  away from 0 and a server that closes before the ack was reconnected in a loop
1112
1124
  with no wait, hundreds of times a second.
@@ -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,18 +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];
2395
- if (gobj_is_destroying(child)) continue;
2438
+ if (walk_steps_over(child, owner, seen)) continue;
2439
+ seen.add(child);
2396
2440
  let ret = walk_ret(cb_walking(child, user_data, user_data2));
2397
2441
  if (ret < 0) return ret;
2398
2442
  }
2399
2443
  else for (let i = 0; i < iter.length; i++) {
2400
2444
  child = iter[i];
2401
- if (gobj_is_destroying(child)) continue;
2445
+ if (walk_steps_over(child, owner, seen)) continue;
2446
+ seen.add(child);
2402
2447
  let ret = walk_ret(cb_walking(child, user_data, user_data2));
2403
2448
  if (ret < 0) return ret;
2404
2449
  }
@@ -2413,24 +2458,23 @@ function rc_walk_by_list(iter_, walk_type, cb_walking, user_data, user_data2) {
2413
2458
  * or
2414
2459
  * WALK_LAST2FIRST
2415
2460
  ***************************************************************/
2416
- function rc_walk_by_level(iter_, walk_type, cb_walking, user_data, user_data2) {
2417
- let iter = [...iter_];
2418
- let ret = rc_walk_by_list(iter, walk_type, cb_walking, user_data, user_data2);
2419
- if (ret < 0) return ret;
2420
- let child;
2421
- if (walk_type & walk_type_t.WALK_LAST2FIRST) for (let i = iter.length; i > 0; i--) {
2422
- child = iter[i - 1];
2423
- if (gobj_is_destroying(child)) continue;
2424
- 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));
2425
2470
  if (ret < 0) return ret;
2426
2471
  }
2427
- else for (let i = 0; i < iter.length; i++) {
2428
- child = iter[i];
2472
+ for (const child of handed) {
2429
2473
  if (gobj_is_destroying(child)) continue;
2430
- ret = rc_walk_by_level(child.dl_children, walk_type, cb_walking, user_data, user_data2);
2474
+ let ret = rc_walk_by_level(child, seen, walk_type, cb_walking, user_data, user_data2);
2431
2475
  if (ret < 0) return ret;
2432
2476
  }
2433
- return ret;
2477
+ return 0;
2434
2478
  }
2435
2479
  /***************************************************************
2436
2480
  * Walking
@@ -2447,27 +2491,29 @@ function rc_walk_by_level(iter_, walk_type, cb_walking, user_data, user_data2) {
2447
2491
  * or
2448
2492
  * WALK_LAST2FIRST
2449
2493
  ***************************************************************/
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_];
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];
2453
2497
  let ret;
2454
2498
  let child;
2455
2499
  if (walk_type & walk_type_t.WALK_BOTTOM2TOP) for (let i = iter.length; i > 0; i--) {
2456
2500
  child = iter[i - 1];
2457
- if (gobj_is_destroying(child)) continue;
2458
- 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);
2459
2503
  if (ret < 0) return ret;
2460
2504
  if (gobj_is_destroying(child)) continue;
2505
+ seen.add(child);
2461
2506
  ret = walk_ret(cb_walking(child, user_data, user_data2));
2462
2507
  if (ret < 0) return ret;
2463
2508
  }
2464
2509
  else for (let i = 0; i < iter.length; i++) {
2465
2510
  child = iter[i];
2466
- if (gobj_is_destroying(child)) continue;
2511
+ if (walk_steps_over(child, owner, seen)) continue;
2512
+ seen.add(child);
2467
2513
  ret = walk_ret(cb_walking(child, user_data, user_data2));
2468
2514
  if (ret < 0) return ret;
2469
2515
  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);
2516
+ ret = rc_walk_by_tree(child, seen, walk_type, cb_walking, user_data, user_data2);
2471
2517
  if (ret < 0) return ret;
2472
2518
  }
2473
2519
  }
@@ -6100,7 +6146,10 @@ function warn_local_storage_once(key, msg) {
6100
6146
  * for ever; a new corrupt entry is a new WARNING. Its text is never
6101
6147
  * shown (JSON.parse() quotes it in its error, and an entry can hold a
6102
6148
  * 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
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
6104
6153
  * default.
6105
6154
  ********************************************/
6106
6155
  function kw_get_local_storage_value(key, default_value, create = false) {
@@ -6114,6 +6163,7 @@ function kw_get_local_storage_value(key, default_value, create = false) {
6114
6163
  warn_local_storage_once(key, `${where} cannot be read (storage blocked?): the default is used`);
6115
6164
  return default_value;
6116
6165
  }
6166
+ let went_well = true;
6117
6167
  let value = null;
6118
6168
  if (text !== null) try {
6119
6169
  value = JSON.parse(text);
@@ -6122,17 +6172,25 @@ function kw_get_local_storage_value(key, default_value, create = false) {
6122
6172
  try {
6123
6173
  store.removeItem(key);
6124
6174
  log_warning(`${where} does not hold json: removed, the default is used`);
6125
- local_storage_warned.delete(key);
6126
6175
  } catch (e2) {
6176
+ went_well = false;
6127
6177
  warn_local_storage_once(key, `${where} does not hold json and cannot be removed: the default is used`);
6128
6178
  }
6129
- text = void 0;
6130
6179
  }
6131
- if (text !== void 0) local_storage_warned.delete(key);
6132
6180
  if (value === null) {
6133
- if (create) kw_set_local_storage_value(key, default_value);
6134
- 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
+ }
6135
6192
  }
6193
+ if (went_well) local_storage_warned.delete(key);
6136
6194
  return value;
6137
6195
  }
6138
6196
  /********************************************
@@ -7372,29 +7430,62 @@ function index_in_list(list, elm) {
7372
7430
  return -1;
7373
7431
  }
7374
7432
  /************************************************************
7433
+ * A new uuid v4 (RFC 9562).
7375
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.
7376
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;
7377
7464
  function node_uuid() {
7378
- let uuid = "";
7465
+ if (uuid_of_this_page) return uuid_of_this_page;
7379
7466
  const store = local_store();
7380
- if (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();
7381
7481
  try {
7382
- uuid = store.getItem("host_uuid");
7482
+ store.setItem("host_uuid", uuid);
7383
7483
  } 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");
7484
+ warn_local_storage_once("host_uuid", "node_uuid(): localStorage cannot be written (storage full or blocked?): a uuid of this page only");
7386
7485
  }
7387
- if (!uuid && window.crypto?.randomUUID) {
7388
- uuid = window.crypto.randomUUID();
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
- }
7394
- }
7395
- } else if (typeof globalThis !== "undefined" && globalThis.crypto?.randomUUID) uuid = globalThis.crypto.randomUUID();
7396
- else uuid = "00000000-0000-0000-0000-000000000000";
7397
- return uuid;
7486
+ }
7487
+ uuid_of_this_page = uuid;
7488
+ return uuid_of_this_page;
7398
7489
  }
7399
7490
  /************************************************************
7400
7491
  * Build a cleaned name
@@ -8679,7 +8770,7 @@ var attrs_table = [
8679
8770
  SDATA(data_type_t.DTP_STRING, "cert_pem", sdata_flag_t.SDF_PERSIST, "", "SSL server certification, PEM str format"),
8680
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."),
8681
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."),
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"),
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"),
8683
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"),
8684
8775
  SDATA(data_type_t.DTP_INTEGER, "timeout_idack", sdata_flag_t.SDF_RD, "5000", "timeout waiting idAck"),
8685
8776
  SDATA(data_type_t.DTP_POINTER, "subscriber", 0, 0, "subscriber of output-events. If null then subscriber is the parent"),