@yuneta/gobj-ui 7.25.13 → 7.25.15

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.
@@ -160,10 +160,11 @@ function mt_create(gobj)
160
160
  * and serial (form_writes_in_flight): answered refused when the
161
161
  * transport closes under them. */
162
162
  gobj.priv.form_writes = {};
163
- /* The topics a write cut by a DROP left unknown: the table may show
164
- * what was typed and the store what it had. Read again when the
165
- * session is back (reload_owed_topics), never out of session. */
166
- gobj.priv.reload_on_session = {};
163
+ /* The session dropped since the tables were read: every open table
164
+ * may be stale (a write the drop cut, node events published while
165
+ * nobody heard them). Read again when the session is back
166
+ * (reread_open_topics), never out of session. */
167
+ gobj.priv.reread_on_session = false;
167
168
  gobj.priv.conn_shell = null; /* the shell whose EV_CONNECTION_STATE we hear */
168
169
 
169
170
  build_ui(gobj);
@@ -187,7 +188,7 @@ function mt_start(gobj)
187
188
  * (yui_shell_set_connection_state). A write in flight when it drops
188
189
  * is never answered, and its form waited for ever in every host
189
190
  * that did not forward EV_TRANSPORT_STATE -- wattyzer and yunovatios
190
- * among them (M8 of the 2026-09-23 review). The transport itself is
191
+ * among them. The transport itself is
191
192
  * not subscribed to: see ac_transport_state(). */
192
193
  let shell = yui_shell_of(gobj);
193
194
  if(shell) {
@@ -2111,7 +2112,7 @@ function ac_mt_command_answer(gobj, event, kw, src)
2111
2112
  log_warning(`${gobj_short_name(gobj)}: the write of ` +
2112
2113
  `'${failed_topic}' was cut by the drop: the topic is ` +
2113
2114
  `read again when the session is back`);
2114
- gobj.priv.reload_on_session[failed_topic] = true;
2115
+ gobj.priv.reread_on_session = true;
2115
2116
  }
2116
2117
  }
2117
2118
  /* Answered once: the transport closing may have answered
@@ -2475,18 +2476,17 @@ function ac_transport_state(gobj, event, kw, src)
2475
2476
 
2476
2477
  /*
2477
2478
  * A write in flight when the transport closes is never answered by
2478
- * the backend: its form stayed open and busy for ever (N8 of the
2479
- * 2026-09-22 review). Answered refused: the form stays open on what
2479
+ * the backend: its form stayed open and busy for ever. Answered
2480
+ * refused: the form stays open on what
2480
2481
  * was typed, and the person saves again when the session is back.
2481
2482
  */
2482
2483
  if(!connected) {
2484
+ /* What happens in the store while the session is down reaches
2485
+ * no table: its node events are published to nobody. Every open
2486
+ * table is read again when the session is back. */
2487
+ gobj.priv.reread_on_session = true;
2483
2488
  abandon_form_writes(gobj.priv.form_writes).forEach((w) => {
2484
2489
  log_warning(`${gobj_short_name(gobj)}: transport closed, the write ${w.form_write} of '${w.topic_name}' is lost`);
2485
- /* Whether it reached the store is unknown: read it again
2486
- * when the session is back. */
2487
- if(w.topic_name) {
2488
- gobj.priv.reload_on_session[w.topic_name] = true;
2489
- }
2490
2490
  answer_form_write(
2491
2491
  gobj,
2492
2492
  get_gobj_formtable(gobj, w.topic_name),
@@ -2495,32 +2495,37 @@ function ac_transport_state(gobj, event, kw, src)
2495
2495
  );
2496
2496
  });
2497
2497
  } else {
2498
- reload_owed_topics(gobj);
2498
+ reread_open_topics(gobj);
2499
2499
  }
2500
2500
  return 0;
2501
2501
  }
2502
2502
 
2503
2503
  /************************************************************
2504
- * The session is back: read again the topics a drop left
2505
- * unknown. Only when the TRANSPORT says so too -- the app's
2506
- * connection (EV_CONNECTION_STATE) can report "up" before the
2507
- * transport of this view is in session, and a read asked then is
2508
- * refused. What is owed stays owed until an edge finds the
2509
- * transport in session; each topic is read once.
2504
+ * The session is back after a drop: read every open table again,
2505
+ * as the schema editor reloads its model. A drop cuts the writes
2506
+ * in flight AND the node events published meanwhile, so any
2507
+ * table may be stale, not only the one a cut write left.
2508
+ *
2509
+ * Only when the TRANSPORT says so too -- the app's connection
2510
+ * (EV_CONNECTION_STATE) can report "up" before the transport of
2511
+ * this view is in session, and a read asked then is refused. The
2512
+ * read waits for an edge that finds the transport in session, and
2513
+ * is done once, though both edges report the same "up".
2510
2514
  ************************************************************/
2511
- function reload_owed_topics(gobj)
2515
+ function reread_open_topics(gobj)
2512
2516
  {
2513
2517
  let priv = gobj.priv;
2514
- if(!is_connected(gobj)) {
2518
+ if(!priv.reread_on_session || !is_connected(gobj)) {
2515
2519
  return;
2516
2520
  }
2517
- let topics = Object.keys(priv.reload_on_session);
2518
- priv.reload_on_session = {};
2519
- topics.forEach((topic_name) => {
2520
- if(!get_gobj_formtable(gobj, topic_name)) {
2521
- return; /* its table is closed: opening it reads it */
2521
+ priv.reread_on_session = false;
2522
+ gobj_match_children(gobj, {
2523
+ __gclass_name__: "C_YUI_TREEDB_TOPIC_WITH_FORM"
2524
+ }).forEach((form) => {
2525
+ let topic_name = gobj_read_attr(form, "topic_name");
2526
+ if(topic_name) {
2527
+ gobj_send_event(gobj, "EV_REFRESH_TOPIC", {topic_name: topic_name}, gobj);
2522
2528
  }
2523
- gobj_send_event(gobj, "EV_REFRESH_TOPIC", {topic_name: topic_name}, gobj);
2524
2529
  });
2525
2530
  }
2526
2531
 
@@ -2643,8 +2648,8 @@ function ac_create_record(gobj, event, kw, src)
2643
2648
 
2644
2649
  /* `create_only`: +New makes a NEW record. With `create` alone a taken
2645
2650
  * id was an update -- the record overwritten and, through autolink
2646
- * with the selects empty, unlinked, answered "Node update!" (M28 of
2647
- * the 2026-09-21 review). A backend older than the option ignores it
2651
+ * with the selects empty, unlinked, answered "Node update!". A
2652
+ * backend older than the option ignores it
2648
2653
  * and keeps the old upsert. */
2649
2654
  let options = {
2650
2655
  list_dict: true,
@@ -2890,8 +2895,8 @@ function ac_update_record(gobj, event, kw, src)
2890
2895
  /************************************************************
2891
2896
  * A write sent with no session never leaves: C_IEVENT_CLI logs
2892
2897
  * "Not in session" and answers null, as it does when the command
2893
- * went -- so a form waiting for the answer waited for ever (N8 of
2894
- * the 2026-09-22 review). Refused before it is sent, aloud.
2898
+ * went -- so a form waiting for the answer waited for ever. Refused
2899
+ * before it is sent, aloud.
2895
2900
  ************************************************************/
2896
2901
  function refuse_without_session(gobj, topic_name)
2897
2902
  {
package/src/form_busy.js CHANGED
@@ -10,9 +10,8 @@
10
10
  * that back. So a second busy(true) while already busy must not
11
11
  * run at all -- it read the buttons it had just disabled as
12
12
  * "disabled on their own", and the busy(false) of a refused write
13
- * left Save and Cancel dead (N9 of the 2026-09-22 review: a record
14
- * with a picked file went busy for the read, then busy again for
15
- * the write). The caller owns `state` ({busy}) for the form's life.
13
+ * left Save and Cancel dead (a record with a picked file went
14
+ * busy for the read, then busy again for the write). The caller owns `state` ({busy}) for the form's life.
16
15
  *
17
16
  * Copyright (c) 2026, ArtGins.
18
17
  * All Rights Reserved.
@@ -37,7 +37,7 @@
37
37
  * A Date -> the value of a `datetime-local` input, in local time,
38
38
  * WITH seconds: the input is built with `step: 1`, which is what
39
39
  * lets it hold them. Written as "YYYY-MM-DDTHH:mm" it lost them on
40
- * every save of any field (M26 of the 2026-09-21 review).
40
+ * every save of any field.
41
41
  ************************************************************/
42
42
  function date_to_datetime_local(value)
43
43
  {
@@ -4,7 +4,7 @@ import {date_to_datetime_local, datetime_local_to_epoch} from "./form_time_value
4
4
  describe("form_time_value", () => {
5
5
 
6
6
  it("an epoch with seconds survives a round trip through the input", () => {
7
- /* M26 of the 2026-09-21 review: the input was written without
7
+ /* The input was written without
8
8
  * seconds, so every save of ANY field moved a writable time
9
9
  * column back up to 59 s. */
10
10
  const epoch = 1790000037; // :37 seconds
@@ -7,8 +7,7 @@
7
7
  * answers, so the host has to answer EVERY way a write ends --
8
8
  * and the transport closing under a write in flight, or a
9
9
  * command that never left because there was no session, are two
10
- * of them that nobody answered: the form stayed busy for ever
11
- * (N8 of the 2026-09-22 review).
10
+ * of them that nobody answered: the form stayed busy for ever.
12
11
  *
13
12
  * A write is known by its TOPIC and its `form_write` serial,
14
13
  * never by the serial alone: each topic has its own form, and
@@ -0,0 +1,33 @@
1
+ /***********************************************************************
2
+ * g6_nodes_tree_fsm.test.js
3
+ *
4
+ * The machine of the real C_G6_NODES_TREE: the events its host
5
+ * sends it are declared, and each has an action. The host's own
6
+ * tests drive a fake engine, which cannot see this.
7
+ *
8
+ * Copyright (c) 2026, ArtGins.
9
+ * All Rights Reserved.
10
+ ***********************************************************************/
11
+ import {describe, test, expect} from "vitest";
12
+ import {install_dom_double} from "../test/dom_double.js";
13
+
14
+ install_dom_double();
15
+
16
+ const {
17
+ gclass_find_by_name, gclass_event_type, gclass_check_fsm,
18
+ } = await import("@yuneta/gobj-js");
19
+ const {register_c_g6_nodes_tree} = await import("./c_g6_nodes_tree.js");
20
+
21
+ describe("C_G6_NODES_TREE", () => {
22
+
23
+ test("hears EV_GRAPHS_WRITE_REFUSED, the host's word on a __graphs__ write", () => {
24
+ register_c_g6_nodes_tree();
25
+ const gclass = gclass_find_by_name("C_G6_NODES_TREE", false);
26
+ expect(gclass).toBeTruthy();
27
+ expect(gclass_event_type(gclass, "EV_GRAPHS_WRITE_REFUSED")).toBeTruthy();
28
+ const idle = gclass.dl_states.find((st) => st.state_name === "ST_IDLE");
29
+ expect(idle.dl_actions.some((a) => a.event_name === "EV_GRAPHS_WRITE_REFUSED" &&
30
+ typeof a.action === "function")).toBe(true);
31
+ expect(gclass_check_fsm(gclass)).toBe(0);
32
+ });
33
+ });
@@ -140,7 +140,35 @@ function apply_graphs_echo(live, saved, records, rec)
140
140
  }
141
141
  }
142
142
 
143
+ /***************************************************************
144
+ * A write of one topic's arrangement did NOT land: the backend
145
+ * refused it, the transport refused it on the way out, or there
146
+ * was no session to send it on.
147
+ *
148
+ * saved {topic_name: properties} what the backend has
149
+ * topic_name the topic of the refused record
150
+ *
151
+ * The view records a write as saved BEFORE it leaves, because
152
+ * nothing answers it when it lands. So a refused one left the view
153
+ * believing the backend held an arrangement it does not, and the
154
+ * next Save found nothing to write. What the backend holds for the
155
+ * topic is not known any more, so it is forgotten: the next Save
156
+ * writes the topic again (plan_graph_saves()).
157
+ *
158
+ * Returns TRUE if there was something to forget.
159
+ ***************************************************************/
160
+ function forget_refused_graphs_write(saved, topic_name)
161
+ {
162
+ if(!is_object(saved) || !topic_name ||
163
+ !Object.prototype.hasOwnProperty.call(saved, topic_name)) {
164
+ return false;
165
+ }
166
+ delete saved[topic_name];
167
+ return true;
168
+ }
169
+
143
170
  export {
171
+ forget_refused_graphs_write,
144
172
  apply_graphs_echo,
145
173
  plan_graph_saves,
146
174
  topic_arrangement_changed,
@@ -18,7 +18,8 @@
18
18
  ***********************************************************************/
19
19
  import {describe, test, expect} from "vitest";
20
20
  import {
21
- plan_graph_saves, topic_arrangement_changed, arrangement_of, apply_graphs_echo
21
+ plan_graph_saves, topic_arrangement_changed, arrangement_of, apply_graphs_echo,
22
+ forget_refused_graphs_write
22
23
  } from "./graph_save_plan.js";
23
24
 
24
25
  const ORIGIN = "f3163a9f-1f37-412a-b8c9-87a4341f14de";
@@ -150,7 +151,7 @@ describe("arrangement_of", () => {
150
151
  * save by another browser, sends back. It used to rebuild the saved
151
152
  * snapshot of EVERY topic, taking the copies from the live objects the
152
153
  * view had already rearranged: what was unsaved in OTHER topics counted
153
- * as saved, and the next Save skipped it (M32 of the 2026-09-21 review).
154
+ * as saved, and the next Save skipped it.
154
155
  */
155
156
  describe("an echo of one __graphs__ record", () => {
156
157
  function state()
@@ -200,3 +201,36 @@ describe("an echo of one __graphs__ record", () => {
200
201
  expect(live.yunos).toBeDefined();
201
202
  });
202
203
  });
204
+
205
+ /*
206
+ * A write of `__graphs__` that the backend REFUSED. The view takes the
207
+ * write for granted before it leaves (nothing answers it when it lands),
208
+ * so a refused one was recorded as saved: the next Save found nothing to
209
+ * write, and the arrangement was never stored.
210
+ */
211
+ describe("a refused __graphs__ write", () => {
212
+
213
+ test("is planned again by the next Save", () => {
214
+ let live = five_topics();
215
+ let saved = five_topics();
216
+ live.yunos.nodes["1"].x = 500;
217
+ expect(plan_graph_saves(live, saved)).toEqual(["yunos"]);
218
+
219
+ /* What save_topic_graph_properties() does before the publish. */
220
+ saved.yunos = JSON.parse(JSON.stringify(live.yunos));
221
+ expect(plan_graph_saves(live, saved)).toEqual([]);
222
+
223
+ expect(forget_refused_graphs_write(saved, "yunos")).toBe(true);
224
+ expect(plan_graph_saves(live, saved)).toEqual(["yunos"]);
225
+ expect(Object.keys(saved)).not.toContain("yunos");
226
+ expect(Object.keys(saved).length).toBe(4);
227
+ });
228
+
229
+ test("a topic the view never believed saved: nothing to forget", () => {
230
+ let saved = five_topics();
231
+ expect(forget_refused_graphs_write(saved, "nothing")).toBe(false);
232
+ expect(forget_refused_graphs_write(saved, "")).toBe(false);
233
+ expect(forget_refused_graphs_write(null, "yunos")).toBe(false);
234
+ expect(Object.keys(saved).length).toBe(5);
235
+ });
236
+ });
@@ -7,9 +7,8 @@
7
7
  * it, and that mark lived in the memory of the session only:
8
8
  * a reload of the page, a reconnect or a refresh of the model
9
9
  * lost the chip, the banner and the export warning while
10
- * __system__ still differed from the file in use (N13 of the
11
- * 2026-09-22 review). The host reads that difference from
12
- * C_TREEDB's `saved-schema` (`draft_changed`, by treedb) and
10
+ * __system__ still differed from the file in use. The host
11
+ * reads that difference from C_TREEDB's `saved-schema` (`draft_changed`, by treedb) and
13
12
  * hands it here; the editor applies it to its model, in place
14
13
  * of what the host said before.
15
14
  *
@@ -4,7 +4,7 @@
4
4
  * EV_DRAFTS driven through a real C_YUI_SCHEMA_EDITOR on a document
5
5
  * double. The helper was right and tested; the ACTION folded the
6
6
  * host's marks into the session's, so a topic the host named before
7
- * a Save stayed a draft after it (M1 of the 2026-09-23 review).
7
+ * a Save stayed a draft after it.
8
8
  *
9
9
  * Copyright (c) 2026, ArtGins.
10
10
  * All Rights Reserved.