@yuneta/gobj-ui 7.25.5 → 7.25.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -70,6 +70,12 @@
70
70
  * `update-node` / `delete-node` on the treedb service, and it may
71
71
  * be a routing adapter two hops away (the agent console's
72
72
  * C_AGENT_TREEDB_LINK) — this view cannot tell, and must not.
73
+ * A DROP of it (the host's EV_TRANSPORT_STATE, or a failure that
74
+ * arrives while the transport is out of session) ends the load or
75
+ * the write it cut, and the model is asked again when the session
76
+ * is back. Every request carries its round (`round` for a load,
77
+ * `write` for a write) in `__md_command__`, so an answer of what
78
+ * was cut is ignored instead of counted in the next one.
73
79
  *
74
80
  * Copyright (c) 2026, ArtGins.
75
81
  * All Rights Reserved.
@@ -108,6 +114,7 @@ import {
108
114
  clean_name,
109
115
  msg_iev_get_stack,
110
116
  kw_get_str,
117
+ kw_get_int,
111
118
  kw_get_dict,
112
119
  is_object,
113
120
  is_array,
@@ -198,6 +205,7 @@ let PRIVATE_DATA = {
198
205
  model: null, /* build_schema_model() of the above */
199
206
  pending: 0, /* `nodes` requests still in flight */
200
207
  load_error: "",
208
+ load_round: 0, /* which load an answer belongs to: echoed as `round` */
201
209
 
202
210
  /* Where the operator is. The url carries it; these mirror it. */
203
211
  treedb_id: "",
@@ -220,9 +228,14 @@ let PRIVATE_DATA = {
220
228
  save_queue: null,
221
229
  save_return: "",
222
230
  save_wrote: false,
231
+ write_tag: 0, /* which write an answer belongs to: echoed as `write` */
223
232
  connected: false,
233
+ reload_on_open: false, /* a drop cut a load or a write: ask the model again */
234
+ dropped_round: 0, /* the load a drop ended: its failures are the drop, said once */
235
+ dropped_write: 0, /* the same for the write a drop ended */
236
+ reload_after_write: false, /* a write given up turned out DONE while another was in flight */
224
237
 
225
- /* A position that arrived while a write was in flight */
238
+ /* A position that arrived while a load or a write was in flight */
226
239
  pending_seg: null,
227
240
 
228
241
  /* The import dialog's plan and the two nodes it draws into */
@@ -458,6 +471,7 @@ function request_model(gobj)
458
471
  priv.records = {treedbs: [], topics: [], cols: []};
459
472
  priv.load_error = "";
460
473
  priv.pending = 0;
474
+ priv.load_round++;
461
475
  gobj_change_state(gobj, "ST_LOADING");
462
476
  show_notice(gobj, "loading the schemas");
463
477
 
@@ -465,7 +479,7 @@ function request_model(gobj)
465
479
  if(remote_command(gobj, "nodes", {
466
480
  topic_name: topic_name,
467
481
  options: {list_dict: true}
468
- }) === 0) {
482
+ }, {round: priv.load_round}) === 0) {
469
483
  priv.pending++;
470
484
  }
471
485
  }
@@ -487,7 +501,7 @@ function request_model(gobj)
487
501
  * answer says which topic it is about, since the command is
488
502
  * `nodes` for all three.
489
503
  ***************************************************************/
490
- function remote_command(gobj, command, kw)
504
+ function remote_command(gobj, command, kw, tag)
491
505
  {
492
506
  let remote = gobj_read_pointer_attr(gobj, "gobj_remote_yuno");
493
507
  let treedb_name = gobj_read_str_attr(gobj, "treedb_name");
@@ -512,11 +526,11 @@ function remote_command(gobj, command, kw)
512
526
  * whichever one is under this view — and `record_id` is here and
513
527
  * NOT at the top level because a delete answers with nothing, and a
514
528
  * parameter the treedb does not know has no business travelling. */
515
- full_kw.__md_command__ = {
529
+ full_kw.__md_command__ = Object.assign({
516
530
  purpose: PURPOSE,
517
531
  topic_name: (kw && kw.topic_name) || "",
518
532
  record_id: (kw && kw.record_id) || ""
519
- };
533
+ }, tag || {});
520
534
  delete full_kw.record_id;
521
535
 
522
536
  let ret = gobj_command(remote, command, full_kw, gobj);
@@ -2362,17 +2376,121 @@ function run_next_write(gobj)
2362
2376
  let command = write_command(write.op);
2363
2377
  let options = write_options(write.op);
2364
2378
 
2379
+ priv.write_tag++;
2365
2380
  if(remote_command(gobj, command, {
2366
2381
  topic_name: write.topic_name,
2367
2382
  record: write.record,
2368
2383
  options: options,
2369
2384
  record_id: write.record.id || ""
2370
- }) < 0) {
2385
+ }, {write: priv.write_tag}) < 0) {
2371
2386
  return end_writes(gobj, t("cannot reach the treedb"));
2372
2387
  }
2373
2388
  return 0;
2374
2389
  }
2375
2390
 
2391
+ /***************************************************************
2392
+ * Does the transport say it is in session? The state the library
2393
+ * reads off every transport (C_IEVENT_CLI and the routing adapters
2394
+ * name it the same).
2395
+ ***************************************************************/
2396
+ function transport_in_session(gobj)
2397
+ {
2398
+ let remote = gobj_read_pointer_attr(gobj, "gobj_remote_yuno");
2399
+
2400
+ return !!remote && gobj_current_state(remote) === "ST_SESSION";
2401
+ }
2402
+
2403
+ /***************************************************************
2404
+ * Is this answer of the load in flight, or of the write in
2405
+ * flight? Each request carries its round in `__md_command__`, and
2406
+ * one the drop abandoned (or a deadline settled) can still come
2407
+ * back: counted, it ended the next load with the old data or
2408
+ * moved the next write's queue.
2409
+ *
2410
+ * The state is part of the question, and that is only true
2411
+ * because NOTHING moves the editor out of ST_LOADING or ST_SAVING
2412
+ * but the end of what is in flight: a position the host sends
2413
+ * meanwhile waits in `pending_seg` (ac_show()). 7.25.6 let EV_SHOW
2414
+ * move a RELOAD out of ST_LOADING, and every answer of it was then
2415
+ * "for a load that is over": the records it had emptied stayed
2416
+ * empty, and the next write patched them into a model with no
2417
+ * treedb at all.
2418
+ ***************************************************************/
2419
+ function is_current_load(gobj, md)
2420
+ {
2421
+ return gobj_current_state(gobj) === "ST_LOADING" &&
2422
+ kw_get_int(gobj, md, "round", 0, 0) === gobj.priv.load_round;
2423
+ }
2424
+
2425
+ function is_current_write(gobj, md)
2426
+ {
2427
+ return gobj_current_state(gobj) === "ST_SAVING" &&
2428
+ kw_get_int(gobj, md, "write", 0, 0) === gobj.priv.write_tag;
2429
+ }
2430
+
2431
+ /***************************************************************
2432
+ * The transport dropped under a load or a write: its answers
2433
+ * will never come. The load goes back to the screen it replaced
2434
+ * (or to "not connected" when there was none), the write ends
2435
+ * with the drop said -- whether it landed is unknown -- and both
2436
+ * mark the model to be asked again when the session is back.
2437
+ * Bumping the round makes an answer of what was cut stale.
2438
+ ***************************************************************/
2439
+ function transport_dropped(gobj)
2440
+ {
2441
+ let priv = gobj.priv;
2442
+ let state = gobj_current_state(gobj);
2443
+
2444
+ priv.reload_on_open = true;
2445
+ if(state === "ST_SAVING") {
2446
+ log_warning(`${gobj_short_name(gobj)}: the transport dropped during a write: ` +
2447
+ `the model is read again on the reconnect`);
2448
+ priv.dropped_write = priv.write_tag;
2449
+ priv.write_tag++;
2450
+ return end_writes(gobj, "the connection dropped during the write");
2451
+ }
2452
+ if(state === "ST_LOADING") {
2453
+ /* Said HERE, once: the transport settles every request the drop
2454
+ * cut, and those failures are this one event (see
2455
+ * ac_mt_command_answer()). */
2456
+ log_warning(`${gobj_short_name(gobj)}: the transport dropped during the load: ` +
2457
+ `the model is read again on the reconnect`);
2458
+ priv.dropped_round = priv.load_round;
2459
+ priv.load_round++;
2460
+ priv.pending = 0;
2461
+ priv.load_error = "";
2462
+ return end_load(gobj);
2463
+ }
2464
+ return 0;
2465
+ }
2466
+
2467
+ /***************************************************************
2468
+ * A load is over -- landed, failed or cut -- and the editor goes
2469
+ * where the operator is: the position the host sent while it was
2470
+ * in flight (ac_show() kept it), or the one it had. Without a
2471
+ * model there is no screen to go to: the position is kept for the
2472
+ * next load, and the editor says why it is empty.
2473
+ ***************************************************************/
2474
+ function end_load(gobj)
2475
+ {
2476
+ let priv = gobj.priv;
2477
+ let seg = priv.pending_seg;
2478
+
2479
+ priv.pending_seg = null;
2480
+ if(priv.model) {
2481
+ if(seg !== null) {
2482
+ return apply_seg(gobj, seg);
2483
+ }
2484
+ return go(gobj, priv.treedb_id, priv.topic_name, priv.diagram, false);
2485
+ }
2486
+ if(seg !== null) {
2487
+ adopt_seg(gobj, seg);
2488
+ }
2489
+ gobj_change_state(gobj, "ST_IDLE");
2490
+ render(gobj);
2491
+ return 0;
2492
+ }
2493
+
2376
2494
  /***************************************************************
2377
2495
  * The queue is done, or gave up. Either way the model is what
2378
2496
  * the answers made it, so the screen is redrawn from it and the
@@ -2402,7 +2520,7 @@ function end_writes(gobj, error)
2402
2520
  });
2403
2521
  }
2404
2522
  priv.save_wrote = false;
2405
- return 0;
2523
+ return reload_if_owed(gobj);
2406
2524
  }
2407
2525
  }
2408
2526
 
@@ -2417,9 +2535,30 @@ function end_writes(gobj, error)
2417
2535
  priv.save_wrote = false;
2418
2536
  render(gobj);
2419
2537
  publish_check(gobj);
2538
+ reload_if_owed(gobj);
2420
2539
  return error ? -1 : 0;
2421
2540
  }
2422
2541
 
2542
+ /***************************************************************
2543
+ * A write given up earlier turned out DONE while another one was
2544
+ * in flight (see ac_mt_command_answer()): the model is read again
2545
+ * now that nothing is.
2546
+ ***************************************************************/
2547
+ function reload_if_owed(gobj)
2548
+ {
2549
+ let priv = gobj.priv;
2550
+
2551
+ if(!priv.reload_after_write) {
2552
+ return 0;
2553
+ }
2554
+ priv.reload_after_write = false;
2555
+ if(!transport_in_session(gobj)) {
2556
+ priv.reload_on_open = true;
2557
+ return 0;
2558
+ }
2559
+ return request_model(gobj);
2560
+ }
2561
+
2423
2562
  /***************************************************************
2424
2563
  * The record a column form produces.
2425
2564
  *
@@ -2525,9 +2664,13 @@ function go(gobj, treedb_id, topic_name, diagram, mirror)
2525
2664
  priv.topic_name = topic_name || "";
2526
2665
  priv.diagram = !!diagram;
2527
2666
 
2667
+ /* No model and no load (a load in flight never gets here: ac_show()
2668
+ * keeps the position for it): nothing to draw, and ST_LOADING would
2669
+ * claim a load that nobody asked for -- the reconnect skipped the one
2670
+ * it owed BECAUSE the state said a load was running. */
2528
2671
  let state = "ST_TREEDBS";
2529
2672
  if(!priv.model) {
2530
- state = "ST_LOADING";
2673
+ state = "ST_IDLE";
2531
2674
  } else if(priv.model.treedbs.length === 0) {
2532
2675
  state = "ST_EMPTY";
2533
2676
  } else if(priv.treedb_id && priv.diagram) {
@@ -2573,18 +2716,39 @@ function position_seg(gobj)
2573
2716
  * empty screen: a shared link outlives the schema it points at.
2574
2717
  ***************************************************************/
2575
2718
  function apply_seg(gobj, seg)
2719
+ {
2720
+ let pos = seg_position(seg);
2721
+
2722
+ return go(gobj, pos.treedb_id, pos.topic_name, pos.diagram, false);
2723
+ }
2724
+
2725
+ function seg_position(seg)
2576
2726
  {
2577
2727
  let parts = String(seg || "").split("/").filter(x => x.length > 0);
2578
2728
  let treedb_id = parts[0] || "";
2579
2729
  let tail = parts[1] || "";
2580
2730
 
2581
2731
  if(!treedb_id) {
2582
- return go(gobj, "", "", false, false);
2732
+ return {treedb_id: "", topic_name: "", diagram: false};
2583
2733
  }
2584
2734
  if(tail === DIAGRAM_SEG) {
2585
- return go(gobj, treedb_id, "", true, false);
2735
+ return {treedb_id: treedb_id, topic_name: "", diagram: true};
2586
2736
  }
2587
- return go(gobj, treedb_id, tail, false, false);
2737
+ return {treedb_id: treedb_id, topic_name: tail, diagram: false};
2738
+ }
2739
+
2740
+ /***************************************************************
2741
+ * Where the operator is, taken WITHOUT a screen: there is no model
2742
+ * to draw it on yet, and the load that brings one goes there.
2743
+ ***************************************************************/
2744
+ function adopt_seg(gobj, seg)
2745
+ {
2746
+ let priv = gobj.priv;
2747
+ let pos = seg_position(seg);
2748
+
2749
+ priv.treedb_id = pos.treedb_id;
2750
+ priv.topic_name = pos.topic_name;
2751
+ priv.diagram = pos.diagram;
2588
2752
  }
2589
2753
 
2590
2754
 
@@ -2627,8 +2791,31 @@ function ac_mt_command_answer(gobj, event, kw, src)
2627
2791
  return 0; /* another panel's answer on a shared transport */
2628
2792
  }
2629
2793
 
2794
+ /* A failure while the transport is not in session is the DROP, not a
2795
+ * refusal: a routing adapter settles what it had in flight when its
2796
+ * session closes, and that can arrive before the host's
2797
+ * EV_TRANSPORT_STATE. Read as a refusal, the write it ended looked
2798
+ * refused, and the reconnect did not reload a model it may have
2799
+ * changed. */
2800
+ if(result < 0 && !transport_in_session(gobj) &&
2801
+ (gobj_current_state(gobj) === "ST_LOADING" ||
2802
+ gobj_current_state(gobj) === "ST_SAVING")) {
2803
+ if((command === "nodes" && is_current_load(gobj, md)) ||
2804
+ (command !== "nodes" && is_current_write(gobj, md))) {
2805
+ return transport_dropped(gobj);
2806
+ }
2807
+ }
2808
+
2630
2809
  if(command === "nodes") {
2631
2810
  let topic_name = kw_get_str(gobj, md, "topic_name", "", 0);
2811
+ if(!is_current_load(gobj, md)) {
2812
+ if(result < 0 && kw_get_int(gobj, md, "round", 0, 0) === priv.dropped_round) {
2813
+ return 0; /* the drop that ended this load failed it: said once, in transport_dropped() */
2814
+ }
2815
+ log_warning(`${gobj_short_name(gobj)}: 'nodes' of '${topic_name}' answered ` +
2816
+ `for a load that is over: ignored`);
2817
+ return 0;
2818
+ }
2632
2819
  priv.pending--;
2633
2820
  if(result < 0) {
2634
2821
  priv.load_error = comment || t("cannot load the schemas");
@@ -2640,9 +2827,7 @@ function ac_mt_command_answer(gobj, event, kw, src)
2640
2827
  }
2641
2828
  if(priv.load_error) {
2642
2829
  priv.model = null;
2643
- gobj_change_state(gobj, "ST_IDLE");
2644
- render(gobj);
2645
- return 0;
2830
+ return end_load(gobj);
2646
2831
  }
2647
2832
  /* Records straight from the store: what an undo would name is the
2648
2833
  * order that was ON SCREEN, and this is the stored one arriving.
@@ -2655,13 +2840,25 @@ function ac_mt_command_answer(gobj, event, kw, src)
2655
2840
  * against. Taken on the write patches instead, it measured every
2656
2841
  * version against itself. */
2657
2842
  start_measuring(gobj);
2658
- /* Back to where the operator was: the load may be a refresh, or
2659
- * the answer to a deep link that arrived before the model. */
2660
- return go(gobj, priv.treedb_id, priv.topic_name, priv.diagram, false);
2843
+ /* Back to where the operator was, or went while it loaded: the
2844
+ * load may be a refresh, or the answer to a deep link that
2845
+ * arrived before the model. */
2846
+ return end_load(gobj);
2661
2847
  }
2662
2848
 
2663
2849
  if(command === "update-node" || command === "delete-node") {
2664
2850
  let topic_name = kw_get_str(gobj, md, "topic_name", "", 0);
2851
+ if(!is_current_write(gobj, md)) {
2852
+ if(result < 0 && kw_get_int(gobj, md, "write", 0, 0) === priv.dropped_write) {
2853
+ return 0; /* the drop that ended this write failed it: said once, in transport_dropped() */
2854
+ }
2855
+ if(result < 0) {
2856
+ log_warning(`${gobj_short_name(gobj)}: '${command}' of '${topic_name}' answered ` +
2857
+ `for a write that is over (result ${result}): ignored`);
2858
+ return 0;
2859
+ }
2860
+ return late_write_done(gobj, command, topic_name);
2861
+ }
2665
2862
  if(result < 0) {
2666
2863
  return end_writes(gobj, comment || t("the treedb refused the write"));
2667
2864
  }
@@ -2697,6 +2894,41 @@ function ac_mt_command_answer(gobj, event, kw, src)
2697
2894
  return 0;
2698
2895
  }
2699
2896
 
2897
+ /***************************************************************
2898
+ * A write this view gave up on (a drop, a deadline) answers that
2899
+ * it was DONE: the store holds a change the model does not show.
2900
+ * The model is read again -- now, or when what is in flight ends,
2901
+ * or on the reconnect. A load in flight needs nothing: it was
2902
+ * asked after the write, on the same transport, and reads it.
2903
+ *
2904
+ * Only when the answer reaches this view. A routing adapter that
2905
+ * settled the write on its own deadline (gui_agent's
2906
+ * C_AGENT_TREEDB_LINK) does not deliver its late answer again --
2907
+ * the view was answered once, as failed -- and echoes the node
2908
+ * event instead, which this view does not hear: there, Refresh is
2909
+ * what reads the store.
2910
+ ***************************************************************/
2911
+ function late_write_done(gobj, command, topic_name)
2912
+ {
2913
+ let priv = gobj.priv;
2914
+ let state = gobj_current_state(gobj);
2915
+
2916
+ log_warning(`${gobj_short_name(gobj)}: '${command}' of '${topic_name}' was given up, ` +
2917
+ `and it was DONE: the model is read again`);
2918
+ if(state === "ST_LOADING") {
2919
+ return 0;
2920
+ }
2921
+ if(state === "ST_SAVING") {
2922
+ priv.reload_after_write = true;
2923
+ return 0;
2924
+ }
2925
+ if(!transport_in_session(gobj)) {
2926
+ priv.reload_on_open = true;
2927
+ return 0;
2928
+ }
2929
+ return request_model(gobj);
2930
+ }
2931
+
2700
2932
  /***************************************************************
2701
2933
  * Remember which topic this write belongs to, which is what
2702
2934
  * "its version still has to move" is asked about.
@@ -2723,10 +2955,14 @@ function ac_show(gobj, event, kw, src)
2723
2955
  {
2724
2956
  let priv = gobj.priv;
2725
2957
  let seg = (kw && kw.subpath) || "";
2958
+ let state = gobj_current_state(gobj);
2726
2959
 
2727
- if(gobj_current_state(gobj) === "ST_SAVING") {
2960
+ /* Nothing but the end of what is in flight moves the editor out of
2961
+ * these two (see is_current_load()): the position waits, and the
2962
+ * last one sent wins. */
2963
+ if(state === "ST_SAVING" || state === "ST_LOADING") {
2728
2964
  priv.pending_seg = seg;
2729
- return 0; /* applied when the write lands (end_writes) */
2965
+ return 0; /* applied when the write lands (end_writes) or the load (end_load) */
2730
2966
  }
2731
2967
  return apply_seg(gobj, seg);
2732
2968
  }
@@ -2743,16 +2979,31 @@ function ac_hide(gobj, event, kw, src)
2743
2979
  }
2744
2980
 
2745
2981
  /***************************************************************
2746
- * The session came up or went down. The model stays either way
2747
- * — a schema does not change under you — but a first load may
2748
- * have had no transport to run on.
2982
+ * The session came up or went down. The model stays -- a schema
2983
+ * does not change under you -- unless the drop cut a load or a
2984
+ * write (transport_dropped()): then it is asked again on the way
2985
+ * back, as it is when a first load had no transport to run on.
2986
+ *
2987
+ * Nothing else ends a load or a write the drop cut: their answers
2988
+ * will never come, and waiting in ST_LOADING skipped the reload
2989
+ * itself (M-1 of the independent review of 7.25.4).
2749
2990
  ***************************************************************/
2750
2991
  function ac_transport_state(gobj, event, kw, src)
2751
2992
  {
2752
2993
  let priv = gobj.priv;
2994
+ let state = gobj_current_state(gobj);
2753
2995
 
2754
2996
  priv.connected = !!(kw && kw.connected);
2755
- if(priv.connected && !priv.model && gobj_current_state(gobj) !== "ST_LOADING") {
2997
+ if(!priv.connected) {
2998
+ if(state === "ST_LOADING" || state === "ST_SAVING") {
2999
+ return transport_dropped(gobj);
3000
+ }
3001
+ render_toolbar(gobj);
3002
+ return 0;
3003
+ }
3004
+ if((!priv.model || priv.reload_on_open) &&
3005
+ state !== "ST_LOADING" && state !== "ST_SAVING") {
3006
+ priv.reload_on_open = false;
2756
3007
  return request_model(gobj);
2757
3008
  }
2758
3009
  render_toolbar(gobj);