@yuneta/gobj-ui 7.25.5 → 7.25.7

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.
@@ -3020,6 +3020,14 @@ function maybe_apply_translator($node, opts) {
3020
3020
  * Non-blocking, auto-dismiss after `opts.timeout` ms (default
3021
3021
  * 5000). `opts.timeout = 0` disables auto-dismiss.
3022
3022
  * Returns `{ close() }` so callers can dismiss programmatically.
3023
+ *
3024
+ * ONE toast per message on screen: a string message of the same
3025
+ * kind as a toast still showing is not stacked under it -- that
3026
+ * toast is kept, its time starts again, and its handle is
3027
+ * returned. One close of a transport settles every request it
3028
+ * cut, each view shows its failure, and the operator got a
3029
+ * column of identical "the connection dropped". A message built
3030
+ * of nodes is never merged: its text is not a key.
3023
3031
  ***************************************************************/
3024
3032
  function show_notification(shell, kind, message, opts) {
3025
3033
  let $layer = notification_layer(shell);
@@ -3027,6 +3035,13 @@ function show_notification(shell, kind, message, opts) {
3027
3035
  log_warning("yui_shell_show_*: shell has no notification layer");
3028
3036
  return { close: () => {} };
3029
3037
  }
3038
+ let key = typeof message === "string" ? `${kind}\u0000${message}` : null;
3039
+ if (key !== null) {
3040
+ for (let $shown of Array.from($layer.children)) if ($shown.__toast_key__ === key && $shown.__toast__) {
3041
+ $shown.__toast__.restart();
3042
+ return $shown.__toast__.handle;
3043
+ }
3044
+ }
3030
3045
  let p_attrs = typeof message === "string" ? { i18n: message } : {};
3031
3046
  let $note = createElement2([
3032
3047
  "div",
@@ -3063,8 +3078,21 @@ function show_notification(shell, kind, message, opts) {
3063
3078
  let $del = $note.querySelector(".delete");
3064
3079
  if ($del) $del.addEventListener("click", close);
3065
3080
  let timeout = opts && opts.timeout != null ? opts.timeout : 5e3;
3066
- if (timeout > 0) timeout_id = setTimeout(close, timeout);
3067
- return { close };
3081
+ let restart = function() {
3082
+ if (timeout_id) {
3083
+ clearTimeout(timeout_id);
3084
+ timeout_id = null;
3085
+ }
3086
+ if (timeout > 0) timeout_id = setTimeout(close, timeout);
3087
+ };
3088
+ restart();
3089
+ let handle = { close };
3090
+ $note.__toast_key__ = key;
3091
+ $note.__toast__ = {
3092
+ handle,
3093
+ restart
3094
+ };
3095
+ return handle;
3068
3096
  }
3069
3097
  function yui_shell_show_info(shell, message, opts) {
3070
3098
  return show_notification(shell, "info", message, opts);
@@ -72096,6 +72124,12 @@ function write_options(op) {
72096
72124
  * `update-node` / `delete-node` on the treedb service, and it may
72097
72125
  * be a routing adapter two hops away (the agent console's
72098
72126
  * C_AGENT_TREEDB_LINK) — this view cannot tell, and must not.
72127
+ * A DROP of it (the host's EV_TRANSPORT_STATE, or a failure that
72128
+ * arrives while the transport is out of session) ends the load or
72129
+ * the write it cut, and the model is asked again when the session
72130
+ * is back. Every request carries its round (`round` for a load,
72131
+ * `write` for a write) in `__md_command__`, so an answer of what
72132
+ * was cut is ignored instead of counted in the next one.
72099
72133
  *
72100
72134
  * Copyright (c) 2026, ArtGins.
72101
72135
  * All Rights Reserved.
@@ -72128,6 +72162,7 @@ var PRIVATE_DATA$1 = {
72128
72162
  model: null,
72129
72163
  pending: 0,
72130
72164
  load_error: "",
72165
+ load_round: 0,
72131
72166
  treedb_id: "",
72132
72167
  topic_name: "",
72133
72168
  diagram: false,
@@ -72141,7 +72176,12 @@ var PRIVATE_DATA$1 = {
72141
72176
  save_queue: null,
72142
72177
  save_return: "",
72143
72178
  save_wrote: false,
72179
+ write_tag: 0,
72144
72180
  connected: false,
72181
+ reload_on_open: false,
72182
+ dropped_round: 0,
72183
+ dropped_write: 0,
72184
+ reload_after_write: false,
72145
72185
  pending_seg: null,
72146
72186
  import_plan: null,
72147
72187
  import_pane: null,
@@ -72312,6 +72352,7 @@ function request_model(gobj) {
72312
72352
  };
72313
72353
  priv.load_error = "";
72314
72354
  priv.pending = 0;
72355
+ priv.load_round++;
72315
72356
  gobj_change_state(gobj, "ST_LOADING");
72316
72357
  show_notice(gobj, "loading the schemas");
72317
72358
  for (let topic_name of [
@@ -72321,7 +72362,7 @@ function request_model(gobj) {
72321
72362
  ]) if (remote_command(gobj, "nodes", {
72322
72363
  topic_name,
72323
72364
  options: { list_dict: true }
72324
- }) === 0) priv.pending++;
72365
+ }, { round: priv.load_round }) === 0) priv.pending++;
72325
72366
  if (priv.pending === 0) {
72326
72367
  priv.load_error = t("cannot reach the treedb");
72327
72368
  gobj_change_state(gobj, "ST_IDLE");
@@ -72339,7 +72380,7 @@ function request_model(gobj) {
72339
72380
  * answer says which topic it is about, since the command is
72340
72381
  * `nodes` for all three.
72341
72382
  ***************************************************************/
72342
- function remote_command(gobj, command, kw) {
72383
+ function remote_command(gobj, command, kw, tag) {
72343
72384
  let remote = gobj_read_pointer_attr(gobj, "gobj_remote_yuno");
72344
72385
  let treedb_name = gobj_read_str_attr(gobj, "treedb_name");
72345
72386
  if (!remote) {
@@ -72350,11 +72391,11 @@ function remote_command(gobj, command, kw) {
72350
72391
  service: treedb_name,
72351
72392
  treedb_name
72352
72393
  }, kw || {});
72353
- full_kw.__md_command__ = {
72394
+ full_kw.__md_command__ = Object.assign({
72354
72395
  purpose: PURPOSE,
72355
72396
  topic_name: kw && kw.topic_name || "",
72356
72397
  record_id: kw && kw.record_id || ""
72357
- };
72398
+ }, tag || {});
72358
72399
  delete full_kw.record_id;
72359
72400
  let ret = gobj_command(remote, command, full_kw, gobj);
72360
72401
  if (ret) {
@@ -74585,12 +74626,92 @@ function run_next_write(gobj) {
74585
74626
  let write = priv.save_queue[0];
74586
74627
  let command = write_command(write.op);
74587
74628
  let options = write_options(write.op);
74629
+ priv.write_tag++;
74588
74630
  if (remote_command(gobj, command, {
74589
74631
  topic_name: write.topic_name,
74590
74632
  record: write.record,
74591
74633
  options,
74592
74634
  record_id: write.record.id || ""
74593
- }) < 0) return end_writes(gobj, t("cannot reach the treedb"));
74635
+ }, { write: priv.write_tag }) < 0) return end_writes(gobj, t("cannot reach the treedb"));
74636
+ return 0;
74637
+ }
74638
+ /***************************************************************
74639
+ * Does the transport say it is in session? The state the library
74640
+ * reads off every transport (C_IEVENT_CLI and the routing adapters
74641
+ * name it the same).
74642
+ ***************************************************************/
74643
+ function transport_in_session(gobj) {
74644
+ let remote = gobj_read_pointer_attr(gobj, "gobj_remote_yuno");
74645
+ return !!remote && gobj_current_state(remote) === "ST_SESSION";
74646
+ }
74647
+ /***************************************************************
74648
+ * Is this answer of the load in flight, or of the write in
74649
+ * flight? Each request carries its round in `__md_command__`, and
74650
+ * one the drop abandoned (or a deadline settled) can still come
74651
+ * back: counted, it ended the next load with the old data or
74652
+ * moved the next write's queue.
74653
+ *
74654
+ * The state is part of the question, and that is only true
74655
+ * because NOTHING moves the editor out of ST_LOADING or ST_SAVING
74656
+ * but the end of what is in flight: a position the host sends
74657
+ * meanwhile waits in `pending_seg` (ac_show()). 7.25.6 let EV_SHOW
74658
+ * move a RELOAD out of ST_LOADING, and every answer of it was then
74659
+ * "for a load that is over": the records it had emptied stayed
74660
+ * empty, and the next write patched them into a model with no
74661
+ * treedb at all.
74662
+ ***************************************************************/
74663
+ function is_current_load(gobj, md) {
74664
+ return gobj_current_state(gobj) === "ST_LOADING" && kw_get_int(gobj, md, "round", 0, 0) === gobj.priv.load_round;
74665
+ }
74666
+ function is_current_write(gobj, md) {
74667
+ return gobj_current_state(gobj) === "ST_SAVING" && kw_get_int(gobj, md, "write", 0, 0) === gobj.priv.write_tag;
74668
+ }
74669
+ /***************************************************************
74670
+ * The transport dropped under a load or a write: its answers
74671
+ * will never come. The load goes back to the screen it replaced
74672
+ * (or to "not connected" when there was none), the write ends
74673
+ * with the drop said -- whether it landed is unknown -- and both
74674
+ * mark the model to be asked again when the session is back.
74675
+ * Bumping the round makes an answer of what was cut stale.
74676
+ ***************************************************************/
74677
+ function transport_dropped(gobj) {
74678
+ let priv = gobj.priv;
74679
+ let state = gobj_current_state(gobj);
74680
+ priv.reload_on_open = true;
74681
+ if (state === "ST_SAVING") {
74682
+ log_warning(`${gobj_short_name(gobj)}: the transport dropped during a write: the model is read again on the reconnect`);
74683
+ priv.dropped_write = priv.write_tag;
74684
+ priv.write_tag++;
74685
+ return end_writes(gobj, "the connection dropped during the write");
74686
+ }
74687
+ if (state === "ST_LOADING") {
74688
+ log_warning(`${gobj_short_name(gobj)}: the transport dropped during the load: the model is read again on the reconnect`);
74689
+ priv.dropped_round = priv.load_round;
74690
+ priv.load_round++;
74691
+ priv.pending = 0;
74692
+ priv.load_error = "";
74693
+ return end_load(gobj);
74694
+ }
74695
+ return 0;
74696
+ }
74697
+ /***************************************************************
74698
+ * A load is over -- landed, failed or cut -- and the editor goes
74699
+ * where the operator is: the position the host sent while it was
74700
+ * in flight (ac_show() kept it), or the one it had. Without a
74701
+ * model there is no screen to go to: the position is kept for the
74702
+ * next load, and the editor says why it is empty.
74703
+ ***************************************************************/
74704
+ function end_load(gobj) {
74705
+ let priv = gobj.priv;
74706
+ let seg = priv.pending_seg;
74707
+ priv.pending_seg = null;
74708
+ if (priv.model) {
74709
+ if (seg !== null) return apply_seg(gobj, seg);
74710
+ return go(gobj, priv.treedb_id, priv.topic_name, priv.diagram, false);
74711
+ }
74712
+ if (seg !== null) adopt_seg(gobj, seg);
74713
+ gobj_change_state(gobj, "ST_IDLE");
74714
+ render(gobj);
74594
74715
  return 0;
74595
74716
  }
74596
74717
  /***************************************************************
@@ -74613,7 +74734,7 @@ function end_writes(gobj, error) {
74613
74734
  apply_seg(gobj, seg);
74614
74735
  if (priv.save_wrote) gobj_publish_event(gobj, "EV_RECORD_WRITTEN", { treedb_name: gobj_read_str_attr(gobj, "treedb_name") });
74615
74736
  priv.save_wrote = false;
74616
- return 0;
74737
+ return reload_if_owed(gobj);
74617
74738
  }
74618
74739
  }
74619
74740
  if (error) yui_shell_show_error(yui_shell_of(gobj), error, { t });
@@ -74621,9 +74742,25 @@ function end_writes(gobj, error) {
74621
74742
  priv.save_wrote = false;
74622
74743
  render(gobj);
74623
74744
  publish_check(gobj);
74745
+ reload_if_owed(gobj);
74624
74746
  return error ? -1 : 0;
74625
74747
  }
74626
74748
  /***************************************************************
74749
+ * A write given up earlier turned out DONE while another one was
74750
+ * in flight (see ac_mt_command_answer()): the model is read again
74751
+ * now that nothing is.
74752
+ ***************************************************************/
74753
+ function reload_if_owed(gobj) {
74754
+ let priv = gobj.priv;
74755
+ if (!priv.reload_after_write) return 0;
74756
+ priv.reload_after_write = false;
74757
+ if (!transport_in_session(gobj)) {
74758
+ priv.reload_on_open = true;
74759
+ return 0;
74760
+ }
74761
+ return request_model(gobj);
74762
+ }
74763
+ /***************************************************************
74627
74764
  * The record a column form produces.
74628
74765
  *
74629
74766
  * Everything the store needs and the operator did not type is
@@ -74691,7 +74828,7 @@ function go(gobj, treedb_id, topic_name, diagram, mirror) {
74691
74828
  priv.topic_name = topic_name || "";
74692
74829
  priv.diagram = !!diagram;
74693
74830
  let state = "ST_TREEDBS";
74694
- if (!priv.model) state = "ST_LOADING";
74831
+ if (!priv.model) state = "ST_IDLE";
74695
74832
  else if (priv.model.treedbs.length === 0) state = "ST_EMPTY";
74696
74833
  else if (priv.treedb_id && priv.diagram) state = "ST_DIAGRAM";
74697
74834
  else if (priv.treedb_id && priv.topic_name) state = "ST_COLUMNS";
@@ -74718,12 +74855,39 @@ function position_seg(gobj) {
74718
74855
  * empty screen: a shared link outlives the schema it points at.
74719
74856
  ***************************************************************/
74720
74857
  function apply_seg(gobj, seg) {
74858
+ let pos = seg_position(seg);
74859
+ return go(gobj, pos.treedb_id, pos.topic_name, pos.diagram, false);
74860
+ }
74861
+ function seg_position(seg) {
74721
74862
  let parts = String(seg || "").split("/").filter((x) => x.length > 0);
74722
74863
  let treedb_id = parts[0] || "";
74723
74864
  let tail = parts[1] || "";
74724
- if (!treedb_id) return go(gobj, "", "", false, false);
74725
- if (tail === DIAGRAM_SEG) return go(gobj, treedb_id, "", true, false);
74726
- return go(gobj, treedb_id, tail, false, false);
74865
+ if (!treedb_id) return {
74866
+ treedb_id: "",
74867
+ topic_name: "",
74868
+ diagram: false
74869
+ };
74870
+ if (tail === DIAGRAM_SEG) return {
74871
+ treedb_id,
74872
+ topic_name: "",
74873
+ diagram: true
74874
+ };
74875
+ return {
74876
+ treedb_id,
74877
+ topic_name: tail,
74878
+ diagram: false
74879
+ };
74880
+ }
74881
+ /***************************************************************
74882
+ * Where the operator is, taken WITHOUT a screen: there is no model
74883
+ * to draw it on yet, and the load that brings one goes there.
74884
+ ***************************************************************/
74885
+ function adopt_seg(gobj, seg) {
74886
+ let priv = gobj.priv;
74887
+ let pos = seg_position(seg);
74888
+ priv.treedb_id = pos.treedb_id;
74889
+ priv.topic_name = pos.topic_name;
74890
+ priv.diagram = pos.diagram;
74727
74891
  }
74728
74892
  /***************************
74729
74893
  * Actions
@@ -74745,25 +74909,39 @@ function ac_mt_command_answer(gobj, event, kw, src) {
74745
74909
  let kw_command = kw_get_dict(gobj, __command__, "kw", {}, 0);
74746
74910
  let md = is_object(kw_command.__md_command__) ? kw_command.__md_command__ : kw_command;
74747
74911
  if (kw_get_str(gobj, md, "purpose", "", 0) !== PURPOSE) return 0;
74912
+ if (result < 0 && !transport_in_session(gobj) && (gobj_current_state(gobj) === "ST_LOADING" || gobj_current_state(gobj) === "ST_SAVING")) {
74913
+ if (command === "nodes" && is_current_load(gobj, md) || command !== "nodes" && is_current_write(gobj, md)) return transport_dropped(gobj);
74914
+ }
74748
74915
  if (command === "nodes") {
74749
74916
  let topic_name = kw_get_str(gobj, md, "topic_name", "", 0);
74917
+ if (!is_current_load(gobj, md)) {
74918
+ if (result < 0 && kw_get_int(gobj, md, "round", 0, 0) === priv.dropped_round) return 0;
74919
+ log_warning(`${gobj_short_name(gobj)}: 'nodes' of '${topic_name}' answered for a load that is over: ignored`);
74920
+ return 0;
74921
+ }
74750
74922
  priv.pending--;
74751
74923
  if (result < 0) priv.load_error = comment || t("cannot load the schemas");
74752
74924
  else if (priv.records && Array.isArray(priv.records[topic_name])) priv.records[topic_name] = Array.isArray(data) ? data : [];
74753
74925
  if (priv.pending > 0) return 0;
74754
74926
  if (priv.load_error) {
74755
74927
  priv.model = null;
74756
- gobj_change_state(gobj, "ST_IDLE");
74757
- render(gobj);
74758
- return 0;
74928
+ return end_load(gobj);
74759
74929
  }
74760
74930
  priv.order_undo = {};
74761
74931
  build_model(gobj);
74762
74932
  start_measuring(gobj);
74763
- return go(gobj, priv.treedb_id, priv.topic_name, priv.diagram, false);
74933
+ return end_load(gobj);
74764
74934
  }
74765
74935
  if (command === "update-node" || command === "delete-node") {
74766
74936
  let topic_name = kw_get_str(gobj, md, "topic_name", "", 0);
74937
+ if (!is_current_write(gobj, md)) {
74938
+ if (result < 0 && kw_get_int(gobj, md, "write", 0, 0) === priv.dropped_write) return 0;
74939
+ if (result < 0) {
74940
+ log_warning(`${gobj_short_name(gobj)}: '${command}' of '${topic_name}' answered for a write that is over (result ${result}): ignored`);
74941
+ return 0;
74942
+ }
74943
+ return late_write_done(gobj, command, topic_name);
74944
+ }
74767
74945
  if (result < 0) return end_writes(gobj, comment || t("the treedb refused the write"));
74768
74946
  priv.save_wrote = true;
74769
74947
  if (command === "delete-node") forget_record(gobj, topic_name, kw_get_str(gobj, md, "record_id", "", 0));
@@ -74788,6 +74966,35 @@ function ac_mt_command_answer(gobj, event, kw, src) {
74788
74966
  return 0;
74789
74967
  }
74790
74968
  /***************************************************************
74969
+ * A write this view gave up on (a drop, a deadline) answers that
74970
+ * it was DONE: the store holds a change the model does not show.
74971
+ * The model is read again -- now, or when what is in flight ends,
74972
+ * or on the reconnect. A load in flight needs nothing: it was
74973
+ * asked after the write, on the same transport, and reads it.
74974
+ *
74975
+ * Only when the answer reaches this view. A routing adapter that
74976
+ * settled the write on its own deadline (gui_agent's
74977
+ * C_AGENT_TREEDB_LINK) does not deliver its late answer again --
74978
+ * the view was answered once, as failed -- and echoes the node
74979
+ * event instead, which this view does not hear: there, Refresh is
74980
+ * what reads the store.
74981
+ ***************************************************************/
74982
+ function late_write_done(gobj, command, topic_name) {
74983
+ let priv = gobj.priv;
74984
+ let state = gobj_current_state(gobj);
74985
+ log_warning(`${gobj_short_name(gobj)}: '${command}' of '${topic_name}' was given up, and it was DONE: the model is read again`);
74986
+ if (state === "ST_LOADING") return 0;
74987
+ if (state === "ST_SAVING") {
74988
+ priv.reload_after_write = true;
74989
+ return 0;
74990
+ }
74991
+ if (!transport_in_session(gobj)) {
74992
+ priv.reload_on_open = true;
74993
+ return 0;
74994
+ }
74995
+ return request_model(gobj);
74996
+ }
74997
+ /***************************************************************
74791
74998
  * Remember which topic this write belongs to, which is what
74792
74999
  * "its version still has to move" is asked about.
74793
75000
  ***************************************************************/
@@ -74807,7 +75014,8 @@ function mark_written(gobj, topic_name, record) {
74807
75014
  function ac_show(gobj, event, kw, src) {
74808
75015
  let priv = gobj.priv;
74809
75016
  let seg = kw && kw.subpath || "";
74810
- if (gobj_current_state(gobj) === "ST_SAVING") {
75017
+ let state = gobj_current_state(gobj);
75018
+ if (state === "ST_SAVING" || state === "ST_LOADING") {
74811
75019
  priv.pending_seg = seg;
74812
75020
  return 0;
74813
75021
  }
@@ -74823,14 +75031,28 @@ function ac_hide(gobj, event, kw, src) {
74823
75031
  return 0;
74824
75032
  }
74825
75033
  /***************************************************************
74826
- * The session came up or went down. The model stays either way
74827
- * — a schema does not change under you — but a first load may
74828
- * have had no transport to run on.
75034
+ * The session came up or went down. The model stays -- a schema
75035
+ * does not change under you -- unless the drop cut a load or a
75036
+ * write (transport_dropped()): then it is asked again on the way
75037
+ * back, as it is when a first load had no transport to run on.
75038
+ *
75039
+ * Nothing else ends a load or a write the drop cut: their answers
75040
+ * will never come, and waiting in ST_LOADING skipped the reload
75041
+ * itself (M-1 of the independent review of 7.25.4).
74829
75042
  ***************************************************************/
74830
75043
  function ac_transport_state(gobj, event, kw, src) {
74831
75044
  let priv = gobj.priv;
75045
+ let state = gobj_current_state(gobj);
74832
75046
  priv.connected = !!(kw && kw.connected);
74833
- if (priv.connected && !priv.model && gobj_current_state(gobj) !== "ST_LOADING") return request_model(gobj);
75047
+ if (!priv.connected) {
75048
+ if (state === "ST_LOADING" || state === "ST_SAVING") return transport_dropped(gobj);
75049
+ render_toolbar(gobj);
75050
+ return 0;
75051
+ }
75052
+ if ((!priv.model || priv.reload_on_open) && state !== "ST_LOADING" && state !== "ST_SAVING") {
75053
+ priv.reload_on_open = false;
75054
+ return request_model(gobj);
75055
+ }
74834
75056
  render_toolbar(gobj);
74835
75057
  return 0;
74836
75058
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.25.5",
3
+ "version": "7.25.7",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",