@yuneta/gobj-js 7.7.3 → 7.8.0

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
@@ -477,6 +477,16 @@ gobj_list_persistent_attrs(gobj, keys)
477
477
 
478
478
  Attributes marked `SDF_PERSIST` are automatically saved/loaded.
479
479
 
480
+ **Always name what you save**: `gobj_save_persistent_attrs(gobj, "attr_name")`
481
+ (a string, or a list/dict of names). The bare call saves every `SDF_PERSIST`
482
+ attr, which is wasteful and can clobber attrs the caller never touched.
483
+
484
+ **Check the result.** `db_save_persistent_attrs()` / `gobj_save_persistent_attrs()`
485
+ return **`0` on success, `-1` on failure**, and a failure is logged through
486
+ `log_error`. `localStorage` can *refuse* a write (quota exceeded, private mode,
487
+ storage blocked): treating that as saved leaves the in-memory attr and the UI
488
+ showing a value the store rejected, which the next reload silently loses.
489
+
480
490
  ### Helpers & Utilities
481
491
 
482
492
  ```javascript
@@ -572,10 +582,54 @@ trace_msg(format, ...args) // cyan, prefixed "MSG"
572
582
  trace_json(json, msg) // dir-dump a JSON object
573
583
 
574
584
  // Redirect error/warning output to a single remote handler
575
- // (info/debug always go to the browser console)
576
585
  set_remote_log_functions(remote_log_fn) // fn(message) — single function
586
+
587
+ // Receive every log record (any level) in your own sink
588
+ set_log_callback(fn) // fn(level, message)
589
+
590
+ // Gate the DIRECT browser-console writes of all the functions above
591
+ set_console_log_enabled(enabled) // default: true (unchanged behaviour)
577
592
  ```
578
593
 
594
+ **Three independent output paths.** `set_console_log_enabled(false)` silences
595
+ only the `console.*` writes: the log-sink callback (`set_log_callback`) and the
596
+ remote log functions (`set_remote_log_functions`) still fire. That is what lets
597
+ a GUI route framework output — **including the automata/FSM trace, which arrives
598
+ as `debug`** — into its own monitor window while keeping the browser console
599
+ clean (gobj-ui's dev-window "Output" selector does exactly this).
600
+
601
+ ### i18n: `refresh_language` and the `data-i18n-*` family
602
+
603
+ The runtime does not translate anything itself — the **app** owns the
604
+ translator (i18next, typically) and passes it in as `t`. What the runtime
605
+ provides is the mechanism to **re-translate live DOM** when the language
606
+ changes:
607
+
608
+ ```javascript
609
+ refresh_language(element, t) // element defaults to `document`
610
+ ```
611
+
612
+ It only re-translates a node that **carries its key**, so passing a string
613
+ through `t()` once at build time is not enough — that text is frozen in the
614
+ mount-time language for the life of the view. Tag the DOM instead:
615
+
616
+ | Attribute | Re-translates |
617
+ |---|---|
618
+ | `data-i18n` | the element's first text node |
619
+ | `data-i18n-title` | the `title` attribute (hover tooltip) |
620
+ | `data-i18n-aria-label` | the `aria-label` (assistive-tech name) |
621
+ | `data-i18n-placeholder` | the `placeholder` of an input/textarea |
622
+
623
+ The three attribute variants exist because `data-i18n` walks **text nodes**,
624
+ which cannot reach an attribute. Anything a third-party **widget** draws is
625
+ reachable by none of them — re-render it from the language-change handler
626
+ instead.
627
+
628
+ **A missing key is invisible**: i18next answers an unknown key with the key
629
+ itself, so it renders as lower-case English and never changes language. Do not
630
+ compose data into a key either (`` `${key} · ${t(mode)}` ``) — split it so each
631
+ translatable half carries its own key.
632
+
579
633
  ### String Formatting
580
634
 
581
635
  ```javascript
@@ -600,6 +654,10 @@ register_c_yuno();
600
654
 
601
655
  Key attributes: `yuno_name`, `yuno_role`, `yuno_id`, `yuno_version`, `yuno_release`, `yuneta_version`, `required_services`, `tracing`, `start_date`, `node_uuid`, `__username__`
602
656
 
657
+ The yuno's `required_services` is the **fallback** for links that declare none
658
+ of their own. A multi-link yuno should set the list on each `C_IEVENT_CLI`
659
+ instead — see [C_IEVENT_CLI](#c_ievent_cli) below.
660
+
603
661
  ### C_TIMER
604
662
 
605
663
  Manages timeouts and periodic timers.
@@ -638,7 +696,23 @@ let remote = gobj_create_service("backend", "C_IEVENT_CLI", {
638
696
  }, gobj_yuno());
639
697
  ```
640
698
 
641
- Key attributes: `url`, `jwt`, `wanted_yuno_role`, `wanted_yuno_name`, `wanted_yuno_service`, `remote_yuno_role`, `remote_yuno_name`, `remote_yuno_service`
699
+ Key attributes: `url`, `jwt`, `wanted_yuno_role`, `wanted_yuno_name`, `wanted_yuno_service`, `remote_yuno_role`, `remote_yuno_name`, `remote_yuno_service`, `required_services`, `timeout_retry`, `timeout_retry_max`
700
+
701
+ **`required_services`** (default `[]`) is the list this link advertises in its
702
+ identity_card, which the remote's `C_AUTHZ` uses to authorize commands. It is
703
+ **per link**: empty falls back to the yuno's own `required_services`, so a
704
+ single-link yuno needs nothing. A **multi-link** yuno must set it per link — a
705
+ yuno-wide list can only be the union of every backend's services, so each
706
+ backend would be handed the service names of all the others, and get a card
707
+ naming services it does not host.
708
+
709
+ **Reconnect backoff.** A dropped link retries after `timeout_retry` (5s), then
710
+ **doubles** up to `timeout_retry_max` (60s), with ±20% jitter. The jitter is
711
+ what breaks lockstep, so N links that dropped together do not stampede a backend
712
+ that is just coming back. The backoff resets when a session is actually reached,
713
+ and on `mt_start` — a deliberate reconnect never inherits a previous run's
714
+ penalty. For the old fixed-interval behaviour set `timeout_retry_max` equal to
715
+ `timeout_retry`.
642
716
 
643
717
  ---
644
718
 
@@ -2630,6 +2630,10 @@ var __log_callback__ = null;
2630
2630
  function set_log_callback(fn) {
2631
2631
  __log_callback__ = typeof fn === "function" ? fn : null;
2632
2632
  }
2633
+ var __console_output_enabled__ = true;
2634
+ function set_console_log_enabled(enabled) {
2635
+ __console_output_enabled__ = enabled ? true : false;
2636
+ }
2633
2637
  var __in_log_callback__ = false;
2634
2638
  function emit_log_callback(level, msg, hora) {
2635
2639
  if (__log_callback__ && !__in_log_callback__) {
@@ -2675,9 +2679,10 @@ function format_log(format) {
2675
2679
  function log_error(format) {
2676
2680
  let msg = format_log(format);
2677
2681
  let hora = current_timestamp();
2678
- if (f_error) if (f_error === window.console.error) f_error("%c" + hora + " ERROR: " + String(msg), "color:red");
2679
- else {
2680
- window.console.error("%c" + hora + " ERROR: " + String(msg), "color:red");
2682
+ if (f_error) if (f_error === window.console.error) {
2683
+ if (__console_output_enabled__) f_error("%c" + hora + " ERROR: " + String(msg), "color:red");
2684
+ } else {
2685
+ if (__console_output_enabled__) window.console.error("%c" + hora + " ERROR: " + String(msg), "color:red");
2681
2686
  f_error(`${hora} ERROR: ${String(msg)}`);
2682
2687
  }
2683
2688
  emit_log_callback("error", msg, hora);
@@ -2685,9 +2690,10 @@ function log_error(format) {
2685
2690
  function log_warning(format) {
2686
2691
  let msg = format_log(format);
2687
2692
  let hora = current_timestamp();
2688
- if (f_warning) if (f_warning === window.console.warn) f_warning("%c" + hora + " WARNING: " + String(msg), "color:yellow");
2689
- else {
2690
- window.console.warn("%c" + hora + " WARNING: " + String(msg), "color:yellow");
2693
+ if (f_warning) if (f_warning === window.console.warn) {
2694
+ if (__console_output_enabled__) f_warning("%c" + hora + " WARNING: " + String(msg), "color:yellow");
2695
+ } else {
2696
+ if (__console_output_enabled__) window.console.warn("%c" + hora + " WARNING: " + String(msg), "color:yellow");
2691
2697
  f_warning(`${hora} WARNING: ${String(msg)}`);
2692
2698
  }
2693
2699
  emit_log_callback("warning", msg, hora);
@@ -2695,24 +2701,32 @@ function log_warning(format) {
2695
2701
  function log_info(format) {
2696
2702
  let msg = format_log(format);
2697
2703
  let hora = current_timestamp();
2698
- if (f_info) f_info("%c" + hora + " INFO: " + String(msg), "color:cyan");
2704
+ if (f_info) {
2705
+ if (__console_output_enabled__) f_info("%c" + hora + " INFO: " + String(msg), "color:cyan");
2706
+ }
2699
2707
  emit_log_callback("info", msg, hora);
2700
2708
  }
2701
2709
  function log_debug(format) {
2702
2710
  let msg = format_log(format);
2703
2711
  let hora = current_timestamp();
2704
- if (f_debug) f_debug("%c" + hora + " DEBUG: " + String(msg), "color:silver");
2712
+ if (f_debug) {
2713
+ if (__console_output_enabled__) f_debug("%c" + hora + " DEBUG: " + String(msg), "color:silver");
2714
+ }
2705
2715
  emit_log_callback("debug", msg, hora);
2706
2716
  }
2707
2717
  function trace_msg(format) {
2708
2718
  let msg = format_log(format);
2709
2719
  let hora = current_timestamp();
2710
- if (f_debug) f_debug("%c" + hora + " MSG: " + String(msg), "color:cyan");
2720
+ if (f_debug) {
2721
+ if (__console_output_enabled__) f_debug("%c" + hora + " MSG: " + String(msg), "color:cyan");
2722
+ }
2711
2723
  emit_log_callback("msg", msg, hora);
2712
2724
  }
2713
2725
  function trace_json(jn, msg) {
2714
- if (msg) window.console.warn("=====> " + msg);
2715
- window.console.dir(jn);
2726
+ if (__console_output_enabled__) {
2727
+ if (msg) window.console.warn("=====> " + msg);
2728
+ window.console.dir(jn);
2729
+ }
2716
2730
  emit_log_callback("json", jn, current_timestamp());
2717
2731
  }
2718
2732
  /************************************************************
@@ -3525,17 +3539,26 @@ function kw_get_local_storage_value(key, default_value, create = false) {
3525
3539
  }
3526
3540
  /********************************************
3527
3541
  * Save a local attribute
3542
+ *
3543
+ * Returns 0 on success, -1 when the value did NOT reach the store.
3544
+ * It used to return nothing and only console.warn, so every caller up the
3545
+ * chain (db_save_persistent_attrs -> gobj_save_persistent_attrs -> the app)
3546
+ * reported success on a store that had rejected the write: a full or blocked
3547
+ * localStorage (quota, private mode) silently discarded the change while the
3548
+ * UI showed it as saved.
3528
3549
  ********************************************/
3529
3550
  function kw_set_local_storage_value(key, value) {
3530
- if (typeof window === "undefined" || !window.localStorage) return;
3551
+ if (typeof window === "undefined" || !window.localStorage) return 0;
3531
3552
  if (!key || value === void 0) {
3532
- window.console.warn(`Invalid key or value for localStorage: key=${key}, value=${value}`);
3533
- return;
3553
+ log_error(`kw_set_local_storage_value(): invalid key or value: key=${key}`);
3554
+ return -1;
3534
3555
  }
3535
3556
  try {
3536
3557
  window.localStorage.setItem(key, JSON.stringify(value));
3558
+ return 0;
3537
3559
  } catch (e) {
3538
- window.console.warn(`Error saving localStorage key "${key}":`, e);
3560
+ log_error(`kw_set_local_storage_value(): cannot save "${key}" (storage full or blocked?): ${e && e.message ? e.message : e}`);
3561
+ return -1;
3539
3562
  }
3540
3563
  }
3541
3564
  /********************************************
@@ -4510,6 +4533,10 @@ function refresh_language(element, t) {
4510
4533
  let value = elem.getAttribute("data-i18n-aria-label");
4511
4534
  if (value) elem.setAttribute("aria-label", t(value));
4512
4535
  });
4536
+ element.querySelectorAll("[data-i18n-placeholder]").forEach(function(elem) {
4537
+ let value = elem.getAttribute("data-i18n-placeholder");
4538
+ if (value) elem.setAttribute("placeholder", t(value));
4539
+ });
4513
4540
  }
4514
4541
  /************************************************************
4515
4542
  * Example Usage
@@ -4691,13 +4718,16 @@ function db_load_persistent_attrs(gobj, keys) {
4691
4718
  }
4692
4719
  }
4693
4720
  /************************************************************
4694
- *
4721
+ * Returns 0 on success, -1 when the attrs did NOT reach the store
4722
+ * (quota exceeded, private mode, storage disabled). Callers must check
4723
+ * it: a discarded write with a UI that shows the value as saved is the
4724
+ * worst of both worlds.
4695
4725
  ************************************************************/
4696
4726
  function db_save_persistent_attrs(gobj, keys) {
4697
4727
  let attrs = kw_clone_by_keys(gobj, gobj_read_attrs(gobj, sdata_flag_t.SDF_PERSIST, 0), keys, false);
4698
4728
  let jn_file = kw_get_local_storage_value(_get_persistent_path(gobj), null, false);
4699
4729
  if (jn_file && is_object(jn_file)) json_object_update_missing(attrs, jn_file);
4700
- kw_set_local_storage_value(_get_persistent_path(gobj), attrs);
4730
+ return kw_set_local_storage_value(_get_persistent_path(gobj), attrs);
4701
4731
  }
4702
4732
  /************************************************************
4703
4733
  *
@@ -5446,7 +5476,9 @@ var attrs_table = [
5446
5476
  SDATA(data_type_t.DTP_STRING, "jwt", sdata_flag_t.SDF_PERSIST, "", "JWT"),
5447
5477
  SDATA(data_type_t.DTP_STRING, "cert_pem", sdata_flag_t.SDF_PERSIST, "", "SSL server certification, PEM str format"),
5448
5478
  SDATA(data_type_t.DTP_JSON, "extra_info", sdata_flag_t.SDF_RD, "{}", "dict data set by user, added to the identity card msg."),
5449
- SDATA(data_type_t.DTP_INTEGER, "timeout_retry", sdata_flag_t.SDF_RD, "5000", "timeout waiting idAck"),
5479
+ 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."),
5480
+ 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"),
5481
+ 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"),
5450
5482
  SDATA(data_type_t.DTP_INTEGER, "timeout_idack", sdata_flag_t.SDF_RD, "5000", "timeout waiting idAck"),
5451
5483
  SDATA(data_type_t.DTP_POINTER, "subscriber", 0, 0, "subscriber of output-events. If null then subscriber is the parent"),
5452
5484
  SDATA_END()
@@ -5459,7 +5491,8 @@ var PRIVATE_DATA = {
5459
5491
  gobj_timer: null,
5460
5492
  inform_on_close: false,
5461
5493
  websocket: null,
5462
- inside_on_open: false
5494
+ inside_on_open: false,
5495
+ retry_ms: 0
5463
5496
  };
5464
5497
  var __gclass__ = null;
5465
5498
  /******************************
@@ -5507,6 +5540,7 @@ function mt_writing(gobj, path) {
5507
5540
  function mt_start(gobj) {
5508
5541
  let priv = gobj.priv;
5509
5542
  gobj_start(priv.gobj_timer);
5543
+ priv.retry_ms = 0;
5510
5544
  priv.websocket = setup_websocket(gobj);
5511
5545
  return 0;
5512
5546
  }
@@ -5765,6 +5799,8 @@ function send_identity_card(gobj) {
5765
5799
  let yuno_version = gobj_read_str_attr(gobj_yuno(), "yuno_version");
5766
5800
  let yuno_release = gobj_read_str_attr(gobj_yuno(), "yuno_release");
5767
5801
  let yuno_tag = gobj_read_str_attr(gobj_yuno(), "yuno_tag");
5802
+ let required_services = gobj_read_attr(gobj, "required_services");
5803
+ if (!required_services || !required_services.length) required_services = gobj_read_attr(gobj_yuno(), "required_services");
5768
5804
  let kw = {
5769
5805
  "yuno_role": gobj_yuno_role(),
5770
5806
  "yuno_id": gobj_yuno_id(),
@@ -5783,7 +5819,7 @@ function send_identity_card(gobj) {
5783
5819
  "id": node_uuid(),
5784
5820
  "user_agent": typeof window !== "undefined" ? window.navigator.userAgent : "",
5785
5821
  "language": typeof window !== "undefined" ? window.navigator.language : "",
5786
- "required_services": gobj_read_attr(gobj_yuno(), "required_services")
5822
+ "required_services": required_services
5787
5823
  };
5788
5824
  msg_iev_push_stack(gobj, kw, IEVENT_STACK_ID, build_cli_ievent_request(gobj, gobj_name(gobj_parent(gobj)), null));
5789
5825
  msg_iev_set_msg_type(gobj, kw, "__identity__");
@@ -5912,9 +5948,37 @@ function ac_on_close(gobj, event, kw, src) {
5912
5948
  was_clean: e.wasClean
5913
5949
  });
5914
5950
  }
5915
- if (gobj_is_running(gobj)) set_timeout(priv.gobj_timer, gobj_read_integer_attr(gobj, "timeout_retry"));
5951
+ if (gobj_is_running(gobj)) set_timeout(priv.gobj_timer, next_retry(gobj));
5916
5952
  return 0;
5917
5953
  }
5954
+ /***************************************************************
5955
+ * How long to wait before the next reconnect attempt.
5956
+ *
5957
+ * The delay used to be a constant (`timeout_retry`, 5s), for ever. A
5958
+ * backend that is down — or a URL with a typo, which never comes back —
5959
+ * was therefore hit every 5 seconds for the whole life of the tab, by
5960
+ * EVERY link pointed at it, all in lockstep. It backs off now: from
5961
+ * `timeout_retry`, doubling, capped at `timeout_retry_max`.
5962
+ *
5963
+ * The jitter (±20%) is what breaks the lockstep. Without it, N links that
5964
+ * went down together retry together for ever — a thundering herd against a
5965
+ * backend that is just coming back up. It is the one place a random number
5966
+ * earns its keep.
5967
+ *
5968
+ * Reset to zero when a session is actually reached
5969
+ * (ac_identity_card_ack), so a link that flaps once does not carry a
5970
+ * minute-long penalty into its next real drop.
5971
+ ***************************************************************/
5972
+ function next_retry(gobj) {
5973
+ let priv = gobj.priv;
5974
+ let base = gobj_read_integer_attr(gobj, "timeout_retry");
5975
+ let max = gobj_read_integer_attr(gobj, "timeout_retry_max");
5976
+ if (max < base) max = base;
5977
+ priv.retry_ms = priv.retry_ms ? Math.min(priv.retry_ms * 2, max) : base;
5978
+ let jitter = priv.retry_ms * .2;
5979
+ let ms = priv.retry_ms - jitter + Math.random() * 2 * jitter;
5980
+ return Math.max(1, Math.round(ms));
5981
+ }
5918
5982
  /********************************************
5919
5983
  *
5920
5984
  ********************************************/
@@ -5955,6 +6019,7 @@ function ac_identity_card_ack(gobj, event, kw, src) {
5955
6019
  } else {
5956
6020
  gobj_change_state(gobj, "ST_SESSION");
5957
6021
  priv.inside_on_open = true;
6022
+ priv.retry_ms = 0;
5958
6023
  if (!priv.inform_on_close) {
5959
6024
  priv.inform_on_close = true;
5960
6025
  let kw_on_open = {
@@ -6378,6 +6443,7 @@ exports.register_c_yuno = register_c_yuno;
6378
6443
  exports.safeSrc = safeSrc;
6379
6444
  exports.sdata_flag_t = sdata_flag_t;
6380
6445
  exports.send_http_json_post = send_http_json_post;
6446
+ exports.set_console_log_enabled = set_console_log_enabled;
6381
6447
  exports.set_log_callback = set_log_callback;
6382
6448
  exports.set_remote_log_functions = set_remote_log_functions;
6383
6449
  exports.set_timeout = set_timeout;