@yuneta/gobj-ui 7.25.6 → 7.25.8

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.
@@ -206,6 +206,7 @@ let PRIVATE_DATA = {
206
206
  pending: 0, /* `nodes` requests still in flight */
207
207
  load_error: "",
208
208
  load_round: 0, /* which load an answer belongs to: echoed as `round` */
209
+ load_keeps_written: false, /* the load in flight is not the host's: `written` survives it */
209
210
 
210
211
  /* Where the operator is. The url carries it; these mirror it. */
211
212
  treedb_id: "",
@@ -231,8 +232,11 @@ let PRIVATE_DATA = {
231
232
  write_tag: 0, /* which write an answer belongs to: echoed as `write` */
232
233
  connected: false,
233
234
  reload_on_open: false, /* a drop cut a load or a write: ask the model again */
235
+ dropped_round: 0, /* the load a drop ended: its failures are the drop, said once */
236
+ dropped_write: 0, /* the same for the write a drop ended */
237
+ reload_after_write: false, /* a write given up turned out DONE while another was in flight */
234
238
 
235
- /* A position that arrived while a write was in flight */
239
+ /* A position that arrived while a load or a write was in flight */
236
240
  pending_seg: null,
237
241
 
238
242
  /* The import dialog's plan and the two nodes it draws into */
@@ -455,8 +459,25 @@ function refuse_if_readonly(gobj, event)
455
459
  * schema set of the yuno is here: everything below — the
456
460
  * drawing, the validation, the export — is computed from these
457
461
  * and asks the backend nothing more.
462
+ *
463
+ * THE LOADING SCREEN IS DRAWN, not assumed. ST_LOADING declares
464
+ * none of the actions of a screen, so the screen it replaces goes
465
+ * and the toolbar stops answering: left up, a click on a card, on
466
+ * Back or on the drawing answered "Event NOT DEFINED in state
467
+ * ST_LOADING" and was lost (third independent review). A dialog
468
+ * left open is answered in ST_LOADING instead (ac_wait_for_the_load).
469
+ *
470
+ * `keep_written`: a load the host did not ask for -- the reconnect,
471
+ * a write that turned out done -- keeps what this session wrote.
472
+ * Only the host's EV_REFRESH (a Save sends it) forgets it.
473
+ *
474
+ * A load that cannot leave WHOLE did not happen (out of session it
475
+ * is not even asked): the records the model was built on are put
476
+ * back (emptied, the next write patched them into a model with no
477
+ * treedb), the round is spent so a request that did leave is
478
+ * stale, and the session back asks again.
458
479
  ***************************************************************/
459
- function request_model(gobj)
480
+ function request_model(gobj, keep_written)
460
481
  {
461
482
  let priv = gobj.priv;
462
483
  let remote = gobj_read_pointer_attr(gobj, "gobj_remote_yuno");
@@ -465,27 +486,41 @@ function request_model(gobj)
465
486
  return -1; /* not mounted on a transport yet: mt_start retries via EV_TRANSPORT_STATE */
466
487
  }
467
488
 
489
+ let previous = priv.records;
468
490
  priv.records = {treedbs: [], topics: [], cols: []};
469
491
  priv.load_error = "";
470
492
  priv.pending = 0;
471
493
  priv.load_round++;
494
+ priv.load_keeps_written = !!keep_written;
472
495
  gobj_change_state(gobj, "ST_LOADING");
473
- show_notice(gobj, "loading the schemas");
474
-
475
- for(let topic_name of [T_TREEDBS, T_TOPICS, T_COLS]) {
476
- if(remote_command(gobj, "nodes", {
477
- topic_name: topic_name,
478
- options: {list_dict: true}
479
- }, {round: priv.load_round}) === 0) {
480
- priv.pending++;
496
+ set_busy(gobj, false);
497
+ render(gobj);
498
+
499
+ /* Out of session nothing is asked: each request would come back
500
+ * refused and logged as an ERROR, for a drop that is not one. */
501
+ let left = 0;
502
+ if(transport_in_session(gobj)) {
503
+ for(let topic_name of [T_TREEDBS, T_TOPICS, T_COLS]) {
504
+ if(remote_command(gobj, "nodes", {
505
+ topic_name: topic_name,
506
+ options: {list_dict: true}
507
+ }, {round: priv.load_round}) === 0) {
508
+ left++;
509
+ }
481
510
  }
482
511
  }
483
- if(priv.pending === 0) {
512
+ if(left < 3) {
513
+ priv.records = previous;
514
+ priv.load_round++;
515
+ priv.reload_on_open = true;
484
516
  priv.load_error = t("cannot reach the treedb");
485
- gobj_change_state(gobj, "ST_IDLE");
486
- show_notice(gobj, "cannot reach the treedb");
517
+ if(priv.model) {
518
+ yui_shell_show_error(yui_shell_of(gobj), "cannot reach the treedb", {t: t});
519
+ }
520
+ end_load(gobj);
487
521
  return -1;
488
522
  }
523
+ priv.pending = left;
489
524
  return 0;
490
525
  }
491
526
 
@@ -568,17 +603,23 @@ function build_model(gobj)
568
603
  * From here on, "did this session change it?" has an answer.
569
604
  *
570
605
  * Called ONLY when records arrive from the store: that is what a
571
- * measurement of "changed since" can be measured against. Both
572
- * halves reset together -- a baseline without its `written` is
573
- * the same false positive from the other side, since every
574
- * version equals a baseline just taken.
606
+ * measurement of "changed since" can be measured against.
607
+ *
608
+ * `written` is forgotten only by a load the HOST asked for (its
609
+ * EV_REFRESH, which a Save sends). Since M36 a write moves no
610
+ * version, so `written` is the draft chip of what this session
611
+ * wrote and no longer half of a version measurement: a reload of
612
+ * the reconnect, or of a write that turned out done, forgot the
613
+ * chips of writes the host had not been told about.
575
614
  ***************************************************************/
576
615
  function start_measuring(gobj)
577
616
  {
578
617
  let priv = gobj.priv;
579
618
 
580
619
  priv.baseline = {};
581
- priv.written = {};
620
+ if(!priv.load_keeps_written) {
621
+ priv.written = {};
622
+ }
582
623
  priv.host_ids = {};
583
624
 
584
625
  if(!priv.model) {
@@ -802,12 +843,19 @@ function render_toolbar(gobj)
802
843
  let state = gobj_current_state(gobj);
803
844
  let readonly = is_readonly(gobj);
804
845
  let has_model = !!priv.model;
846
+ /* The position can outlive its screen (a load that failed keeps the
847
+ * url's; ST_EMPTY has a treedb in the url and none in the model),
848
+ * and a button there sends what only a treedb screen declares. */
849
+ let on_a_treedb = state !== "ST_IDLE" && state !== "ST_EMPTY";
850
+ /* Nothing is legal while the model is in the air: every action is
851
+ * computed against it. */
852
+ let in_flight = state === "ST_SAVING" || state === "ST_LOADING";
805
853
 
806
854
  /*----------------------------------------------*
807
855
  * Left: back, then the position as a trail.
808
856
  *----------------------------------------------*/
809
857
  let $left = [];
810
- if(priv.treedb_id) {
858
+ if(priv.treedb_id && on_a_treedb) {
811
859
  $left.push(["button", {class: "SCHEMA_BACK button",
812
860
  type: "button",
813
861
  title: t("back"), "aria-label": t("back"),
@@ -844,7 +892,7 @@ function render_toolbar(gobj)
844
892
  * refused says why when it is pressed.
845
893
  *----------------------------------------------*/
846
894
  let $right = [];
847
- if(priv.treedb_id && has_model) {
895
+ if(priv.treedb_id && has_model && on_a_treedb) {
848
896
  /* The compass, like every other door to a SCHEMA: the
849
897
  * `hexagon-nodes` it wore is the graph of the DATA. */
850
898
  $right.push(toolbar_button("SCHEMA_DIAGRAM_BTN", "yi-compass-drafting",
@@ -858,7 +906,7 @@ function render_toolbar(gobj)
858
906
  "import", "EV_IMPORT", false));
859
907
  }
860
908
  }
861
- if(priv.topic_name && !readonly && !priv.diagram) {
909
+ if(priv.topic_name && !readonly && !priv.diagram && on_a_treedb) {
862
910
  /* Only while there IS somewhere to go back to: a drag is a write,
863
911
  * and this is the way back from one. */
864
912
  if(undo_order_writes(gobj, current_topic(gobj)).length > 0) {
@@ -868,7 +916,7 @@ function render_toolbar(gobj)
868
916
  $right.push(toolbar_button("SCHEMA_ADD_COL_BTN", "yi-plus",
869
917
  "new column", "EV_ADD_COLUMN", false));
870
918
  }
871
- if(priv.treedb_id && !priv.topic_name && !readonly && !priv.diagram) {
919
+ if(priv.treedb_id && !priv.topic_name && !readonly && !priv.diagram && on_a_treedb) {
872
920
  $right.push(toolbar_button("SCHEMA_ADD_TOPIC_BTN", "yi-plus",
873
921
  "new topic", "EV_ADD_TOPIC", false));
874
922
  }
@@ -891,7 +939,7 @@ function render_toolbar(gobj)
891
939
  * destroyed. */
892
940
  for(let $button of $bar.querySelectorAll("button")) {
893
941
  $button.__gobj__ = gobj;
894
- if(state === "ST_SAVING") {
942
+ if(in_flight) {
895
943
  $button.disabled = true;
896
944
  }
897
945
  }
@@ -915,19 +963,25 @@ function render(gobj)
915
963
  }
916
964
  render_toolbar(gobj);
917
965
 
966
+ /* A screen with nothing to draw takes the drawing down too: left
967
+ * alive, it was only re-shown on the way back -- into a body that
968
+ * no longer held it. */
918
969
  if(state === "ST_LOADING") {
919
970
  show_notice(gobj, "loading the schemas");
971
+ destroy_diagram(gobj);
920
972
  clear($body);
921
973
  return;
922
974
  }
923
975
  if(state === "ST_IDLE") {
924
976
  show_notice(gobj, priv.load_error ? "cannot load the schemas" : "not connected",
925
977
  priv.load_error);
978
+ destroy_diagram(gobj);
926
979
  clear($body);
927
980
  return;
928
981
  }
929
982
  if(state === "ST_EMPTY") {
930
983
  show_notice(gobj, "this yuno stores no schema");
984
+ destroy_diagram(gobj);
931
985
  clear($body);
932
986
  return;
933
987
  }
@@ -1468,12 +1522,21 @@ function render_diagram(gobj)
1468
1522
  let $body = $of(gobj, ".SCHEMA_BODY");
1469
1523
  let treedb = current_treedb(gobj);
1470
1524
 
1471
- if(!$body || !treedb) {
1525
+ if(!$body) {
1526
+ return;
1527
+ }
1528
+ /* Said on an EMPTY body, as the topics and the columns say it:
1529
+ * render() clears the body for every screen but this one, and the
1530
+ * screen before stayed up under the notice, clickable. */
1531
+ if(!treedb) {
1532
+ destroy_diagram(gobj);
1533
+ clear($body);
1472
1534
  show_notice(gobj, "that treedb is not here any more", priv.treedb_id);
1473
1535
  return;
1474
1536
  }
1475
1537
  if(!gclass_find_by_name("C_YUI_TREEDB_SCHEMA")) {
1476
1538
  log_error(`${gobj_short_name(gobj)}: C_YUI_TREEDB_SCHEMA not registered by the app`);
1539
+ clear($body);
1477
1540
  show_notice(gobj, "the schema drawing is not available");
1478
1541
  return;
1479
1542
  }
@@ -2403,6 +2466,15 @@ function transport_in_session(gobj)
2403
2466
  * one the drop abandoned (or a deadline settled) can still come
2404
2467
  * back: counted, it ended the next load with the old data or
2405
2468
  * moved the next write's queue.
2469
+ *
2470
+ * The state is part of the question, and that is only true
2471
+ * because NOTHING moves the editor out of ST_LOADING or ST_SAVING
2472
+ * but the end of what is in flight: a position the host sends
2473
+ * meanwhile waits in `pending_seg` (ac_show()). 7.25.6 let EV_SHOW
2474
+ * move a RELOAD out of ST_LOADING, and every answer of it was then
2475
+ * "for a load that is over": the records it had emptied stayed
2476
+ * empty, and the next write patched them into a model with no
2477
+ * treedb at all.
2406
2478
  ***************************************************************/
2407
2479
  function is_current_load(gobj, md)
2408
2480
  {
@@ -2431,19 +2503,51 @@ function transport_dropped(gobj)
2431
2503
 
2432
2504
  priv.reload_on_open = true;
2433
2505
  if(state === "ST_SAVING") {
2506
+ log_warning(`${gobj_short_name(gobj)}: the transport dropped during a write: ` +
2507
+ `the model is read again on the reconnect`);
2508
+ priv.dropped_write = priv.write_tag;
2434
2509
  priv.write_tag++;
2435
2510
  return end_writes(gobj, "the connection dropped during the write");
2436
2511
  }
2437
2512
  if(state === "ST_LOADING") {
2513
+ /* Said HERE, once: the transport settles every request the drop
2514
+ * cut, and those failures are this one event (see
2515
+ * ac_mt_command_answer()). */
2516
+ log_warning(`${gobj_short_name(gobj)}: the transport dropped during the load: ` +
2517
+ `the model is read again on the reconnect`);
2518
+ priv.dropped_round = priv.load_round;
2438
2519
  priv.load_round++;
2439
2520
  priv.pending = 0;
2440
2521
  priv.load_error = "";
2441
- if(priv.model) {
2442
- return go(gobj, priv.treedb_id, priv.topic_name, priv.diagram, false);
2522
+ return end_load(gobj);
2523
+ }
2524
+ return 0;
2525
+ }
2526
+
2527
+ /***************************************************************
2528
+ * A load is over -- landed, failed or cut -- and the editor goes
2529
+ * where the operator is: the position the host sent while it was
2530
+ * in flight (ac_show() kept it), or the one it had. Without a
2531
+ * model there is no screen to go to: the position is kept for the
2532
+ * next load, and the editor says why it is empty.
2533
+ ***************************************************************/
2534
+ function end_load(gobj)
2535
+ {
2536
+ let priv = gobj.priv;
2537
+ let seg = priv.pending_seg;
2538
+
2539
+ priv.pending_seg = null;
2540
+ if(priv.model) {
2541
+ if(seg !== null) {
2542
+ return apply_seg(gobj, seg);
2443
2543
  }
2444
- gobj_change_state(gobj, "ST_IDLE");
2445
- render(gobj);
2544
+ return go(gobj, priv.treedb_id, priv.topic_name, priv.diagram, false);
2545
+ }
2546
+ if(seg !== null) {
2547
+ adopt_seg(gobj, seg);
2446
2548
  }
2549
+ gobj_change_state(gobj, "ST_IDLE");
2550
+ render(gobj);
2447
2551
  return 0;
2448
2552
  }
2449
2553
 
@@ -2457,29 +2561,15 @@ function end_writes(gobj, error)
2457
2561
  {
2458
2562
  let priv = gobj.priv;
2459
2563
 
2564
+ let seg = priv.pending_seg;
2565
+
2460
2566
  priv.save_queue = [];
2567
+ priv.pending_seg = null;
2461
2568
  gobj_change_state(gobj, priv.save_return || "ST_TREEDBS");
2462
2569
  priv.save_return = "";
2463
2570
  show_notice(gobj, "");
2464
2571
  set_busy(gobj, false);
2465
2572
 
2466
- /* A position that arrived while the write was in flight: applied
2467
- * now, so a Back pressed mid-save is not simply lost. */
2468
- if(priv.pending_seg !== null) {
2469
- let seg = priv.pending_seg;
2470
- priv.pending_seg = null;
2471
- if(!error) {
2472
- apply_seg(gobj, seg);
2473
- if(priv.save_wrote) {
2474
- gobj_publish_event(gobj, "EV_RECORD_WRITTEN", {
2475
- treedb_name: gobj_read_str_attr(gobj, "treedb_name")
2476
- });
2477
- }
2478
- priv.save_wrote = false;
2479
- return 0;
2480
- }
2481
- }
2482
-
2483
2573
  if(error) {
2484
2574
  yui_shell_show_error(yui_shell_of(gobj), error, {t: t});
2485
2575
  }
@@ -2489,11 +2579,42 @@ function end_writes(gobj, error)
2489
2579
  });
2490
2580
  }
2491
2581
  priv.save_wrote = false;
2492
- render(gobj);
2493
- publish_check(gobj);
2582
+
2583
+ /* A position that arrived while the write was in flight: applied
2584
+ * now, so a Back pressed mid-save is not simply lost -- with an
2585
+ * error too. The host's url already says it, and a load cut by a
2586
+ * drop applies it (end_load()): the write kept the old screen, and
2587
+ * the url and the view disagreed. */
2588
+ if(seg !== null) {
2589
+ apply_seg(gobj, seg);
2590
+ } else {
2591
+ render(gobj);
2592
+ publish_check(gobj);
2593
+ }
2594
+ reload_if_owed(gobj);
2494
2595
  return error ? -1 : 0;
2495
2596
  }
2496
2597
 
2598
+ /***************************************************************
2599
+ * A write given up earlier turned out DONE while another one was
2600
+ * in flight (see ac_mt_command_answer()): the model is read again
2601
+ * now that nothing is.
2602
+ ***************************************************************/
2603
+ function reload_if_owed(gobj)
2604
+ {
2605
+ let priv = gobj.priv;
2606
+
2607
+ if(!priv.reload_after_write) {
2608
+ return 0;
2609
+ }
2610
+ priv.reload_after_write = false;
2611
+ if(!transport_in_session(gobj)) {
2612
+ priv.reload_on_open = true;
2613
+ return 0;
2614
+ }
2615
+ return request_model(gobj, true);
2616
+ }
2617
+
2497
2618
  /***************************************************************
2498
2619
  * The record a column form produces.
2499
2620
  *
@@ -2595,13 +2716,20 @@ function go(gobj, treedb_id, topic_name, diagram, mirror)
2595
2716
  {
2596
2717
  let priv = gobj.priv;
2597
2718
 
2719
+ /* Moving IS the answer to a position that waited: one left behind
2720
+ * sent a later load back to it. */
2721
+ priv.pending_seg = null;
2598
2722
  priv.treedb_id = treedb_id || "";
2599
2723
  priv.topic_name = topic_name || "";
2600
2724
  priv.diagram = !!diagram;
2601
2725
 
2726
+ /* No model and no load (a load in flight never gets here: ac_show()
2727
+ * keeps the position for it): nothing to draw, and ST_LOADING would
2728
+ * claim a load that nobody asked for -- the reconnect skipped the one
2729
+ * it owed BECAUSE the state said a load was running. */
2602
2730
  let state = "ST_TREEDBS";
2603
2731
  if(!priv.model) {
2604
- state = "ST_LOADING";
2732
+ state = "ST_IDLE";
2605
2733
  } else if(priv.model.treedbs.length === 0) {
2606
2734
  state = "ST_EMPTY";
2607
2735
  } else if(priv.treedb_id && priv.diagram) {
@@ -2647,18 +2775,39 @@ function position_seg(gobj)
2647
2775
  * empty screen: a shared link outlives the schema it points at.
2648
2776
  ***************************************************************/
2649
2777
  function apply_seg(gobj, seg)
2778
+ {
2779
+ let pos = seg_position(seg);
2780
+
2781
+ return go(gobj, pos.treedb_id, pos.topic_name, pos.diagram, false);
2782
+ }
2783
+
2784
+ function seg_position(seg)
2650
2785
  {
2651
2786
  let parts = String(seg || "").split("/").filter(x => x.length > 0);
2652
2787
  let treedb_id = parts[0] || "";
2653
2788
  let tail = parts[1] || "";
2654
2789
 
2655
2790
  if(!treedb_id) {
2656
- return go(gobj, "", "", false, false);
2791
+ return {treedb_id: "", topic_name: "", diagram: false};
2657
2792
  }
2658
2793
  if(tail === DIAGRAM_SEG) {
2659
- return go(gobj, treedb_id, "", true, false);
2794
+ return {treedb_id: treedb_id, topic_name: "", diagram: true};
2660
2795
  }
2661
- return go(gobj, treedb_id, tail, false, false);
2796
+ return {treedb_id: treedb_id, topic_name: tail, diagram: false};
2797
+ }
2798
+
2799
+ /***************************************************************
2800
+ * Where the operator is, taken WITHOUT a screen: there is no model
2801
+ * to draw it on yet, and the load that brings one goes there.
2802
+ ***************************************************************/
2803
+ function adopt_seg(gobj, seg)
2804
+ {
2805
+ let priv = gobj.priv;
2806
+ let pos = seg_position(seg);
2807
+
2808
+ priv.treedb_id = pos.treedb_id;
2809
+ priv.topic_name = pos.topic_name;
2810
+ priv.diagram = pos.diagram;
2662
2811
  }
2663
2812
 
2664
2813
 
@@ -2719,6 +2868,9 @@ function ac_mt_command_answer(gobj, event, kw, src)
2719
2868
  if(command === "nodes") {
2720
2869
  let topic_name = kw_get_str(gobj, md, "topic_name", "", 0);
2721
2870
  if(!is_current_load(gobj, md)) {
2871
+ if(result < 0 && kw_get_int(gobj, md, "round", 0, 0) === priv.dropped_round) {
2872
+ return 0; /* the drop that ended this load failed it: said once, in transport_dropped() */
2873
+ }
2722
2874
  log_warning(`${gobj_short_name(gobj)}: 'nodes' of '${topic_name}' answered ` +
2723
2875
  `for a load that is over: ignored`);
2724
2876
  return 0;
@@ -2734,9 +2886,7 @@ function ac_mt_command_answer(gobj, event, kw, src)
2734
2886
  }
2735
2887
  if(priv.load_error) {
2736
2888
  priv.model = null;
2737
- gobj_change_state(gobj, "ST_IDLE");
2738
- render(gobj);
2739
- return 0;
2889
+ return end_load(gobj);
2740
2890
  }
2741
2891
  /* Records straight from the store: what an undo would name is the
2742
2892
  * order that was ON SCREEN, and this is the stored one arriving.
@@ -2749,17 +2899,24 @@ function ac_mt_command_answer(gobj, event, kw, src)
2749
2899
  * against. Taken on the write patches instead, it measured every
2750
2900
  * version against itself. */
2751
2901
  start_measuring(gobj);
2752
- /* Back to where the operator was: the load may be a refresh, or
2753
- * the answer to a deep link that arrived before the model. */
2754
- return go(gobj, priv.treedb_id, priv.topic_name, priv.diagram, false);
2902
+ /* Back to where the operator was, or went while it loaded: the
2903
+ * load may be a refresh, or the answer to a deep link that
2904
+ * arrived before the model. */
2905
+ return end_load(gobj);
2755
2906
  }
2756
2907
 
2757
2908
  if(command === "update-node" || command === "delete-node") {
2758
2909
  let topic_name = kw_get_str(gobj, md, "topic_name", "", 0);
2759
2910
  if(!is_current_write(gobj, md)) {
2760
- log_warning(`${gobj_short_name(gobj)}: '${command}' of '${topic_name}' answered ` +
2761
- `for a write that is over (result ${result}): ignored`);
2762
- return 0;
2911
+ if(result < 0 && kw_get_int(gobj, md, "write", 0, 0) === priv.dropped_write) {
2912
+ return 0; /* the drop that ended this write failed it: said once, in transport_dropped() */
2913
+ }
2914
+ if(result < 0) {
2915
+ log_warning(`${gobj_short_name(gobj)}: '${command}' of '${topic_name}' answered ` +
2916
+ `for a write that is over (result ${result}): ignored`);
2917
+ return 0;
2918
+ }
2919
+ return late_write_done(gobj, command, topic_name, data);
2763
2920
  }
2764
2921
  if(result < 0) {
2765
2922
  return end_writes(gobj, comment || t("the treedb refused the write"));
@@ -2776,8 +2933,17 @@ function ac_mt_command_answer(gobj, event, kw, src)
2776
2933
  /* The treedb wrote it and did not describe it back: ask
2777
2934
  * again rather than draw a model that has drifted. */
2778
2935
  log_warning(`${gobj_short_name(gobj)}: '${command}' answered no record`);
2936
+ if(topic_name === T_TOPICS || topic_name === T_COLS) {
2937
+ let write = priv.save_queue[0];
2938
+ mark_written(gobj, topic_name, write ? write.record : null);
2939
+ }
2779
2940
  priv.save_queue = [];
2780
- request_model(gobj);
2941
+ priv.save_return = "";
2942
+ priv.save_wrote = false;
2943
+ gobj_publish_event(gobj, "EV_RECORD_WRITTEN", {
2944
+ treedb_name: gobj_read_str_attr(gobj, "treedb_name")
2945
+ });
2946
+ request_model(gobj, true);
2781
2947
  return 0;
2782
2948
  }
2783
2949
  }
@@ -2796,6 +2962,81 @@ function ac_mt_command_answer(gobj, event, kw, src)
2796
2962
  return 0;
2797
2963
  }
2798
2964
 
2965
+ /***************************************************************
2966
+ * A write this view gave up on (a drop, a deadline) answers that
2967
+ * it was DONE: the store holds a change the model does not show.
2968
+ * The model is read again -- now, or when what is in flight ends,
2969
+ * or on the reconnect. A load in flight needs nothing: it was
2970
+ * asked after the write, on the same transport, and reads it.
2971
+ *
2972
+ * Only when the answer reaches this view. A routing adapter that
2973
+ * settled the write on its own deadline (gui_agent's
2974
+ * C_AGENT_TREEDB_LINK) does not deliver its late answer again --
2975
+ * the view was answered once, as failed -- and echoes the node
2976
+ * event instead, which this view does not hear: there, Refresh is
2977
+ * what reads the store.
2978
+ *
2979
+ * It is a WRITE like any other for the host (EV_RECORD_WRITTEN):
2980
+ * the host was told nothing when it was given up, and it owns the
2981
+ * Save that publishes it. The reload keeps what this session wrote
2982
+ * (request_model()), and the record it answered marks its topic.
2983
+ ***************************************************************/
2984
+ function late_write_done(gobj, command, topic_name, data)
2985
+ {
2986
+ let priv = gobj.priv;
2987
+ let state = gobj_current_state(gobj);
2988
+
2989
+ log_warning(`${gobj_short_name(gobj)}: '${command}' of '${topic_name}' was given up, ` +
2990
+ `and it was DONE: the model is read again`);
2991
+ mark_written_record(gobj, topic_name, is_object(data) ? data
2992
+ : (Array.isArray(data) && is_object(data[0]) ? data[0] : null));
2993
+ gobj_publish_event(gobj, "EV_RECORD_WRITTEN", {
2994
+ treedb_name: gobj_read_str_attr(gobj, "treedb_name")
2995
+ });
2996
+ if(state === "ST_LOADING") {
2997
+ return 0;
2998
+ }
2999
+ if(state === "ST_SAVING") {
3000
+ priv.reload_after_write = true;
3001
+ return 0;
3002
+ }
3003
+ if(!transport_in_session(gobj)) {
3004
+ priv.reload_on_open = true;
3005
+ return 0;
3006
+ }
3007
+ return request_model(gobj, true);
3008
+ }
3009
+
3010
+ /***************************************************************
3011
+ * The topic a record written belongs to, read off the RECORD and
3012
+ * not off the screen: a write answered late may be about a topic
3013
+ * the operator has left. A column names its topic in its fkey
3014
+ * (`topics^<topic id>^cols`); a topic is its own id.
3015
+ ***************************************************************/
3016
+ function mark_written_record(gobj, topic_name, record)
3017
+ {
3018
+ let priv = gobj.priv;
3019
+
3020
+ if(!record) {
3021
+ return;
3022
+ }
3023
+ if(topic_name === T_TOPICS && typeof record.id === "string" && record.id) {
3024
+ priv.written[record.id] = true;
3025
+ return;
3026
+ }
3027
+ if(topic_name !== T_COLS) {
3028
+ return;
3029
+ }
3030
+ let refs = Array.isArray(record.topics) ? record.topics
3031
+ : (typeof record.topics === "string" ? [record.topics] : []);
3032
+ for(let ref of refs) {
3033
+ let parts = String(ref).split("^");
3034
+ if(parts.length >= 2 && parts[1]) {
3035
+ priv.written[parts[1]] = true;
3036
+ }
3037
+ }
3038
+ }
3039
+
2799
3040
  /***************************************************************
2800
3041
  * Remember which topic this write belongs to, which is what
2801
3042
  * "its version still has to move" is asked about.
@@ -2822,10 +3063,14 @@ function ac_show(gobj, event, kw, src)
2822
3063
  {
2823
3064
  let priv = gobj.priv;
2824
3065
  let seg = (kw && kw.subpath) || "";
3066
+ let state = gobj_current_state(gobj);
2825
3067
 
2826
- if(gobj_current_state(gobj) === "ST_SAVING") {
3068
+ /* Nothing but the end of what is in flight moves the editor out of
3069
+ * these two (see is_current_load()): the position waits, and the
3070
+ * last one sent wins. */
3071
+ if(state === "ST_SAVING" || state === "ST_LOADING") {
2827
3072
  priv.pending_seg = seg;
2828
- return 0; /* applied when the write lands (end_writes) */
3073
+ return 0; /* applied when the write lands (end_writes) or the load (end_load) */
2829
3074
  }
2830
3075
  return apply_seg(gobj, seg);
2831
3076
  }
@@ -2867,7 +3112,7 @@ function ac_transport_state(gobj, event, kw, src)
2867
3112
  if((!priv.model || priv.reload_on_open) &&
2868
3113
  state !== "ST_LOADING" && state !== "ST_SAVING") {
2869
3114
  priv.reload_on_open = false;
2870
- return request_model(gobj);
3115
+ return request_model(gobj, true);
2871
3116
  }
2872
3117
  render_toolbar(gobj);
2873
3118
  return 0;
@@ -2886,7 +3131,28 @@ function ac_language_changed(gobj, event, kw, src)
2886
3131
 
2887
3132
  function ac_refresh(gobj, event, kw, src)
2888
3133
  {
2889
- return request_model(gobj);
3134
+ return request_model(gobj, false);
3135
+ }
3136
+
3137
+ /***************************************************************
3138
+ * What a DIALOG sends while the model is being read again: the
3139
+ * Save of a form left open when the reload started, a
3140
+ * confirmation, an import. The loading screen took the rest of
3141
+ * the view away (request_model()); a dialog is the one thing the
3142
+ * operator can still reach, and what it asks is computed against
3143
+ * a model the answers in the air are about to replace.
3144
+ *
3145
+ * Refused, and SAID -- and the dialog stays open with what was
3146
+ * typed, so the same Save works once the load is in. Undeclared,
3147
+ * it answered "Event NOT DEFINED in state ST_LOADING" and the
3148
+ * edit was gone with no word (third independent review).
3149
+ ***************************************************************/
3150
+ function ac_wait_for_the_load(gobj, event, kw, src)
3151
+ {
3152
+ log_warning(`${gobj_short_name(gobj)}: ${event} refused, the schemas are loading`);
3153
+ yui_shell_show_error(yui_shell_of(gobj),
3154
+ "the schemas are loading: try again when they are in", {t: t});
3155
+ return -1;
2890
3156
  }
2891
3157
 
2892
3158
  /***************************************************************
@@ -3518,7 +3784,14 @@ function create_gclass(gclass_name)
3518
3784
  const states = [
3519
3785
  ["ST_IDLE", COMMON.slice()],
3520
3786
 
3521
- ["ST_LOADING", COMMON.slice()],
3787
+ ["ST_LOADING", COMMON.concat([
3788
+ ["EV_SAVE_COLUMN", ac_wait_for_the_load, null],
3789
+ ["EV_SAVE_TOPIC", ac_wait_for_the_load, null],
3790
+ ["EV_DELETE_ORPHAN", ac_wait_for_the_load, null],
3791
+ ["EV_PREVIEW_IMPORT", ac_wait_for_the_load, null],
3792
+ ["EV_APPLY_IMPORT", ac_wait_for_the_load, null],
3793
+ ["EV_CONFIRMED", ac_wait_for_the_load, null]
3794
+ ])],
3522
3795
 
3523
3796
  ["ST_EMPTY", COMMON.slice()],
3524
3797