@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.
@@ -1,4 +1,4 @@
1
- import { SDATA, SDATA_END, clean_name, createElement2, createOneHtml, create_template_record, data_type_t, debounce, delete_from_list, duplicate_objects, empty_json, empty_string, escapeHtml, event_flag_t, flat_diff, gclass_create, gclass_find_by_name, gclass_flag_t, getPositionRelativeToBody, gobj_bottom_gobj, gobj_change_state, gobj_command, gobj_create, gobj_create_pure_child, gobj_create_service, gobj_current_state, gobj_destroy, gobj_find_child, gobj_find_service, gobj_full_name, gobj_gclass_name, gobj_get_gclass_trace_level2, gobj_global_trace_level, gobj_global_trace_no_level, gobj_is_destroying, gobj_is_playing, gobj_is_pure_child, gobj_is_running, gobj_is_service, gobj_is_volatil, gobj_match_children, gobj_name, gobj_parent, gobj_post_event, gobj_publish_event, gobj_read_attr, gobj_read_bool_attr, gobj_read_integer_attr, gobj_read_pointer_attr, gobj_read_str_attr, gobj_save_persistent_attrs, gobj_send_event, gobj_set_trace_machine_format, gobj_short_name, gobj_start, gobj_start_tree, gobj_stop, gobj_stop_children, gobj_subscribe_event, gobj_unsubscribe_event, gobj_write_attr, gobj_write_bool_attr, gobj_write_integer_attr, gobj_write_str_attr, gobj_yuno, gobj_yuno_name, is_array, is_boolean, is_date, is_gobj, is_null, is_number, is_object, is_string, json2flat, json_deep_copy, json_is_identical, json_object_size, json_object_update, json_object_update_missing, json_size, kw_clone_by_keys, kw_flag_t, kw_get_dict, kw_get_dict_value, kw_get_int, kw_get_local_storage_value, kw_get_str, kw_has_key, kw_set_dict_value, kw_set_local_storage_value, kwid_find_one_record, kwid_new_dict, log_error, log_warning, msg_iev_get_stack, parseBoolean, parseSVG, refresh_language, safeSrc, sdata_flag_t, set_console_log_enabled, set_log_callback, sprintf, str_in_list, template_get_field_desc, trace_level_t, trace_msg, treedb_decoder_fkey, treedb_encoder_fkey, treedb_get_field_desc, treedb_hook_data_size } from "@yuneta/gobj-js";
1
+ import { SDATA, SDATA_END, clean_name, createElement2, createOneHtml, create_template_record, data_type_t, debounce, delete_from_list, duplicate_objects, empty_json, empty_string, escapeHtml, event_flag_t, flat_diff, gclass_create, gclass_find_by_name, gclass_flag_t, getPositionRelativeToBody, gobj_bottom_gobj, gobj_change_state, gobj_command, gobj_create, gobj_create_pure_child, gobj_create_service, gobj_current_state, gobj_destroy, gobj_find_child, gobj_find_service, gobj_full_name, gobj_gclass_name, gobj_get_gclass_trace_level2, gobj_global_trace_level, gobj_global_trace_no_level, gobj_has_event, gobj_is_destroying, gobj_is_playing, gobj_is_pure_child, gobj_is_running, gobj_is_service, gobj_is_volatil, gobj_match_children, gobj_name, gobj_parent, gobj_post_event, gobj_publish_event, gobj_read_attr, gobj_read_bool_attr, gobj_read_integer_attr, gobj_read_pointer_attr, gobj_read_str_attr, gobj_save_persistent_attrs, gobj_send_event, gobj_set_trace_machine_format, gobj_short_name, gobj_start, gobj_start_tree, gobj_stop, gobj_stop_children, gobj_subscribe_event, gobj_unsubscribe_event, gobj_write_attr, gobj_write_bool_attr, gobj_write_integer_attr, gobj_write_str_attr, gobj_yuno, gobj_yuno_name, is_array, is_boolean, is_date, is_gobj, is_null, is_number, is_object, is_string, json2flat, json_deep_copy, json_is_identical, json_object_size, json_object_update, json_object_update_missing, json_size, kw_clone_by_keys, kw_flag_t, kw_get_dict, kw_get_dict_value, kw_get_int, kw_get_local_storage_value, kw_get_str, kw_has_key, kw_set_dict_value, kw_set_local_storage_value, kwid_find_one_record, kwid_new_dict, log_error, log_warning, msg_iev_get_stack, parseBoolean, parseSVG, refresh_language, safeSrc, sdata_flag_t, set_console_log_enabled, set_log_callback, sprintf, str_in_list, template_get_field_desc, trace_level_t, trace_msg, treedb_decoder_fkey, treedb_encoder_fkey, treedb_get_field_desc, treedb_hook_data_size } from "@yuneta/gobj-js";
2
2
  import i18next, { t } from "i18next";
3
3
  import TomSelect from "tom-select";
4
4
  import { createJSONEditor } from "vanilla-jsoneditor";
@@ -919,6 +919,7 @@ var PRIVATE_DATA$22 = {
919
919
  avatar_nodes: [],
920
920
  translator: null,
921
921
  conn_nodes: [],
922
+ connected: null,
922
923
  active_dropdown: null
923
924
  };
924
925
  var __gclass__$22 = null;
@@ -2550,11 +2551,28 @@ function ac_nav_item_close(gobj, event, kw, src) {
2550
2551
  /***************************************************************
2551
2552
  * FSM
2552
2553
  ***************************************************************/
2554
+ /***************************************************************
2555
+ * EV_CONNECTION_STATE is an OPTIONAL fact: a view that waits for a
2556
+ * backend answer subscribes to it by name. An app that subscribed
2557
+ * to EVERY event of its shell with one attr (`subscriber`) did not
2558
+ * ask for it and does not declare it -- published to it anyway, the
2559
+ * event would be an FSM error in the app on every connection edge,
2560
+ * and adding it to the shell a breaking change for every such app.
2561
+ * So it reaches only the subscribers that declare it. Every other
2562
+ * event of the shell goes out as before.
2563
+ ***************************************************************/
2564
+ function mt_publication_pre_filter(gobj, subs, event, kw) {
2565
+ if (event !== "EV_CONNECTION_STATE") return 1;
2566
+ let subscriber = subs && subs.subscriber;
2567
+ if (!subscriber || !gobj_has_event(subscriber, event, 0)) return 0;
2568
+ return 1;
2569
+ }
2553
2570
  var gmt$22 = {
2554
2571
  mt_create: mt_create$22,
2555
2572
  mt_start: mt_start$20,
2556
2573
  mt_stop: mt_stop$20,
2557
- mt_destroy: mt_destroy$22
2574
+ mt_destroy: mt_destroy$22,
2575
+ mt_publication_pre_filter
2558
2576
  };
2559
2577
  function create_gclass$22(gclass_name) {
2560
2578
  if (__gclass__$22) {
@@ -2584,7 +2602,8 @@ function create_gclass$22(gclass_name) {
2584
2602
  ["EV_NAV_ITEM_CLOSE", event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_PUBLIC_EVENT | event_flag_t.EVF_NO_WARN_SUBS],
2585
2603
  ["EV_ROUTE_REQUESTED", event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_PUBLIC_EVENT | event_flag_t.EVF_NO_WARN_SUBS],
2586
2604
  ["EV_ROUTE_CHANGED", event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_PUBLIC_EVENT | event_flag_t.EVF_NO_WARN_SUBS],
2587
- ["EV_LANGUAGE_CHANGED", event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_PUBLIC_EVENT | event_flag_t.EVF_NO_WARN_SUBS]
2605
+ ["EV_LANGUAGE_CHANGED", event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_PUBLIC_EVENT | event_flag_t.EVF_NO_WARN_SUBS],
2606
+ ["EV_CONNECTION_STATE", event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_NO_WARN_SUBS]
2588
2607
  ];
2589
2608
  __gclass__$22 = gclass_create(gclass_name, event_types, states, gmt$22, 0, attrs_table$22, PRIVATE_DATA$22, 0, 0, 0, gclass_flag_t.gcflag_no_check_output_events);
2590
2609
  if (!__gclass__$22) return -1;
@@ -2867,13 +2886,17 @@ function yui_shell_translate(shell_gobj, $el) {
2867
2886
  * is-connected / is-disconnected classes; CSS owns the look.
2868
2887
  ************************************************************/
2869
2888
  function yui_shell_set_connection_state(shell_gobj, connected) {
2889
+ if (!shell_gobj || !is_gobj(shell_gobj) || gobj_is_destroying(shell_gobj)) return;
2870
2890
  let priv = shell_gobj.priv;
2871
- if (!priv || !is_array(priv.conn_nodes)) return;
2891
+ if (!priv) return;
2872
2892
  let on = !!connected;
2873
- for (let $n of priv.conn_nodes) {
2893
+ let changed = priv.connected !== on;
2894
+ priv.connected = on;
2895
+ if (is_array(priv.conn_nodes)) for (let $n of priv.conn_nodes) {
2874
2896
  $n.classList.toggle("is-connected", on);
2875
2897
  $n.classList.toggle("is-disconnected", !on);
2876
2898
  }
2899
+ if (changed) gobj_publish_event(shell_gobj, "EV_CONNECTION_STATE", { connected: on });
2877
2900
  }
2878
2901
  /************************************************************
2879
2902
  * Swap the icon of a toolbar item at runtime (host-driven,
@@ -7314,6 +7337,29 @@ function json_diff_rows(doc_a, doc_b) {
7314
7337
  * them, and they must be each other's inverse: a writable time
7315
7338
  * column travels back on EVERY save of the record.
7316
7339
  *
7340
+ * KNOWN LIMIT -- the change of hour. A `datetime-local` holds a
7341
+ * LOCAL WALL time and no offset, and a wall time is not always
7342
+ * one instant:
7343
+ *
7344
+ * - the night the clocks go back (autumn), one hour happens
7345
+ * twice. "02:30" names two instants an hour apart, and
7346
+ * `new Date("YYYY-MM-DDT02:30:00")` answers the EARLIER one
7347
+ * (ECMAScript's "compatible" disambiguation). An epoch in the
7348
+ * SECOND pass of that hour is shown as 02:30 and comes back
7349
+ * one hour earlier: saving the record -- any field of it --
7350
+ * moves a writable time column that holds it by -3600 s.
7351
+ * - the night the clocks go forward (spring), one hour does not
7352
+ * exist. No epoch is shown in it, so nothing read comes back
7353
+ * wrong; a wall time TYPED in it is moved forward by the
7354
+ * browser.
7355
+ *
7356
+ * Not fixable here without the offset, which the input cannot
7357
+ * carry. What is safe: a read-only time column never goes back
7358
+ * (treedb_write_plan.js, col_goes_back_to_treedb), and a time
7359
+ * pkey2 goes back as the record had it, never through this
7360
+ * widget (instance_keys_of). A writable time column edited or
7361
+ * merely saved inside the repeated hour is the exposed case.
7362
+ *
7317
7363
  * Copyright (c) 2026, ArtGins.
7318
7364
  * All Rights Reserved.
7319
7365
  ****************************************************************************/
@@ -23021,30 +23067,46 @@ function yui_shell_show_route_map(shell, opts) {
23021
23067
  * form_writes_in_flight.js
23022
23068
  *
23023
23069
  * The writes of topic forms a C_YUI_TREEDB_TOPICS view has sent
23024
- * and not yet answered, by their `form_write` serial. A form that
23025
- * waits for its answer (form_waits_for_answer) stays open and
23026
- * busy until the host answers, so the host has to answer EVERY
23027
- * way a write ends -- and the transport closing under a write in
23028
- * flight, or a command that never left because there was no
23029
- * session, are two of them that nobody answered: the form stayed
23030
- * busy for ever (N8 of the 2026-09-22 review).
23070
+ * and not yet answered. A form that waits for its answer
23071
+ * (form_waits_for_answer) stays open and busy until the host
23072
+ * answers, so the host has to answer EVERY way a write ends --
23073
+ * and the transport closing under a write in flight, or a
23074
+ * command that never left because there was no session, are two
23075
+ * of them that nobody answered: the form stayed busy for ever
23076
+ * (N8 of the 2026-09-22 review).
23077
+ *
23078
+ * A write is known by its TOPIC and its `form_write` serial,
23079
+ * never by the serial alone: each topic has its own form, and
23080
+ * each form counts its serials from 1. Keyed by the serial, two
23081
+ * forms with a write in flight at once shared the key "1", the
23082
+ * first answer settled both, and the transport closing then
23083
+ * answered only one of them.
23031
23084
  *
23032
23085
  * Copyright (c) 2026, ArtGins.
23033
23086
  * All Rights Reserved.
23034
23087
  ***********************************************************************/
23035
23088
  /************************************************************
23089
+ * The key of one write: `<topic>^<serial>`.
23090
+ ************************************************************/
23091
+ function form_write_key(topic_name, form_write) {
23092
+ return `${topic_name || ""}^${form_write}`;
23093
+ }
23094
+ /************************************************************
23036
23095
  * A write left: remember whose it is
23037
23096
  ************************************************************/
23038
23097
  function track_form_write(in_flight, form_write, topic_name) {
23039
23098
  if (!in_flight || !form_write) return;
23040
- in_flight[form_write] = topic_name || "";
23099
+ in_flight[form_write_key(topic_name, form_write)] = {
23100
+ form_write,
23101
+ topic_name: topic_name || ""
23102
+ };
23041
23103
  }
23042
23104
  /************************************************************
23043
23105
  * A write was answered, one way or the other
23044
23106
  ************************************************************/
23045
- function settle_form_write(in_flight, form_write) {
23107
+ function settle_form_write(in_flight, form_write, topic_name) {
23046
23108
  if (!in_flight || !form_write) return;
23047
- delete in_flight[form_write];
23109
+ delete in_flight[form_write_key(topic_name, form_write)];
23048
23110
  }
23049
23111
  /************************************************************
23050
23112
  * Nothing in flight can be answered any more: take them all,
@@ -23053,12 +23115,13 @@ function settle_form_write(in_flight, form_write) {
23053
23115
  function abandon_form_writes(in_flight) {
23054
23116
  const abandoned = [];
23055
23117
  if (!in_flight) return abandoned;
23056
- Object.keys(in_flight).forEach((serial) => {
23118
+ Object.keys(in_flight).forEach((key) => {
23119
+ const w = in_flight[key];
23057
23120
  abandoned.push({
23058
- form_write: Number(serial),
23059
- topic_name: in_flight[serial]
23121
+ form_write: Number(w.form_write),
23122
+ topic_name: w.topic_name
23060
23123
  });
23061
- delete in_flight[serial];
23124
+ delete in_flight[key];
23062
23125
  });
23063
23126
  return abandoned;
23064
23127
  }
@@ -23630,6 +23693,7 @@ var __gclass__$6 = null;
23630
23693
  ***************************************************************/
23631
23694
  function mt_create$6(gobj) {
23632
23695
  gobj.priv.form_writes = {};
23696
+ gobj.priv.conn_shell = null;
23633
23697
  build_ui$6(gobj);
23634
23698
  let subscriber = gobj_read_pointer_attr(gobj, "subscriber");
23635
23699
  if (!subscriber) subscriber = gobj_parent(gobj);
@@ -23639,12 +23703,22 @@ function mt_create$6(gobj) {
23639
23703
  * Framework Method: Start
23640
23704
  ***************************************************************/
23641
23705
  function mt_start$6(gobj) {
23706
+ let shell = yui_shell_of(gobj);
23707
+ if (shell) {
23708
+ gobj_subscribe_event(shell, "EV_CONNECTION_STATE", {}, gobj);
23709
+ gobj.priv.conn_shell = shell;
23710
+ }
23642
23711
  request_treedb_descs$1(gobj);
23643
23712
  }
23644
23713
  /***************************************************************
23645
23714
  * Framework Method: Stop
23646
23715
  ***************************************************************/
23647
23716
  function mt_stop$6(gobj) {
23717
+ let priv = gobj.priv;
23718
+ if (priv.conn_shell) {
23719
+ if (!gobj_is_destroying(priv.conn_shell)) gobj_unsubscribe_event(priv.conn_shell, "EV_CONNECTION_STATE", {}, gobj);
23720
+ priv.conn_shell = null;
23721
+ }
23648
23722
  let shell = yui_shell_of(gobj);
23649
23723
  if (shell) yui_shell_set_sub_routes(shell, gobj_read_str_attr(gobj, "base_route"), null);
23650
23724
  close_json_viewer$1(gobj);
@@ -25050,7 +25124,7 @@ function ac_mt_command_answer$2(gobj, event, kw, src) {
25050
25124
  if (command === "update-node" || command === "create-node") {
25051
25125
  let failed_topic = kw_get_str(gobj, kw_command, "topic_name", "", 0);
25052
25126
  if (failed_topic) gobj_send_event(gobj, "EV_REFRESH_TOPIC", { topic_name: failed_topic }, gobj);
25053
- settle_form_write(gobj.priv.form_writes, kw_get_int(gobj, kw_command, "form_write", 0, 0));
25127
+ settle_form_write(gobj.priv.form_writes, kw_get_int(gobj, kw_command, "form_write", 0, 0), failed_topic);
25054
25128
  answer_form_write(gobj, get_gobj_formtable(gobj, failed_topic), kw_get_int(gobj, kw_command, "form_write", 0, 0), false);
25055
25129
  }
25056
25130
  }
@@ -25088,7 +25162,7 @@ function ac_mt_command_answer$2(gobj, event, kw, src) {
25088
25162
  created: command === "create-node",
25089
25163
  command
25090
25164
  });
25091
- settle_form_write(gobj.priv.form_writes, kw_get_int(gobj, kw_command, "form_write", 0, 0));
25165
+ settle_form_write(gobj.priv.form_writes, kw_get_int(gobj, kw_command, "form_write", 0, 0), kw_get_str(gobj, kw_command, "topic_name", "", 0));
25092
25166
  answer_form_write(gobj, get_gobj_formtable(gobj, kw_get_str(gobj, kw_command, "topic_name", "", 0)), kw_get_int(gobj, kw_command, "form_write", 0, 0), true);
25093
25167
  break;
25094
25168
  case "delete-node": break;
@@ -25232,11 +25306,19 @@ function ac_back_to_topics(gobj, event, kw, src) {
25232
25306
  return 0;
25233
25307
  }
25234
25308
  /************************************************************
25235
- * The host (C_TREEDB_VIEW) forwards the backend transport edges here so the
25236
- * toolbar can disable the JSON viewers the moment the session drops (and
25237
- * re-enable them on reconnect) — the library view must not subscribe to the
25238
- * C_IEVENT_CLI itself (that forwards the subscription upstream and breaks
25239
- * the session).
25309
+ * A backend transport edge, from either of two places:
25310
+ * - EV_TRANSPORT_STATE, forwarded by a host that keeps one
25311
+ * transport per view (gui_treedb's C_TREEDB_VIEW, gui_agent's
25312
+ * C_AGENT_TREEDB_VIEW);
25313
+ * - EV_CONNECTION_STATE, published by the SHELL when the app tells
25314
+ * it its backend went up or down (yui_shell_set_connection_state),
25315
+ * which is what every single-backend app already does for the
25316
+ * toolbar dot. No host has to remember anything for this one.
25317
+ * Both can arrive for one edge; what is done here is idempotent.
25318
+ * The toolbar disables the JSON viewers the moment the session drops
25319
+ * (and re-enables them on reconnect). The library view must not
25320
+ * subscribe to the C_IEVENT_CLI itself: that forwards the subscription
25321
+ * upstream and breaks the session.
25240
25322
  ************************************************************/
25241
25323
  function ac_transport_state$2(gobj, event, kw, src) {
25242
25324
  let connected = !!(kw && kw.connected);
@@ -25768,6 +25850,11 @@ function create_gclass$6(gclass_name) {
25768
25850
  "EV_TRANSPORT_STATE",
25769
25851
  ac_transport_state$2,
25770
25852
  null
25853
+ ],
25854
+ [
25855
+ "EV_CONNECTION_STATE",
25856
+ ac_transport_state$2,
25857
+ null
25771
25858
  ]
25772
25859
  ]]];
25773
25860
  const event_types = [
@@ -25798,6 +25885,7 @@ function create_gclass$6(gclass_name) {
25798
25885
  ["EV_HIDE", 0],
25799
25886
  ["EV_BACK_TO_TOPICS", 0],
25800
25887
  ["EV_TRANSPORT_STATE", 0],
25888
+ ["EV_CONNECTION_STATE", 0],
25801
25889
  ["EV_TOPIC_SELECTED", event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_NO_WARN_SUBS]
25802
25890
  ];
25803
25891
  __gclass__$6 = gclass_create(gclass_name, event_types, states, gmt$6, 0, attrs_table$6, PRIVATE_DATA$6, 0, 0, 0, 0);
@@ -26515,6 +26603,23 @@ function col_goes_back_to_treedb(desc, col) {
26515
26603
  if (field_desc.is_writable || field_desc.type === "fkey" || field_desc.is_file || col.id === pkey) return true;
26516
26604
  return topic_pkey2s(d).includes(col.id);
26517
26605
  }
26606
+ /************************************************************
26607
+ * The INSTANCE an update of `record` is for: its pkey2 values, as the
26608
+ * record held them when the form was opened, {pkey2: value}.
26609
+ *
26610
+ * They go back as they WERE, never as the form's widget hands them: a
26611
+ * `time` pkey2 is an epoch rendered in a `datetime-local`, and a value
26612
+ * that crossed the widget (seconds dropped, a local wall time read
26613
+ * back, an hour the autumn change names twice) names ANOTHER instance
26614
+ * -- or none, and C_NODE falls back to the primary. The pkey2 is an
26615
+ * address, not a field of the edit.
26616
+ ************************************************************/
26617
+ function instance_keys_of(record, desc) {
26618
+ const keys = {};
26619
+ if (!record || typeof record !== "object") return keys;
26620
+ 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];
26621
+ return keys;
26622
+ }
26518
26623
  //#endregion
26519
26624
  //#region src/delete_impact.js
26520
26625
  /***********************************************************************
@@ -28246,6 +28351,7 @@ function open_form_dialog(gobj, mode, record) {
28246
28351
  },
28247
28352
  []
28248
28353
  ]);
28354
+ priv.form_instance = form_mode === "update" ? instance_keys_of(record, desc) : null;
28249
28355
  let form_plan = write_plan(gobj);
28250
28356
  let form = gobj_create_pure_child("form_" + clean_name(gobj_name(gobj)), "C_YUI_FORM", {
28251
28357
  template: build_form_template(gobj),
@@ -28314,6 +28420,7 @@ function teardown_form_child(gobj) {
28314
28420
  priv.reading_files = 0;
28315
28421
  priv.awaiting_write = 0;
28316
28422
  priv.form_busy = { busy: false };
28423
+ priv.form_instance = null;
28317
28424
  if (priv.form) {
28318
28425
  if (gobj_is_running(priv.form)) gobj_stop(priv.form);
28319
28426
  gobj_destroy(priv.form);
@@ -29573,6 +29680,7 @@ function publish_treedb_write(gobj, mode, record, jn_files, form_write) {
29573
29680
  topic_name: gobj_read_str_attr(gobj, "topic_name"),
29574
29681
  record: strip_read_only_cols(gobj, record)
29575
29682
  };
29683
+ if (mode !== "create" && gobj.priv.form_instance) Object.assign(kw_write.record, gobj.priv.form_instance);
29576
29684
  if (form_write) kw_write.form_write = form_write;
29577
29685
  if (jn_files && Object.keys(jn_files).length > 0) kw_write.record.__files__ = jn_files;
29578
29686
  gobj_publish_event(gobj, mode === "create" ? "EV_CREATE_RECORD" : "EV_UPDATE_RECORD", kw_write);
@@ -70273,16 +70381,39 @@ function register_c_yui_treedb_schema() {
70273
70381
  * __system__ still differed from the file in use (N13 of the
70274
70382
  * 2026-09-22 review). The host reads that difference from
70275
70383
  * C_TREEDB's `saved-schema` (`draft_changed`, by treedb) and
70276
- * hands it here; the editor applies it to its model.
70384
+ * hands it here; the editor applies it to its model, in place
70385
+ * of what the host said before.
70386
+ *
70387
+ * What `draft_changed` measures depends on the node: the draft
70388
+ * against the SAVED schema on a node newer than yunetas 7.25.3,
70389
+ * and against the file IN USE on a 7.25.3 node -- where a topic saved and
70390
+ * not yet applied is still named, and the editor, which cannot
70391
+ * tell the two apart, shows it as a draft until the Apply.
70277
70392
  *
70278
70393
  * Copyright (c) 2026, ArtGins.
70279
70394
  * All Rights Reserved.
70280
70395
  ***********************************************************************/
70281
70396
  /************************************************************
70397
+ * The topics of `model` the host names as drafts, as
70398
+ * {topic id: true}: `drafts` is {treedb_name: [topic names]}. A
70399
+ * treedb or a topic the model does not hold is ignored.
70400
+ *
70401
+ * What the editor keeps is THIS, apart from what its own session
70402
+ * wrote, and replaced whole on every EV_DRAFTS: the host is the
70403
+ * truth after a reload. Folded into the session's marks, a topic the
70404
+ * host once named stayed a draft after the save that published it.
70405
+ ************************************************************/
70406
+ function host_draft_ids(model, drafts) {
70407
+ const ids = {};
70408
+ mark_host_drafts(ids, model, drafts);
70409
+ return ids;
70410
+ }
70411
+ /************************************************************
70282
70412
  * Mark in `written` the topics of `model` the host names as
70283
70413
  * drafts: `drafts` is {treedb_name: [topic names]}. A treedb or a
70284
70414
  * topic the model does not hold is ignored. Returns how many were
70285
- * marked.
70415
+ * marked. It only ADDS: a caller that must forget what the host
70416
+ * no longer names starts from an empty map (host_draft_ids()).
70286
70417
  ************************************************************/
70287
70418
  function mark_host_drafts(written, model, drafts) {
70288
70419
  let marked = 0;
@@ -71921,8 +72052,9 @@ function write_options(op) {
71921
72052
  * the owner's design of M36 (2026-09-21 review) every write here
71922
72053
  * raised both versions, so an edit half made was already the
71923
72054
  * schema of the next start. The topic list marks what this session
71924
- * wrote and has not saved; the host's EV_REFRESH after a save is
71925
- * what forgets it.
72055
+ * wrote and has not saved (the host's EV_REFRESH after a save is
72056
+ * what forgets it) and what the host says is not saved (EV_DRAFTS,
72057
+ * replaced whole each time it is sent).
71926
72058
  *
71927
72059
  * WHAT ELSE IS HERE, and why each is here and not somewhere else:
71928
72060
  *
@@ -71964,6 +72096,12 @@ function write_options(op) {
71964
72096
  * `update-node` / `delete-node` on the treedb service, and it may
71965
72097
  * be a routing adapter two hops away (the agent console's
71966
72098
  * C_AGENT_TREEDB_LINK) — this view cannot tell, and must not.
72099
+ * A DROP of it (the host's EV_TRANSPORT_STATE, or a failure that
72100
+ * arrives while the transport is out of session) ends the load or
72101
+ * the write it cut, and the model is asked again when the session
72102
+ * is back. Every request carries its round (`round` for a load,
72103
+ * `write` for a write) in `__md_command__`, so an answer of what
72104
+ * was cut is ignored instead of counted in the next one.
71967
72105
  *
71968
72106
  * Copyright (c) 2026, ArtGins.
71969
72107
  * All Rights Reserved.
@@ -71996,18 +72134,23 @@ var PRIVATE_DATA$1 = {
71996
72134
  model: null,
71997
72135
  pending: 0,
71998
72136
  load_error: "",
72137
+ load_round: 0,
71999
72138
  treedb_id: "",
72000
72139
  topic_name: "",
72001
72140
  diagram: false,
72002
72141
  baseline: null,
72003
72142
  written: null,
72143
+ host_drafts: null,
72144
+ host_ids: null,
72004
72145
  order_undo: null,
72005
72146
  diagram_gobj: null,
72006
72147
  dialog: null,
72007
72148
  save_queue: null,
72008
72149
  save_return: "",
72009
72150
  save_wrote: false,
72151
+ write_tag: 0,
72010
72152
  connected: false,
72153
+ reload_on_open: false,
72011
72154
  pending_seg: null,
72012
72155
  import_plan: null,
72013
72156
  import_pane: null,
@@ -72025,6 +72168,7 @@ function mt_create$1(gobj) {
72025
72168
  priv.baseline = {};
72026
72169
  priv.written = {};
72027
72170
  priv.host_drafts = {};
72171
+ priv.host_ids = {};
72028
72172
  priv.save_queue = [];
72029
72173
  build_ui$1(gobj);
72030
72174
  let subscriber = gobj_read_pointer_attr(gobj, "subscriber");
@@ -72177,6 +72321,7 @@ function request_model(gobj) {
72177
72321
  };
72178
72322
  priv.load_error = "";
72179
72323
  priv.pending = 0;
72324
+ priv.load_round++;
72180
72325
  gobj_change_state(gobj, "ST_LOADING");
72181
72326
  show_notice(gobj, "loading the schemas");
72182
72327
  for (let topic_name of [
@@ -72186,7 +72331,7 @@ function request_model(gobj) {
72186
72331
  ]) if (remote_command(gobj, "nodes", {
72187
72332
  topic_name,
72188
72333
  options: { list_dict: true }
72189
- }) === 0) priv.pending++;
72334
+ }, { round: priv.load_round }) === 0) priv.pending++;
72190
72335
  if (priv.pending === 0) {
72191
72336
  priv.load_error = t("cannot reach the treedb");
72192
72337
  gobj_change_state(gobj, "ST_IDLE");
@@ -72204,7 +72349,7 @@ function request_model(gobj) {
72204
72349
  * answer says which topic it is about, since the command is
72205
72350
  * `nodes` for all three.
72206
72351
  ***************************************************************/
72207
- function remote_command(gobj, command, kw) {
72352
+ function remote_command(gobj, command, kw, tag) {
72208
72353
  let remote = gobj_read_pointer_attr(gobj, "gobj_remote_yuno");
72209
72354
  let treedb_name = gobj_read_str_attr(gobj, "treedb_name");
72210
72355
  if (!remote) {
@@ -72215,11 +72360,11 @@ function remote_command(gobj, command, kw) {
72215
72360
  service: treedb_name,
72216
72361
  treedb_name
72217
72362
  }, kw || {});
72218
- full_kw.__md_command__ = {
72363
+ full_kw.__md_command__ = Object.assign({
72219
72364
  purpose: PURPOSE,
72220
72365
  topic_name: kw && kw.topic_name || "",
72221
72366
  record_id: kw && kw.record_id || ""
72222
- };
72367
+ }, tag || {});
72223
72368
  delete full_kw.record_id;
72224
72369
  let ret = gobj_command(remote, command, full_kw, gobj);
72225
72370
  if (ret) {
@@ -72264,25 +72409,33 @@ function start_measuring(gobj) {
72264
72409
  let priv = gobj.priv;
72265
72410
  priv.baseline = {};
72266
72411
  priv.written = {};
72412
+ priv.host_ids = {};
72267
72413
  if (!priv.model) return;
72268
72414
  for (let treedb of priv.model.treedbs) for (let topic of treedb.topics) priv.baseline[topic.id] = topic.topic_version;
72269
- mark_host_drafts(priv.written, priv.model, priv.host_drafts);
72415
+ priv.host_ids = host_draft_ids(priv.model, priv.host_drafts);
72270
72416
  }
72271
72417
  /***************************************************************
72272
- * "This topic was written in this session and is not saved."
72418
+ * "This topic holds an edit that is not saved."
72273
72419
  *
72274
72420
  * An edit is a DRAFT: it moves no version and reaches no treedb
72275
72421
  * until the host saves it (C_TREEDB's save-schema, which raises the
72276
- * versions of what changed) -- and the save makes the host send
72277
- * EV_REFRESH, whose reload is what forgets the drafts.
72422
+ * versions of what changed). Two sources say a topic is one, and
72423
+ * they are kept APART because they are forgotten differently:
72424
+ * - `written`: what THIS session wrote since the model was loaded
72425
+ * (a reload, the host's EV_REFRESH, forgets it -- the store says
72426
+ * what is left);
72427
+ * - `host_ids`: what the host said last (EV_DRAFTS), replaced WHOLE
72428
+ * by the next EV_DRAFTS. Folded into `written`, a topic the host
72429
+ * named once stayed a draft after the Save that published it.
72278
72430
  *
72279
72431
  * ONE predicate, because it is asked in three places -- the topic
72280
72432
  * list draws a chip with it, the column view a banner, the export
72281
72433
  * a warning -- and copies of a rule are how they drift.
72282
72434
  ***************************************************************/
72283
- function topic_is_draft(written, topic) {
72284
- if (!topic || !written) return false;
72285
- return written[topic.id] ? true : false;
72435
+ function topic_is_draft(priv, topic) {
72436
+ if (!topic || !priv) return false;
72437
+ if (priv.written && priv.written[topic.id]) return true;
72438
+ return priv.host_ids && priv.host_ids[topic.id] ? true : false;
72286
72439
  }
72287
72440
  /***************************************************************
72288
72441
  * Say what the whole store would be refused for.
@@ -72724,7 +72877,7 @@ function render_topics(gobj, $body) {
72724
72877
  let twice = repeated_names(treedb.topics);
72725
72878
  let $rows = [];
72726
72879
  for (let topic of treedb.topics) {
72727
- let pending = topic_is_draft(priv.written, topic);
72880
+ let pending = topic_is_draft(priv, topic);
72728
72881
  let ambiguous = !!twice[topic.name];
72729
72882
  let address = ambiguous ? topic.id : topic.name;
72730
72883
  let $actions = [];
@@ -73047,7 +73200,7 @@ function render_columns(gobj, $body) {
73047
73200
  ***************************************************************/
73048
73201
  function draft_banner(gobj, topic) {
73049
73202
  let priv = gobj.priv;
73050
- if (!topic_is_draft(priv.written, topic)) return null;
73203
+ if (!topic_is_draft(priv, topic)) return null;
73051
73204
  if (is_readonly(gobj)) return null;
73052
73205
  return createElement2([
73053
73206
  "div",
@@ -73937,7 +74090,7 @@ function open_report(gobj, findings, treedb_id) {
73937
74090
  ***************************************************************/
73938
74091
  function open_export(gobj, treedb) {
73939
74092
  let priv = gobj.priv;
73940
- let drafts = treedb.topics.some((topic) => topic_is_draft(priv.written, topic));
74093
+ let drafts = treedb.topics.some((topic) => topic_is_draft(priv, topic));
73941
74094
  let c_text = schema_to_c(treedb);
73942
74095
  let json_text = JSON.stringify(schema_to_json(treedb), null, 4);
73943
74096
  let $content = createElement2([
@@ -74442,12 +74595,61 @@ function run_next_write(gobj) {
74442
74595
  let write = priv.save_queue[0];
74443
74596
  let command = write_command(write.op);
74444
74597
  let options = write_options(write.op);
74598
+ priv.write_tag++;
74445
74599
  if (remote_command(gobj, command, {
74446
74600
  topic_name: write.topic_name,
74447
74601
  record: write.record,
74448
74602
  options,
74449
74603
  record_id: write.record.id || ""
74450
- }) < 0) return end_writes(gobj, t("cannot reach the treedb"));
74604
+ }, { write: priv.write_tag }) < 0) return end_writes(gobj, t("cannot reach the treedb"));
74605
+ return 0;
74606
+ }
74607
+ /***************************************************************
74608
+ * Does the transport say it is in session? The state the library
74609
+ * reads off every transport (C_IEVENT_CLI and the routing adapters
74610
+ * name it the same).
74611
+ ***************************************************************/
74612
+ function transport_in_session(gobj) {
74613
+ let remote = gobj_read_pointer_attr(gobj, "gobj_remote_yuno");
74614
+ return !!remote && gobj_current_state(remote) === "ST_SESSION";
74615
+ }
74616
+ /***************************************************************
74617
+ * Is this answer of the load in flight, or of the write in
74618
+ * flight? Each request carries its round in `__md_command__`, and
74619
+ * one the drop abandoned (or a deadline settled) can still come
74620
+ * back: counted, it ended the next load with the old data or
74621
+ * moved the next write's queue.
74622
+ ***************************************************************/
74623
+ function is_current_load(gobj, md) {
74624
+ return gobj_current_state(gobj) === "ST_LOADING" && kw_get_int(gobj, md, "round", 0, 0) === gobj.priv.load_round;
74625
+ }
74626
+ function is_current_write(gobj, md) {
74627
+ return gobj_current_state(gobj) === "ST_SAVING" && kw_get_int(gobj, md, "write", 0, 0) === gobj.priv.write_tag;
74628
+ }
74629
+ /***************************************************************
74630
+ * The transport dropped under a load or a write: its answers
74631
+ * will never come. The load goes back to the screen it replaced
74632
+ * (or to "not connected" when there was none), the write ends
74633
+ * with the drop said -- whether it landed is unknown -- and both
74634
+ * mark the model to be asked again when the session is back.
74635
+ * Bumping the round makes an answer of what was cut stale.
74636
+ ***************************************************************/
74637
+ function transport_dropped(gobj) {
74638
+ let priv = gobj.priv;
74639
+ let state = gobj_current_state(gobj);
74640
+ priv.reload_on_open = true;
74641
+ if (state === "ST_SAVING") {
74642
+ priv.write_tag++;
74643
+ return end_writes(gobj, "the connection dropped during the write");
74644
+ }
74645
+ if (state === "ST_LOADING") {
74646
+ priv.load_round++;
74647
+ priv.pending = 0;
74648
+ priv.load_error = "";
74649
+ if (priv.model) return go(gobj, priv.treedb_id, priv.topic_name, priv.diagram, false);
74650
+ gobj_change_state(gobj, "ST_IDLE");
74651
+ render(gobj);
74652
+ }
74451
74653
  return 0;
74452
74654
  }
74453
74655
  /***************************************************************
@@ -74602,8 +74804,15 @@ function ac_mt_command_answer(gobj, event, kw, src) {
74602
74804
  let kw_command = kw_get_dict(gobj, __command__, "kw", {}, 0);
74603
74805
  let md = is_object(kw_command.__md_command__) ? kw_command.__md_command__ : kw_command;
74604
74806
  if (kw_get_str(gobj, md, "purpose", "", 0) !== PURPOSE) return 0;
74807
+ if (result < 0 && !transport_in_session(gobj) && (gobj_current_state(gobj) === "ST_LOADING" || gobj_current_state(gobj) === "ST_SAVING")) {
74808
+ if (command === "nodes" && is_current_load(gobj, md) || command !== "nodes" && is_current_write(gobj, md)) return transport_dropped(gobj);
74809
+ }
74605
74810
  if (command === "nodes") {
74606
74811
  let topic_name = kw_get_str(gobj, md, "topic_name", "", 0);
74812
+ if (!is_current_load(gobj, md)) {
74813
+ log_warning(`${gobj_short_name(gobj)}: 'nodes' of '${topic_name}' answered for a load that is over: ignored`);
74814
+ return 0;
74815
+ }
74607
74816
  priv.pending--;
74608
74817
  if (result < 0) priv.load_error = comment || t("cannot load the schemas");
74609
74818
  else if (priv.records && Array.isArray(priv.records[topic_name])) priv.records[topic_name] = Array.isArray(data) ? data : [];
@@ -74621,6 +74830,10 @@ function ac_mt_command_answer(gobj, event, kw, src) {
74621
74830
  }
74622
74831
  if (command === "update-node" || command === "delete-node") {
74623
74832
  let topic_name = kw_get_str(gobj, md, "topic_name", "", 0);
74833
+ if (!is_current_write(gobj, md)) {
74834
+ log_warning(`${gobj_short_name(gobj)}: '${command}' of '${topic_name}' answered for a write that is over (result ${result}): ignored`);
74835
+ return 0;
74836
+ }
74624
74837
  if (result < 0) return end_writes(gobj, comment || t("the treedb refused the write"));
74625
74838
  priv.save_wrote = true;
74626
74839
  if (command === "delete-node") forget_record(gobj, topic_name, kw_get_str(gobj, md, "record_id", "", 0));
@@ -74680,14 +74893,28 @@ function ac_hide(gobj, event, kw, src) {
74680
74893
  return 0;
74681
74894
  }
74682
74895
  /***************************************************************
74683
- * The session came up or went down. The model stays either way
74684
- * — a schema does not change under you — but a first load may
74685
- * have had no transport to run on.
74896
+ * The session came up or went down. The model stays -- a schema
74897
+ * does not change under you -- unless the drop cut a load or a
74898
+ * write (transport_dropped()): then it is asked again on the way
74899
+ * back, as it is when a first load had no transport to run on.
74900
+ *
74901
+ * Nothing else ends a load or a write the drop cut: their answers
74902
+ * will never come, and waiting in ST_LOADING skipped the reload
74903
+ * itself (M-1 of the independent review of 7.25.4).
74686
74904
  ***************************************************************/
74687
74905
  function ac_transport_state(gobj, event, kw, src) {
74688
74906
  let priv = gobj.priv;
74907
+ let state = gobj_current_state(gobj);
74689
74908
  priv.connected = !!(kw && kw.connected);
74690
- if (priv.connected && !priv.model && gobj_current_state(gobj) !== "ST_LOADING") return request_model(gobj);
74909
+ if (!priv.connected) {
74910
+ if (state === "ST_LOADING" || state === "ST_SAVING") return transport_dropped(gobj);
74911
+ render_toolbar(gobj);
74912
+ return 0;
74913
+ }
74914
+ if ((!priv.model || priv.reload_on_open) && state !== "ST_LOADING" && state !== "ST_SAVING") {
74915
+ priv.reload_on_open = false;
74916
+ return request_model(gobj);
74917
+ }
74691
74918
  render_toolbar(gobj);
74692
74919
  return 0;
74693
74920
  }
@@ -74705,18 +74932,22 @@ function ac_refresh$1(gobj, event, kw, src) {
74705
74932
  }
74706
74933
  /***************************************************************
74707
74934
  * {drafts: {treedb_name: [topic names]}} -- the host says which
74708
- * topics hold a draft in __system__ that the file in use does not
74709
- * have (C_TREEDB's saved-schema, `draft_changed`). The mark of a
74710
- * write lived in this session's memory only, so a reload of the
74711
- * page, a reconnect or a refresh showed no draft while __system__
74712
- * still differed from the file (N13 of the 2026-09-22 review).
74713
- * Replaces what the host said before; what this session wrote
74714
- * stays marked.
74935
+ * topics hold a draft in __system__ that is not saved (C_TREEDB's
74936
+ * saved-schema, `draft_changed`). The mark of a write lived in this
74937
+ * session's memory only, so a reload of the page, a reconnect or a
74938
+ * refresh showed no draft while __system__ still differed (N13 of
74939
+ * the 2026-09-22 review).
74940
+ *
74941
+ * REPLACES what the host said before -- the host is the truth after
74942
+ * a reload, and a Save is exactly the host saying "none any more".
74943
+ * It only ever added, and a topic stayed a draft after the Save that
74944
+ * published it (M1 of the 2026-09-23 review). What this session wrote
74945
+ * stays marked: it is kept apart (see topic_is_draft()).
74715
74946
  ***************************************************************/
74716
74947
  function ac_drafts(gobj, event, kw, src) {
74717
74948
  let priv = gobj.priv;
74718
74949
  priv.host_drafts = kw && kw.drafts && typeof kw.drafts === "object" ? kw.drafts : {};
74719
- mark_host_drafts(priv.written, priv.model, priv.host_drafts);
74950
+ priv.host_ids = host_draft_ids(priv.model, priv.host_drafts);
74720
74951
  if (priv.model) render(gobj);
74721
74952
  return 0;
74722
74953
  }