@yuneta/gobj-ui 7.25.4 → 7.25.6

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.
@@ -924,6 +924,7 @@ var PRIVATE_DATA$22 = {
924
924
  avatar_nodes: [],
925
925
  translator: null,
926
926
  conn_nodes: [],
927
+ connected: null,
927
928
  active_dropdown: null
928
929
  };
929
930
  var __gclass__$22 = null;
@@ -2555,11 +2556,28 @@ function ac_nav_item_close(gobj, event, kw, src) {
2555
2556
  /***************************************************************
2556
2557
  * FSM
2557
2558
  ***************************************************************/
2559
+ /***************************************************************
2560
+ * EV_CONNECTION_STATE is an OPTIONAL fact: a view that waits for a
2561
+ * backend answer subscribes to it by name. An app that subscribed
2562
+ * to EVERY event of its shell with one attr (`subscriber`) did not
2563
+ * ask for it and does not declare it -- published to it anyway, the
2564
+ * event would be an FSM error in the app on every connection edge,
2565
+ * and adding it to the shell a breaking change for every such app.
2566
+ * So it reaches only the subscribers that declare it. Every other
2567
+ * event of the shell goes out as before.
2568
+ ***************************************************************/
2569
+ function mt_publication_pre_filter(gobj, subs, event, kw) {
2570
+ if (event !== "EV_CONNECTION_STATE") return 1;
2571
+ let subscriber = subs && subs.subscriber;
2572
+ if (!subscriber || !(0, _yuneta_gobj_js.gobj_has_event)(subscriber, event, 0)) return 0;
2573
+ return 1;
2574
+ }
2558
2575
  var gmt$22 = {
2559
2576
  mt_create: mt_create$22,
2560
2577
  mt_start: mt_start$20,
2561
2578
  mt_stop: mt_stop$20,
2562
- mt_destroy: mt_destroy$22
2579
+ mt_destroy: mt_destroy$22,
2580
+ mt_publication_pre_filter
2563
2581
  };
2564
2582
  function create_gclass$22(gclass_name) {
2565
2583
  if (__gclass__$22) {
@@ -2589,7 +2607,8 @@ function create_gclass$22(gclass_name) {
2589
2607
  ["EV_NAV_ITEM_CLOSE", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT | _yuneta_gobj_js.event_flag_t.EVF_PUBLIC_EVENT | _yuneta_gobj_js.event_flag_t.EVF_NO_WARN_SUBS],
2590
2608
  ["EV_ROUTE_REQUESTED", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT | _yuneta_gobj_js.event_flag_t.EVF_PUBLIC_EVENT | _yuneta_gobj_js.event_flag_t.EVF_NO_WARN_SUBS],
2591
2609
  ["EV_ROUTE_CHANGED", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT | _yuneta_gobj_js.event_flag_t.EVF_PUBLIC_EVENT | _yuneta_gobj_js.event_flag_t.EVF_NO_WARN_SUBS],
2592
- ["EV_LANGUAGE_CHANGED", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT | _yuneta_gobj_js.event_flag_t.EVF_PUBLIC_EVENT | _yuneta_gobj_js.event_flag_t.EVF_NO_WARN_SUBS]
2610
+ ["EV_LANGUAGE_CHANGED", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT | _yuneta_gobj_js.event_flag_t.EVF_PUBLIC_EVENT | _yuneta_gobj_js.event_flag_t.EVF_NO_WARN_SUBS],
2611
+ ["EV_CONNECTION_STATE", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT | _yuneta_gobj_js.event_flag_t.EVF_NO_WARN_SUBS]
2593
2612
  ];
2594
2613
  __gclass__$22 = (0, _yuneta_gobj_js.gclass_create)(gclass_name, event_types, states, gmt$22, 0, attrs_table$22, PRIVATE_DATA$22, 0, 0, 0, _yuneta_gobj_js.gclass_flag_t.gcflag_no_check_output_events);
2595
2614
  if (!__gclass__$22) return -1;
@@ -2872,13 +2891,17 @@ function yui_shell_translate(shell_gobj, $el) {
2872
2891
  * is-connected / is-disconnected classes; CSS owns the look.
2873
2892
  ************************************************************/
2874
2893
  function yui_shell_set_connection_state(shell_gobj, connected) {
2894
+ if (!shell_gobj || !(0, _yuneta_gobj_js.is_gobj)(shell_gobj) || (0, _yuneta_gobj_js.gobj_is_destroying)(shell_gobj)) return;
2875
2895
  let priv = shell_gobj.priv;
2876
- if (!priv || !(0, _yuneta_gobj_js.is_array)(priv.conn_nodes)) return;
2896
+ if (!priv) return;
2877
2897
  let on = !!connected;
2878
- for (let $n of priv.conn_nodes) {
2898
+ let changed = priv.connected !== on;
2899
+ priv.connected = on;
2900
+ if ((0, _yuneta_gobj_js.is_array)(priv.conn_nodes)) for (let $n of priv.conn_nodes) {
2879
2901
  $n.classList.toggle("is-connected", on);
2880
2902
  $n.classList.toggle("is-disconnected", !on);
2881
2903
  }
2904
+ if (changed) (0, _yuneta_gobj_js.gobj_publish_event)(shell_gobj, "EV_CONNECTION_STATE", { connected: on });
2882
2905
  }
2883
2906
  /************************************************************
2884
2907
  * Swap the icon of a toolbar item at runtime (host-driven,
@@ -7319,6 +7342,29 @@ function json_diff_rows(doc_a, doc_b) {
7319
7342
  * them, and they must be each other's inverse: a writable time
7320
7343
  * column travels back on EVERY save of the record.
7321
7344
  *
7345
+ * KNOWN LIMIT -- the change of hour. A `datetime-local` holds a
7346
+ * LOCAL WALL time and no offset, and a wall time is not always
7347
+ * one instant:
7348
+ *
7349
+ * - the night the clocks go back (autumn), one hour happens
7350
+ * twice. "02:30" names two instants an hour apart, and
7351
+ * `new Date("YYYY-MM-DDT02:30:00")` answers the EARLIER one
7352
+ * (ECMAScript's "compatible" disambiguation). An epoch in the
7353
+ * SECOND pass of that hour is shown as 02:30 and comes back
7354
+ * one hour earlier: saving the record -- any field of it --
7355
+ * moves a writable time column that holds it by -3600 s.
7356
+ * - the night the clocks go forward (spring), one hour does not
7357
+ * exist. No epoch is shown in it, so nothing read comes back
7358
+ * wrong; a wall time TYPED in it is moved forward by the
7359
+ * browser.
7360
+ *
7361
+ * Not fixable here without the offset, which the input cannot
7362
+ * carry. What is safe: a read-only time column never goes back
7363
+ * (treedb_write_plan.js, col_goes_back_to_treedb), and a time
7364
+ * pkey2 goes back as the record had it, never through this
7365
+ * widget (instance_keys_of). A writable time column edited or
7366
+ * merely saved inside the repeated hour is the exposed case.
7367
+ *
7322
7368
  * Copyright (c) 2026, ArtGins.
7323
7369
  * All Rights Reserved.
7324
7370
  ****************************************************************************/
@@ -23026,30 +23072,46 @@ function yui_shell_show_route_map(shell, opts) {
23026
23072
  * form_writes_in_flight.js
23027
23073
  *
23028
23074
  * The writes of topic forms a C_YUI_TREEDB_TOPICS view has sent
23029
- * and not yet answered, by their `form_write` serial. A form that
23030
- * waits for its answer (form_waits_for_answer) stays open and
23031
- * busy until the host answers, so the host has to answer EVERY
23032
- * way a write ends -- and the transport closing under a write in
23033
- * flight, or a command that never left because there was no
23034
- * session, are two of them that nobody answered: the form stayed
23035
- * busy for ever (N8 of the 2026-09-22 review).
23075
+ * and not yet answered. A form that waits for its answer
23076
+ * (form_waits_for_answer) stays open and busy until the host
23077
+ * answers, so the host has to answer EVERY way a write ends --
23078
+ * and the transport closing under a write in flight, or a
23079
+ * command that never left because there was no session, are two
23080
+ * of them that nobody answered: the form stayed busy for ever
23081
+ * (N8 of the 2026-09-22 review).
23082
+ *
23083
+ * A write is known by its TOPIC and its `form_write` serial,
23084
+ * never by the serial alone: each topic has its own form, and
23085
+ * each form counts its serials from 1. Keyed by the serial, two
23086
+ * forms with a write in flight at once shared the key "1", the
23087
+ * first answer settled both, and the transport closing then
23088
+ * answered only one of them.
23036
23089
  *
23037
23090
  * Copyright (c) 2026, ArtGins.
23038
23091
  * All Rights Reserved.
23039
23092
  ***********************************************************************/
23040
23093
  /************************************************************
23094
+ * The key of one write: `<topic>^<serial>`.
23095
+ ************************************************************/
23096
+ function form_write_key(topic_name, form_write) {
23097
+ return `${topic_name || ""}^${form_write}`;
23098
+ }
23099
+ /************************************************************
23041
23100
  * A write left: remember whose it is
23042
23101
  ************************************************************/
23043
23102
  function track_form_write(in_flight, form_write, topic_name) {
23044
23103
  if (!in_flight || !form_write) return;
23045
- in_flight[form_write] = topic_name || "";
23104
+ in_flight[form_write_key(topic_name, form_write)] = {
23105
+ form_write,
23106
+ topic_name: topic_name || ""
23107
+ };
23046
23108
  }
23047
23109
  /************************************************************
23048
23110
  * A write was answered, one way or the other
23049
23111
  ************************************************************/
23050
- function settle_form_write(in_flight, form_write) {
23112
+ function settle_form_write(in_flight, form_write, topic_name) {
23051
23113
  if (!in_flight || !form_write) return;
23052
- delete in_flight[form_write];
23114
+ delete in_flight[form_write_key(topic_name, form_write)];
23053
23115
  }
23054
23116
  /************************************************************
23055
23117
  * Nothing in flight can be answered any more: take them all,
@@ -23058,12 +23120,13 @@ function settle_form_write(in_flight, form_write) {
23058
23120
  function abandon_form_writes(in_flight) {
23059
23121
  const abandoned = [];
23060
23122
  if (!in_flight) return abandoned;
23061
- Object.keys(in_flight).forEach((serial) => {
23123
+ Object.keys(in_flight).forEach((key) => {
23124
+ const w = in_flight[key];
23062
23125
  abandoned.push({
23063
- form_write: Number(serial),
23064
- topic_name: in_flight[serial]
23126
+ form_write: Number(w.form_write),
23127
+ topic_name: w.topic_name
23065
23128
  });
23066
- delete in_flight[serial];
23129
+ delete in_flight[key];
23067
23130
  });
23068
23131
  return abandoned;
23069
23132
  }
@@ -23635,6 +23698,7 @@ var __gclass__$6 = null;
23635
23698
  ***************************************************************/
23636
23699
  function mt_create$6(gobj) {
23637
23700
  gobj.priv.form_writes = {};
23701
+ gobj.priv.conn_shell = null;
23638
23702
  build_ui$6(gobj);
23639
23703
  let subscriber = (0, _yuneta_gobj_js.gobj_read_pointer_attr)(gobj, "subscriber");
23640
23704
  if (!subscriber) subscriber = (0, _yuneta_gobj_js.gobj_parent)(gobj);
@@ -23644,12 +23708,22 @@ function mt_create$6(gobj) {
23644
23708
  * Framework Method: Start
23645
23709
  ***************************************************************/
23646
23710
  function mt_start$6(gobj) {
23711
+ let shell = yui_shell_of(gobj);
23712
+ if (shell) {
23713
+ (0, _yuneta_gobj_js.gobj_subscribe_event)(shell, "EV_CONNECTION_STATE", {}, gobj);
23714
+ gobj.priv.conn_shell = shell;
23715
+ }
23647
23716
  request_treedb_descs$1(gobj);
23648
23717
  }
23649
23718
  /***************************************************************
23650
23719
  * Framework Method: Stop
23651
23720
  ***************************************************************/
23652
23721
  function mt_stop$6(gobj) {
23722
+ let priv = gobj.priv;
23723
+ if (priv.conn_shell) {
23724
+ if (!(0, _yuneta_gobj_js.gobj_is_destroying)(priv.conn_shell)) (0, _yuneta_gobj_js.gobj_unsubscribe_event)(priv.conn_shell, "EV_CONNECTION_STATE", {}, gobj);
23725
+ priv.conn_shell = null;
23726
+ }
23653
23727
  let shell = yui_shell_of(gobj);
23654
23728
  if (shell) yui_shell_set_sub_routes(shell, (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "base_route"), null);
23655
23729
  close_json_viewer$1(gobj);
@@ -25055,7 +25129,7 @@ function ac_mt_command_answer$2(gobj, event, kw, src) {
25055
25129
  if (command === "update-node" || command === "create-node") {
25056
25130
  let failed_topic = (0, _yuneta_gobj_js.kw_get_str)(gobj, kw_command, "topic_name", "", 0);
25057
25131
  if (failed_topic) (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_REFRESH_TOPIC", { topic_name: failed_topic }, gobj);
25058
- settle_form_write(gobj.priv.form_writes, (0, _yuneta_gobj_js.kw_get_int)(gobj, kw_command, "form_write", 0, 0));
25132
+ settle_form_write(gobj.priv.form_writes, (0, _yuneta_gobj_js.kw_get_int)(gobj, kw_command, "form_write", 0, 0), failed_topic);
25059
25133
  answer_form_write(gobj, get_gobj_formtable(gobj, failed_topic), (0, _yuneta_gobj_js.kw_get_int)(gobj, kw_command, "form_write", 0, 0), false);
25060
25134
  }
25061
25135
  }
@@ -25093,7 +25167,7 @@ function ac_mt_command_answer$2(gobj, event, kw, src) {
25093
25167
  created: command === "create-node",
25094
25168
  command
25095
25169
  });
25096
- settle_form_write(gobj.priv.form_writes, (0, _yuneta_gobj_js.kw_get_int)(gobj, kw_command, "form_write", 0, 0));
25170
+ settle_form_write(gobj.priv.form_writes, (0, _yuneta_gobj_js.kw_get_int)(gobj, kw_command, "form_write", 0, 0), (0, _yuneta_gobj_js.kw_get_str)(gobj, kw_command, "topic_name", "", 0));
25097
25171
  answer_form_write(gobj, get_gobj_formtable(gobj, (0, _yuneta_gobj_js.kw_get_str)(gobj, kw_command, "topic_name", "", 0)), (0, _yuneta_gobj_js.kw_get_int)(gobj, kw_command, "form_write", 0, 0), true);
25098
25172
  break;
25099
25173
  case "delete-node": break;
@@ -25237,11 +25311,19 @@ function ac_back_to_topics(gobj, event, kw, src) {
25237
25311
  return 0;
25238
25312
  }
25239
25313
  /************************************************************
25240
- * The host (C_TREEDB_VIEW) forwards the backend transport edges here so the
25241
- * toolbar can disable the JSON viewers the moment the session drops (and
25242
- * re-enable them on reconnect) — the library view must not subscribe to the
25243
- * C_IEVENT_CLI itself (that forwards the subscription upstream and breaks
25244
- * the session).
25314
+ * A backend transport edge, from either of two places:
25315
+ * - EV_TRANSPORT_STATE, forwarded by a host that keeps one
25316
+ * transport per view (gui_treedb's C_TREEDB_VIEW, gui_agent's
25317
+ * C_AGENT_TREEDB_VIEW);
25318
+ * - EV_CONNECTION_STATE, published by the SHELL when the app tells
25319
+ * it its backend went up or down (yui_shell_set_connection_state),
25320
+ * which is what every single-backend app already does for the
25321
+ * toolbar dot. No host has to remember anything for this one.
25322
+ * Both can arrive for one edge; what is done here is idempotent.
25323
+ * The toolbar disables the JSON viewers the moment the session drops
25324
+ * (and re-enables them on reconnect). The library view must not
25325
+ * subscribe to the C_IEVENT_CLI itself: that forwards the subscription
25326
+ * upstream and breaks the session.
25245
25327
  ************************************************************/
25246
25328
  function ac_transport_state$2(gobj, event, kw, src) {
25247
25329
  let connected = !!(kw && kw.connected);
@@ -25773,6 +25855,11 @@ function create_gclass$6(gclass_name) {
25773
25855
  "EV_TRANSPORT_STATE",
25774
25856
  ac_transport_state$2,
25775
25857
  null
25858
+ ],
25859
+ [
25860
+ "EV_CONNECTION_STATE",
25861
+ ac_transport_state$2,
25862
+ null
25776
25863
  ]
25777
25864
  ]]];
25778
25865
  const event_types = [
@@ -25803,6 +25890,7 @@ function create_gclass$6(gclass_name) {
25803
25890
  ["EV_HIDE", 0],
25804
25891
  ["EV_BACK_TO_TOPICS", 0],
25805
25892
  ["EV_TRANSPORT_STATE", 0],
25893
+ ["EV_CONNECTION_STATE", 0],
25806
25894
  ["EV_TOPIC_SELECTED", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT | _yuneta_gobj_js.event_flag_t.EVF_NO_WARN_SUBS]
25807
25895
  ];
25808
25896
  __gclass__$6 = (0, _yuneta_gobj_js.gclass_create)(gclass_name, event_types, states, gmt$6, 0, attrs_table$6, PRIVATE_DATA$6, 0, 0, 0, 0);
@@ -26520,6 +26608,23 @@ function col_goes_back_to_treedb(desc, col) {
26520
26608
  if (field_desc.is_writable || field_desc.type === "fkey" || field_desc.is_file || col.id === pkey) return true;
26521
26609
  return topic_pkey2s(d).includes(col.id);
26522
26610
  }
26611
+ /************************************************************
26612
+ * The INSTANCE an update of `record` is for: its pkey2 values, as the
26613
+ * record held them when the form was opened, {pkey2: value}.
26614
+ *
26615
+ * They go back as they WERE, never as the form's widget hands them: a
26616
+ * `time` pkey2 is an epoch rendered in a `datetime-local`, and a value
26617
+ * that crossed the widget (seconds dropped, a local wall time read
26618
+ * back, an hour the autumn change names twice) names ANOTHER instance
26619
+ * -- or none, and C_NODE falls back to the primary. The pkey2 is an
26620
+ * address, not a field of the edit.
26621
+ ************************************************************/
26622
+ function instance_keys_of(record, desc) {
26623
+ const keys = {};
26624
+ if (!record || typeof record !== "object") return keys;
26625
+ for (const k of topic_pkey2s(desc || {})) if (Object.prototype.hasOwnProperty.call(record, k) && record[k] !== void 0 && record[k] !== null) keys[k] = record[k];
26626
+ return keys;
26627
+ }
26523
26628
  //#endregion
26524
26629
  //#region src/delete_impact.js
26525
26630
  /***********************************************************************
@@ -28251,6 +28356,7 @@ function open_form_dialog(gobj, mode, record) {
28251
28356
  },
28252
28357
  []
28253
28358
  ]);
28359
+ priv.form_instance = form_mode === "update" ? instance_keys_of(record, desc) : null;
28254
28360
  let form_plan = write_plan(gobj);
28255
28361
  let form = (0, _yuneta_gobj_js.gobj_create_pure_child)("form_" + (0, _yuneta_gobj_js.clean_name)((0, _yuneta_gobj_js.gobj_name)(gobj)), "C_YUI_FORM", {
28256
28362
  template: build_form_template(gobj),
@@ -28319,6 +28425,7 @@ function teardown_form_child(gobj) {
28319
28425
  priv.reading_files = 0;
28320
28426
  priv.awaiting_write = 0;
28321
28427
  priv.form_busy = { busy: false };
28428
+ priv.form_instance = null;
28322
28429
  if (priv.form) {
28323
28430
  if ((0, _yuneta_gobj_js.gobj_is_running)(priv.form)) (0, _yuneta_gobj_js.gobj_stop)(priv.form);
28324
28431
  (0, _yuneta_gobj_js.gobj_destroy)(priv.form);
@@ -29578,6 +29685,7 @@ function publish_treedb_write(gobj, mode, record, jn_files, form_write) {
29578
29685
  topic_name: (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "topic_name"),
29579
29686
  record: strip_read_only_cols(gobj, record)
29580
29687
  };
29688
+ if (mode !== "create" && gobj.priv.form_instance) Object.assign(kw_write.record, gobj.priv.form_instance);
29581
29689
  if (form_write) kw_write.form_write = form_write;
29582
29690
  if (jn_files && Object.keys(jn_files).length > 0) kw_write.record.__files__ = jn_files;
29583
29691
  (0, _yuneta_gobj_js.gobj_publish_event)(gobj, mode === "create" ? "EV_CREATE_RECORD" : "EV_UPDATE_RECORD", kw_write);
@@ -70278,16 +70386,39 @@ function register_c_yui_treedb_schema() {
70278
70386
  * __system__ still differed from the file in use (N13 of the
70279
70387
  * 2026-09-22 review). The host reads that difference from
70280
70388
  * C_TREEDB's `saved-schema` (`draft_changed`, by treedb) and
70281
- * hands it here; the editor applies it to its model.
70389
+ * hands it here; the editor applies it to its model, in place
70390
+ * of what the host said before.
70391
+ *
70392
+ * What `draft_changed` measures depends on the node: the draft
70393
+ * against the SAVED schema on a node newer than yunetas 7.25.3,
70394
+ * and against the file IN USE on a 7.25.3 node -- where a topic saved and
70395
+ * not yet applied is still named, and the editor, which cannot
70396
+ * tell the two apart, shows it as a draft until the Apply.
70282
70397
  *
70283
70398
  * Copyright (c) 2026, ArtGins.
70284
70399
  * All Rights Reserved.
70285
70400
  ***********************************************************************/
70286
70401
  /************************************************************
70402
+ * The topics of `model` the host names as drafts, as
70403
+ * {topic id: true}: `drafts` is {treedb_name: [topic names]}. A
70404
+ * treedb or a topic the model does not hold is ignored.
70405
+ *
70406
+ * What the editor keeps is THIS, apart from what its own session
70407
+ * wrote, and replaced whole on every EV_DRAFTS: the host is the
70408
+ * truth after a reload. Folded into the session's marks, a topic the
70409
+ * host once named stayed a draft after the save that published it.
70410
+ ************************************************************/
70411
+ function host_draft_ids(model, drafts) {
70412
+ const ids = {};
70413
+ mark_host_drafts(ids, model, drafts);
70414
+ return ids;
70415
+ }
70416
+ /************************************************************
70287
70417
  * Mark in `written` the topics of `model` the host names as
70288
70418
  * drafts: `drafts` is {treedb_name: [topic names]}. A treedb or a
70289
70419
  * topic the model does not hold is ignored. Returns how many were
70290
- * marked.
70420
+ * marked. It only ADDS: a caller that must forget what the host
70421
+ * no longer names starts from an empty map (host_draft_ids()).
70291
70422
  ************************************************************/
70292
70423
  function mark_host_drafts(written, model, drafts) {
70293
70424
  let marked = 0;
@@ -71926,8 +72057,9 @@ function write_options(op) {
71926
72057
  * the owner's design of M36 (2026-09-21 review) every write here
71927
72058
  * raised both versions, so an edit half made was already the
71928
72059
  * schema of the next start. The topic list marks what this session
71929
- * wrote and has not saved; the host's EV_REFRESH after a save is
71930
- * what forgets it.
72060
+ * wrote and has not saved (the host's EV_REFRESH after a save is
72061
+ * what forgets it) and what the host says is not saved (EV_DRAFTS,
72062
+ * replaced whole each time it is sent).
71931
72063
  *
71932
72064
  * WHAT ELSE IS HERE, and why each is here and not somewhere else:
71933
72065
  *
@@ -71969,6 +72101,12 @@ function write_options(op) {
71969
72101
  * `update-node` / `delete-node` on the treedb service, and it may
71970
72102
  * be a routing adapter two hops away (the agent console's
71971
72103
  * C_AGENT_TREEDB_LINK) — this view cannot tell, and must not.
72104
+ * A DROP of it (the host's EV_TRANSPORT_STATE, or a failure that
72105
+ * arrives while the transport is out of session) ends the load or
72106
+ * the write it cut, and the model is asked again when the session
72107
+ * is back. Every request carries its round (`round` for a load,
72108
+ * `write` for a write) in `__md_command__`, so an answer of what
72109
+ * was cut is ignored instead of counted in the next one.
71972
72110
  *
71973
72111
  * Copyright (c) 2026, ArtGins.
71974
72112
  * All Rights Reserved.
@@ -72001,18 +72139,23 @@ var PRIVATE_DATA$1 = {
72001
72139
  model: null,
72002
72140
  pending: 0,
72003
72141
  load_error: "",
72142
+ load_round: 0,
72004
72143
  treedb_id: "",
72005
72144
  topic_name: "",
72006
72145
  diagram: false,
72007
72146
  baseline: null,
72008
72147
  written: null,
72148
+ host_drafts: null,
72149
+ host_ids: null,
72009
72150
  order_undo: null,
72010
72151
  diagram_gobj: null,
72011
72152
  dialog: null,
72012
72153
  save_queue: null,
72013
72154
  save_return: "",
72014
72155
  save_wrote: false,
72156
+ write_tag: 0,
72015
72157
  connected: false,
72158
+ reload_on_open: false,
72016
72159
  pending_seg: null,
72017
72160
  import_plan: null,
72018
72161
  import_pane: null,
@@ -72030,6 +72173,7 @@ function mt_create$1(gobj) {
72030
72173
  priv.baseline = {};
72031
72174
  priv.written = {};
72032
72175
  priv.host_drafts = {};
72176
+ priv.host_ids = {};
72033
72177
  priv.save_queue = [];
72034
72178
  build_ui$1(gobj);
72035
72179
  let subscriber = (0, _yuneta_gobj_js.gobj_read_pointer_attr)(gobj, "subscriber");
@@ -72182,6 +72326,7 @@ function request_model(gobj) {
72182
72326
  };
72183
72327
  priv.load_error = "";
72184
72328
  priv.pending = 0;
72329
+ priv.load_round++;
72185
72330
  (0, _yuneta_gobj_js.gobj_change_state)(gobj, "ST_LOADING");
72186
72331
  show_notice(gobj, "loading the schemas");
72187
72332
  for (let topic_name of [
@@ -72191,7 +72336,7 @@ function request_model(gobj) {
72191
72336
  ]) if (remote_command(gobj, "nodes", {
72192
72337
  topic_name,
72193
72338
  options: { list_dict: true }
72194
- }) === 0) priv.pending++;
72339
+ }, { round: priv.load_round }) === 0) priv.pending++;
72195
72340
  if (priv.pending === 0) {
72196
72341
  priv.load_error = (0, i18next.t)("cannot reach the treedb");
72197
72342
  (0, _yuneta_gobj_js.gobj_change_state)(gobj, "ST_IDLE");
@@ -72209,7 +72354,7 @@ function request_model(gobj) {
72209
72354
  * answer says which topic it is about, since the command is
72210
72355
  * `nodes` for all three.
72211
72356
  ***************************************************************/
72212
- function remote_command(gobj, command, kw) {
72357
+ function remote_command(gobj, command, kw, tag) {
72213
72358
  let remote = (0, _yuneta_gobj_js.gobj_read_pointer_attr)(gobj, "gobj_remote_yuno");
72214
72359
  let treedb_name = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "treedb_name");
72215
72360
  if (!remote) {
@@ -72220,11 +72365,11 @@ function remote_command(gobj, command, kw) {
72220
72365
  service: treedb_name,
72221
72366
  treedb_name
72222
72367
  }, kw || {});
72223
- full_kw.__md_command__ = {
72368
+ full_kw.__md_command__ = Object.assign({
72224
72369
  purpose: PURPOSE,
72225
72370
  topic_name: kw && kw.topic_name || "",
72226
72371
  record_id: kw && kw.record_id || ""
72227
- };
72372
+ }, tag || {});
72228
72373
  delete full_kw.record_id;
72229
72374
  let ret = (0, _yuneta_gobj_js.gobj_command)(remote, command, full_kw, gobj);
72230
72375
  if (ret) {
@@ -72269,25 +72414,33 @@ function start_measuring(gobj) {
72269
72414
  let priv = gobj.priv;
72270
72415
  priv.baseline = {};
72271
72416
  priv.written = {};
72417
+ priv.host_ids = {};
72272
72418
  if (!priv.model) return;
72273
72419
  for (let treedb of priv.model.treedbs) for (let topic of treedb.topics) priv.baseline[topic.id] = topic.topic_version;
72274
- mark_host_drafts(priv.written, priv.model, priv.host_drafts);
72420
+ priv.host_ids = host_draft_ids(priv.model, priv.host_drafts);
72275
72421
  }
72276
72422
  /***************************************************************
72277
- * "This topic was written in this session and is not saved."
72423
+ * "This topic holds an edit that is not saved."
72278
72424
  *
72279
72425
  * An edit is a DRAFT: it moves no version and reaches no treedb
72280
72426
  * until the host saves it (C_TREEDB's save-schema, which raises the
72281
- * versions of what changed) -- and the save makes the host send
72282
- * EV_REFRESH, whose reload is what forgets the drafts.
72427
+ * versions of what changed). Two sources say a topic is one, and
72428
+ * they are kept APART because they are forgotten differently:
72429
+ * - `written`: what THIS session wrote since the model was loaded
72430
+ * (a reload, the host's EV_REFRESH, forgets it -- the store says
72431
+ * what is left);
72432
+ * - `host_ids`: what the host said last (EV_DRAFTS), replaced WHOLE
72433
+ * by the next EV_DRAFTS. Folded into `written`, a topic the host
72434
+ * named once stayed a draft after the Save that published it.
72283
72435
  *
72284
72436
  * ONE predicate, because it is asked in three places -- the topic
72285
72437
  * list draws a chip with it, the column view a banner, the export
72286
72438
  * a warning -- and copies of a rule are how they drift.
72287
72439
  ***************************************************************/
72288
- function topic_is_draft(written, topic) {
72289
- if (!topic || !written) return false;
72290
- return written[topic.id] ? true : false;
72440
+ function topic_is_draft(priv, topic) {
72441
+ if (!topic || !priv) return false;
72442
+ if (priv.written && priv.written[topic.id]) return true;
72443
+ return priv.host_ids && priv.host_ids[topic.id] ? true : false;
72291
72444
  }
72292
72445
  /***************************************************************
72293
72446
  * Say what the whole store would be refused for.
@@ -72729,7 +72882,7 @@ function render_topics(gobj, $body) {
72729
72882
  let twice = repeated_names(treedb.topics);
72730
72883
  let $rows = [];
72731
72884
  for (let topic of treedb.topics) {
72732
- let pending = topic_is_draft(priv.written, topic);
72885
+ let pending = topic_is_draft(priv, topic);
72733
72886
  let ambiguous = !!twice[topic.name];
72734
72887
  let address = ambiguous ? topic.id : topic.name;
72735
72888
  let $actions = [];
@@ -73052,7 +73205,7 @@ function render_columns(gobj, $body) {
73052
73205
  ***************************************************************/
73053
73206
  function draft_banner(gobj, topic) {
73054
73207
  let priv = gobj.priv;
73055
- if (!topic_is_draft(priv.written, topic)) return null;
73208
+ if (!topic_is_draft(priv, topic)) return null;
73056
73209
  if (is_readonly(gobj)) return null;
73057
73210
  return (0, _yuneta_gobj_js.createElement2)([
73058
73211
  "div",
@@ -73942,7 +74095,7 @@ function open_report(gobj, findings, treedb_id) {
73942
74095
  ***************************************************************/
73943
74096
  function open_export(gobj, treedb) {
73944
74097
  let priv = gobj.priv;
73945
- let drafts = treedb.topics.some((topic) => topic_is_draft(priv.written, topic));
74098
+ let drafts = treedb.topics.some((topic) => topic_is_draft(priv, topic));
73946
74099
  let c_text = schema_to_c(treedb);
73947
74100
  let json_text = JSON.stringify(schema_to_json(treedb), null, 4);
73948
74101
  let $content = (0, _yuneta_gobj_js.createElement2)([
@@ -74447,12 +74600,61 @@ function run_next_write(gobj) {
74447
74600
  let write = priv.save_queue[0];
74448
74601
  let command = write_command(write.op);
74449
74602
  let options = write_options(write.op);
74603
+ priv.write_tag++;
74450
74604
  if (remote_command(gobj, command, {
74451
74605
  topic_name: write.topic_name,
74452
74606
  record: write.record,
74453
74607
  options,
74454
74608
  record_id: write.record.id || ""
74455
- }) < 0) return end_writes(gobj, (0, i18next.t)("cannot reach the treedb"));
74609
+ }, { write: priv.write_tag }) < 0) return end_writes(gobj, (0, i18next.t)("cannot reach the treedb"));
74610
+ return 0;
74611
+ }
74612
+ /***************************************************************
74613
+ * Does the transport say it is in session? The state the library
74614
+ * reads off every transport (C_IEVENT_CLI and the routing adapters
74615
+ * name it the same).
74616
+ ***************************************************************/
74617
+ function transport_in_session(gobj) {
74618
+ let remote = (0, _yuneta_gobj_js.gobj_read_pointer_attr)(gobj, "gobj_remote_yuno");
74619
+ return !!remote && (0, _yuneta_gobj_js.gobj_current_state)(remote) === "ST_SESSION";
74620
+ }
74621
+ /***************************************************************
74622
+ * Is this answer of the load in flight, or of the write in
74623
+ * flight? Each request carries its round in `__md_command__`, and
74624
+ * one the drop abandoned (or a deadline settled) can still come
74625
+ * back: counted, it ended the next load with the old data or
74626
+ * moved the next write's queue.
74627
+ ***************************************************************/
74628
+ function is_current_load(gobj, md) {
74629
+ return (0, _yuneta_gobj_js.gobj_current_state)(gobj) === "ST_LOADING" && (0, _yuneta_gobj_js.kw_get_int)(gobj, md, "round", 0, 0) === gobj.priv.load_round;
74630
+ }
74631
+ function is_current_write(gobj, md) {
74632
+ return (0, _yuneta_gobj_js.gobj_current_state)(gobj) === "ST_SAVING" && (0, _yuneta_gobj_js.kw_get_int)(gobj, md, "write", 0, 0) === gobj.priv.write_tag;
74633
+ }
74634
+ /***************************************************************
74635
+ * The transport dropped under a load or a write: its answers
74636
+ * will never come. The load goes back to the screen it replaced
74637
+ * (or to "not connected" when there was none), the write ends
74638
+ * with the drop said -- whether it landed is unknown -- and both
74639
+ * mark the model to be asked again when the session is back.
74640
+ * Bumping the round makes an answer of what was cut stale.
74641
+ ***************************************************************/
74642
+ function transport_dropped(gobj) {
74643
+ let priv = gobj.priv;
74644
+ let state = (0, _yuneta_gobj_js.gobj_current_state)(gobj);
74645
+ priv.reload_on_open = true;
74646
+ if (state === "ST_SAVING") {
74647
+ priv.write_tag++;
74648
+ return end_writes(gobj, "the connection dropped during the write");
74649
+ }
74650
+ if (state === "ST_LOADING") {
74651
+ priv.load_round++;
74652
+ priv.pending = 0;
74653
+ priv.load_error = "";
74654
+ if (priv.model) return go(gobj, priv.treedb_id, priv.topic_name, priv.diagram, false);
74655
+ (0, _yuneta_gobj_js.gobj_change_state)(gobj, "ST_IDLE");
74656
+ render(gobj);
74657
+ }
74456
74658
  return 0;
74457
74659
  }
74458
74660
  /***************************************************************
@@ -74607,8 +74809,15 @@ function ac_mt_command_answer(gobj, event, kw, src) {
74607
74809
  let kw_command = (0, _yuneta_gobj_js.kw_get_dict)(gobj, __command__, "kw", {}, 0);
74608
74810
  let md = (0, _yuneta_gobj_js.is_object)(kw_command.__md_command__) ? kw_command.__md_command__ : kw_command;
74609
74811
  if ((0, _yuneta_gobj_js.kw_get_str)(gobj, md, "purpose", "", 0) !== PURPOSE) return 0;
74812
+ if (result < 0 && !transport_in_session(gobj) && ((0, _yuneta_gobj_js.gobj_current_state)(gobj) === "ST_LOADING" || (0, _yuneta_gobj_js.gobj_current_state)(gobj) === "ST_SAVING")) {
74813
+ if (command === "nodes" && is_current_load(gobj, md) || command !== "nodes" && is_current_write(gobj, md)) return transport_dropped(gobj);
74814
+ }
74610
74815
  if (command === "nodes") {
74611
74816
  let topic_name = (0, _yuneta_gobj_js.kw_get_str)(gobj, md, "topic_name", "", 0);
74817
+ if (!is_current_load(gobj, md)) {
74818
+ (0, _yuneta_gobj_js.log_warning)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: 'nodes' of '${topic_name}' answered for a load that is over: ignored`);
74819
+ return 0;
74820
+ }
74612
74821
  priv.pending--;
74613
74822
  if (result < 0) priv.load_error = comment || (0, i18next.t)("cannot load the schemas");
74614
74823
  else if (priv.records && Array.isArray(priv.records[topic_name])) priv.records[topic_name] = Array.isArray(data) ? data : [];
@@ -74626,6 +74835,10 @@ function ac_mt_command_answer(gobj, event, kw, src) {
74626
74835
  }
74627
74836
  if (command === "update-node" || command === "delete-node") {
74628
74837
  let topic_name = (0, _yuneta_gobj_js.kw_get_str)(gobj, md, "topic_name", "", 0);
74838
+ if (!is_current_write(gobj, md)) {
74839
+ (0, _yuneta_gobj_js.log_warning)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: '${command}' of '${topic_name}' answered for a write that is over (result ${result}): ignored`);
74840
+ return 0;
74841
+ }
74629
74842
  if (result < 0) return end_writes(gobj, comment || (0, i18next.t)("the treedb refused the write"));
74630
74843
  priv.save_wrote = true;
74631
74844
  if (command === "delete-node") forget_record(gobj, topic_name, (0, _yuneta_gobj_js.kw_get_str)(gobj, md, "record_id", "", 0));
@@ -74685,14 +74898,28 @@ function ac_hide(gobj, event, kw, src) {
74685
74898
  return 0;
74686
74899
  }
74687
74900
  /***************************************************************
74688
- * The session came up or went down. The model stays either way
74689
- * — a schema does not change under you — but a first load may
74690
- * have had no transport to run on.
74901
+ * The session came up or went down. The model stays -- a schema
74902
+ * does not change under you -- unless the drop cut a load or a
74903
+ * write (transport_dropped()): then it is asked again on the way
74904
+ * back, as it is when a first load had no transport to run on.
74905
+ *
74906
+ * Nothing else ends a load or a write the drop cut: their answers
74907
+ * will never come, and waiting in ST_LOADING skipped the reload
74908
+ * itself (M-1 of the independent review of 7.25.4).
74691
74909
  ***************************************************************/
74692
74910
  function ac_transport_state(gobj, event, kw, src) {
74693
74911
  let priv = gobj.priv;
74912
+ let state = (0, _yuneta_gobj_js.gobj_current_state)(gobj);
74694
74913
  priv.connected = !!(kw && kw.connected);
74695
- if (priv.connected && !priv.model && (0, _yuneta_gobj_js.gobj_current_state)(gobj) !== "ST_LOADING") return request_model(gobj);
74914
+ if (!priv.connected) {
74915
+ if (state === "ST_LOADING" || state === "ST_SAVING") return transport_dropped(gobj);
74916
+ render_toolbar(gobj);
74917
+ return 0;
74918
+ }
74919
+ if ((!priv.model || priv.reload_on_open) && state !== "ST_LOADING" && state !== "ST_SAVING") {
74920
+ priv.reload_on_open = false;
74921
+ return request_model(gobj);
74922
+ }
74696
74923
  render_toolbar(gobj);
74697
74924
  return 0;
74698
74925
  }
@@ -74710,18 +74937,22 @@ function ac_refresh$1(gobj, event, kw, src) {
74710
74937
  }
74711
74938
  /***************************************************************
74712
74939
  * {drafts: {treedb_name: [topic names]}} -- the host says which
74713
- * topics hold a draft in __system__ that the file in use does not
74714
- * have (C_TREEDB's saved-schema, `draft_changed`). The mark of a
74715
- * write lived in this session's memory only, so a reload of the
74716
- * page, a reconnect or a refresh showed no draft while __system__
74717
- * still differed from the file (N13 of the 2026-09-22 review).
74718
- * Replaces what the host said before; what this session wrote
74719
- * stays marked.
74940
+ * topics hold a draft in __system__ that is not saved (C_TREEDB's
74941
+ * saved-schema, `draft_changed`). The mark of a write lived in this
74942
+ * session's memory only, so a reload of the page, a reconnect or a
74943
+ * refresh showed no draft while __system__ still differed (N13 of
74944
+ * the 2026-09-22 review).
74945
+ *
74946
+ * REPLACES what the host said before -- the host is the truth after
74947
+ * a reload, and a Save is exactly the host saying "none any more".
74948
+ * It only ever added, and a topic stayed a draft after the Save that
74949
+ * published it (M1 of the 2026-09-23 review). What this session wrote
74950
+ * stays marked: it is kept apart (see topic_is_draft()).
74720
74951
  ***************************************************************/
74721
74952
  function ac_drafts(gobj, event, kw, src) {
74722
74953
  let priv = gobj.priv;
74723
74954
  priv.host_drafts = kw && kw.drafts && typeof kw.drafts === "object" ? kw.drafts : {};
74724
- mark_host_drafts(priv.written, priv.model, priv.host_drafts);
74955
+ priv.host_ids = host_draft_ids(priv.model, priv.host_drafts);
74725
74956
  if (priv.model) render(gobj);
74726
74957
  return 0;
74727
74958
  }