@yuneta/gobj-js 7.7.2 → 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
  /********************************************
@@ -3622,7 +3645,7 @@ function kwid_new_dict(gobj, kw, path) {
3622
3645
  if (is_object(kw)) new_dict = kw;
3623
3646
  else if (is_array(kw)) for (let i = 0; i < kw.length; i++) {
3624
3647
  let kv = kw[i];
3625
- let id = kw_get_str(kv, "id", null, false);
3648
+ let id = kw_get_str(gobj, kv, "id", "", 0);
3626
3649
  if (!empty_string(id)) new_dict[id] = kv;
3627
3650
  }
3628
3651
  else log_error(`${gobj_short_name(gobj)} kwid_new_dict: data type unknown`);
@@ -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
  *
@@ -4913,6 +4943,7 @@ function treedb_decoder_hook(col, hook) {
4913
4943
  * is_writable // field writable (no readonly)
4914
4944
  * default_value
4915
4945
  * placeholder
4946
+ * fkey // fkey mapping {topic_name: hook_name}, null if none
4916
4947
  * ]
4917
4948
  *
4918
4949
  * The template is recursive, in values you can set dictionaries, or arrays,
@@ -4982,7 +5013,8 @@ function treedb_get_field_desc(col) {
4982
5013
  is_hidden: false,
4983
5014
  default_value: col.default,
4984
5015
  placeholder: col.placeholder,
4985
- fillspace: col.fillspace || 4
5016
+ fillspace: col.fillspace || 4,
5017
+ fkey: col.fkey || null
4986
5018
  };
4987
5019
  if (!col.flag) return field_desc;
4988
5020
  for (let i = 0; i < col.flag.length; i++) {
@@ -5028,7 +5060,8 @@ function template_get_field_desc(key, value) {
5028
5060
  is_writable: false,
5029
5061
  is_hidden: false,
5030
5062
  default_value: void 0,
5031
- placeholder: void 0
5063
+ placeholder: void 0,
5064
+ fkey: null
5032
5065
  };
5033
5066
  if (is_string(value)) {
5034
5067
  let values = value.split(".");
@@ -5072,7 +5105,9 @@ function create_template_record(template, kw) {
5072
5105
  let new_record = {};
5073
5106
  if (!kw) kw = {};
5074
5107
  Object.entries(template).forEach(([field, col]) => {
5075
- new_record[field] = kw_get_dict_value(null, kw, field, kw_get_dict_value(col, "default"));
5108
+ let default_value;
5109
+ if (is_object(col)) default_value = col.default;
5110
+ new_record[field] = kw_get_dict_value(null, kw, field, default_value);
5076
5111
  });
5077
5112
  return new_record;
5078
5113
  }
@@ -5441,7 +5476,9 @@ var attrs_table = [
5441
5476
  SDATA(data_type_t.DTP_STRING, "jwt", sdata_flag_t.SDF_PERSIST, "", "JWT"),
5442
5477
  SDATA(data_type_t.DTP_STRING, "cert_pem", sdata_flag_t.SDF_PERSIST, "", "SSL server certification, PEM str format"),
5443
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."),
5444
- 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"),
5445
5482
  SDATA(data_type_t.DTP_INTEGER, "timeout_idack", sdata_flag_t.SDF_RD, "5000", "timeout waiting idAck"),
5446
5483
  SDATA(data_type_t.DTP_POINTER, "subscriber", 0, 0, "subscriber of output-events. If null then subscriber is the parent"),
5447
5484
  SDATA_END()
@@ -5454,7 +5491,8 @@ var PRIVATE_DATA = {
5454
5491
  gobj_timer: null,
5455
5492
  inform_on_close: false,
5456
5493
  websocket: null,
5457
- inside_on_open: false
5494
+ inside_on_open: false,
5495
+ retry_ms: 0
5458
5496
  };
5459
5497
  var __gclass__ = null;
5460
5498
  /******************************
@@ -5502,6 +5540,7 @@ function mt_writing(gobj, path) {
5502
5540
  function mt_start(gobj) {
5503
5541
  let priv = gobj.priv;
5504
5542
  gobj_start(priv.gobj_timer);
5543
+ priv.retry_ms = 0;
5505
5544
  priv.websocket = setup_websocket(gobj);
5506
5545
  return 0;
5507
5546
  }
@@ -5760,6 +5799,8 @@ function send_identity_card(gobj) {
5760
5799
  let yuno_version = gobj_read_str_attr(gobj_yuno(), "yuno_version");
5761
5800
  let yuno_release = gobj_read_str_attr(gobj_yuno(), "yuno_release");
5762
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");
5763
5804
  let kw = {
5764
5805
  "yuno_role": gobj_yuno_role(),
5765
5806
  "yuno_id": gobj_yuno_id(),
@@ -5778,7 +5819,7 @@ function send_identity_card(gobj) {
5778
5819
  "id": node_uuid(),
5779
5820
  "user_agent": typeof window !== "undefined" ? window.navigator.userAgent : "",
5780
5821
  "language": typeof window !== "undefined" ? window.navigator.language : "",
5781
- "required_services": gobj_read_attr(gobj_yuno(), "required_services")
5822
+ "required_services": required_services
5782
5823
  };
5783
5824
  msg_iev_push_stack(gobj, kw, IEVENT_STACK_ID, build_cli_ievent_request(gobj, gobj_name(gobj_parent(gobj)), null));
5784
5825
  msg_iev_set_msg_type(gobj, kw, "__identity__");
@@ -5907,9 +5948,37 @@ function ac_on_close(gobj, event, kw, src) {
5907
5948
  was_clean: e.wasClean
5908
5949
  });
5909
5950
  }
5910
- 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));
5911
5952
  return 0;
5912
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
+ }
5913
5982
  /********************************************
5914
5983
  *
5915
5984
  ********************************************/
@@ -5950,6 +6019,7 @@ function ac_identity_card_ack(gobj, event, kw, src) {
5950
6019
  } else {
5951
6020
  gobj_change_state(gobj, "ST_SESSION");
5952
6021
  priv.inside_on_open = true;
6022
+ priv.retry_ms = 0;
5953
6023
  if (!priv.inform_on_close) {
5954
6024
  priv.inform_on_close = true;
5955
6025
  let kw_on_open = {
@@ -6373,6 +6443,7 @@ exports.register_c_yuno = register_c_yuno;
6373
6443
  exports.safeSrc = safeSrc;
6374
6444
  exports.sdata_flag_t = sdata_flag_t;
6375
6445
  exports.send_http_json_post = send_http_json_post;
6446
+ exports.set_console_log_enabled = set_console_log_enabled;
6376
6447
  exports.set_log_callback = set_log_callback;
6377
6448
  exports.set_remote_log_functions = set_remote_log_functions;
6378
6449
  exports.set_timeout = set_timeout;