@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.
- package/README.md +86 -1
- package/dist/gobj-ui.cjs.js +287 -56
- package/dist/gobj-ui.es.js +288 -57
- package/package.json +1 -1
- package/src/c_yui_schema_editor.js +165 -32
- package/src/c_yui_shell.js +61 -8
- package/src/c_yui_treedb_topic_with_form.js +12 -1
- package/src/c_yui_treedb_topics.js +41 -10
- package/src/form_time_value.js +23 -0
- package/src/form_writes_in_flight.js +32 -13
- package/src/form_writes_in_flight.test.js +18 -3
- package/src/host_drafts.js +28 -2
- package/src/host_drafts.test.js +24 -1
- package/src/schema_editor_drafts.wiring.test.js +176 -0
- package/src/schema_editor_transport.wiring.test.js +284 -0
- package/src/schema_version_bump.test.js +10 -5
- package/src/treedb_topics_transport.wiring.test.js +228 -0
- package/src/treedb_write_plan.js +32 -1
- package/src/treedb_write_plan.test.js +22 -1
package/package.json
CHANGED
|
@@ -26,8 +26,9 @@
|
|
|
26
26
|
* the owner's design of M36 (2026-09-21 review) every write here
|
|
27
27
|
* raised both versions, so an edit half made was already the
|
|
28
28
|
* schema of the next start. The topic list marks what this session
|
|
29
|
-
* wrote and has not saved
|
|
30
|
-
* what forgets it
|
|
29
|
+
* wrote and has not saved (the host's EV_REFRESH after a save is
|
|
30
|
+
* what forgets it) and what the host says is not saved (EV_DRAFTS,
|
|
31
|
+
* replaced whole each time it is sent).
|
|
31
32
|
*
|
|
32
33
|
* WHAT ELSE IS HERE, and why each is here and not somewhere else:
|
|
33
34
|
*
|
|
@@ -69,6 +70,12 @@
|
|
|
69
70
|
* `update-node` / `delete-node` on the treedb service, and it may
|
|
70
71
|
* be a routing adapter two hops away (the agent console's
|
|
71
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.
|
|
72
79
|
*
|
|
73
80
|
* Copyright (c) 2026, ArtGins.
|
|
74
81
|
* All Rights Reserved.
|
|
@@ -107,13 +114,14 @@ import {
|
|
|
107
114
|
clean_name,
|
|
108
115
|
msg_iev_get_stack,
|
|
109
116
|
kw_get_str,
|
|
117
|
+
kw_get_int,
|
|
110
118
|
kw_get_dict,
|
|
111
119
|
is_object,
|
|
112
120
|
is_array,
|
|
113
121
|
empty_string,
|
|
114
122
|
} from "@yuneta/gobj-js";
|
|
115
123
|
import {yui_tint} from "./bulma_tint.js";
|
|
116
|
-
import {
|
|
124
|
+
import {host_draft_ids} from "./host_drafts.js";
|
|
117
125
|
|
|
118
126
|
import "./c_yui_schema_editor.css";
|
|
119
127
|
|
|
@@ -197,6 +205,7 @@ let PRIVATE_DATA = {
|
|
|
197
205
|
model: null, /* build_schema_model() of the above */
|
|
198
206
|
pending: 0, /* `nodes` requests still in flight */
|
|
199
207
|
load_error: "",
|
|
208
|
+
load_round: 0, /* which load an answer belongs to: echoed as `round` */
|
|
200
209
|
|
|
201
210
|
/* Where the operator is. The url carries it; these mirror it. */
|
|
202
211
|
treedb_id: "",
|
|
@@ -207,6 +216,8 @@ let PRIVATE_DATA = {
|
|
|
207
216
|
* version still has to move. */
|
|
208
217
|
baseline: null, /* {topic id: topic_version} at load time */
|
|
209
218
|
written: null, /* {topic id: true} */
|
|
219
|
+
host_drafts: null, /* {treedb: [topic names]}, the host's last EV_DRAFTS */
|
|
220
|
+
host_ids: null, /* host_drafts resolved on the model, {topic id: true} */
|
|
210
221
|
order_undo: null, /* {topic id: [{id, order}]} before the first drag */
|
|
211
222
|
|
|
212
223
|
/* Children and overlays */
|
|
@@ -217,7 +228,9 @@ let PRIVATE_DATA = {
|
|
|
217
228
|
save_queue: null,
|
|
218
229
|
save_return: "",
|
|
219
230
|
save_wrote: false,
|
|
231
|
+
write_tag: 0, /* which write an answer belongs to: echoed as `write` */
|
|
220
232
|
connected: false,
|
|
233
|
+
reload_on_open: false, /* a drop cut a load or a write: ask the model again */
|
|
221
234
|
|
|
222
235
|
/* A position that arrived while a write was in flight */
|
|
223
236
|
pending_seg: null,
|
|
@@ -250,6 +263,7 @@ function mt_create(gobj)
|
|
|
250
263
|
priv.baseline = {};
|
|
251
264
|
priv.written = {};
|
|
252
265
|
priv.host_drafts = {}; /* what the host says is a draft, {treedb: [topics]} (EV_DRAFTS) */
|
|
266
|
+
priv.host_ids = {}; /* the same, as {topic id: true} of the model */
|
|
253
267
|
priv.save_queue = [];
|
|
254
268
|
|
|
255
269
|
build_ui(gobj);
|
|
@@ -454,6 +468,7 @@ function request_model(gobj)
|
|
|
454
468
|
priv.records = {treedbs: [], topics: [], cols: []};
|
|
455
469
|
priv.load_error = "";
|
|
456
470
|
priv.pending = 0;
|
|
471
|
+
priv.load_round++;
|
|
457
472
|
gobj_change_state(gobj, "ST_LOADING");
|
|
458
473
|
show_notice(gobj, "loading the schemas");
|
|
459
474
|
|
|
@@ -461,7 +476,7 @@ function request_model(gobj)
|
|
|
461
476
|
if(remote_command(gobj, "nodes", {
|
|
462
477
|
topic_name: topic_name,
|
|
463
478
|
options: {list_dict: true}
|
|
464
|
-
}) === 0) {
|
|
479
|
+
}, {round: priv.load_round}) === 0) {
|
|
465
480
|
priv.pending++;
|
|
466
481
|
}
|
|
467
482
|
}
|
|
@@ -483,7 +498,7 @@ function request_model(gobj)
|
|
|
483
498
|
* answer says which topic it is about, since the command is
|
|
484
499
|
* `nodes` for all three.
|
|
485
500
|
***************************************************************/
|
|
486
|
-
function remote_command(gobj, command, kw)
|
|
501
|
+
function remote_command(gobj, command, kw, tag)
|
|
487
502
|
{
|
|
488
503
|
let remote = gobj_read_pointer_attr(gobj, "gobj_remote_yuno");
|
|
489
504
|
let treedb_name = gobj_read_str_attr(gobj, "treedb_name");
|
|
@@ -508,11 +523,11 @@ function remote_command(gobj, command, kw)
|
|
|
508
523
|
* whichever one is under this view — and `record_id` is here and
|
|
509
524
|
* NOT at the top level because a delete answers with nothing, and a
|
|
510
525
|
* parameter the treedb does not know has no business travelling. */
|
|
511
|
-
full_kw.__md_command__ = {
|
|
526
|
+
full_kw.__md_command__ = Object.assign({
|
|
512
527
|
purpose: PURPOSE,
|
|
513
528
|
topic_name: (kw && kw.topic_name) || "",
|
|
514
529
|
record_id: (kw && kw.record_id) || ""
|
|
515
|
-
};
|
|
530
|
+
}, tag || {});
|
|
516
531
|
delete full_kw.record_id;
|
|
517
532
|
|
|
518
533
|
let ret = gobj_command(remote, command, full_kw, gobj);
|
|
@@ -564,6 +579,7 @@ function start_measuring(gobj)
|
|
|
564
579
|
|
|
565
580
|
priv.baseline = {};
|
|
566
581
|
priv.written = {};
|
|
582
|
+
priv.host_ids = {};
|
|
567
583
|
|
|
568
584
|
if(!priv.model) {
|
|
569
585
|
return;
|
|
@@ -573,29 +589,39 @@ function start_measuring(gobj)
|
|
|
573
589
|
priv.baseline[topic.id] = topic.topic_version;
|
|
574
590
|
}
|
|
575
591
|
}
|
|
576
|
-
/* A draft is a draft after a reload too: what the host said
|
|
577
|
-
*
|
|
578
|
-
|
|
592
|
+
/* A draft is a draft after a reload too: what the host said last
|
|
593
|
+
* is resolved against the new model (host_drafts.js), apart from
|
|
594
|
+
* what this session writes, until the host says otherwise. */
|
|
595
|
+
priv.host_ids = host_draft_ids(priv.model, priv.host_drafts);
|
|
579
596
|
}
|
|
580
597
|
|
|
581
598
|
/***************************************************************
|
|
582
|
-
* "This topic
|
|
599
|
+
* "This topic holds an edit that is not saved."
|
|
583
600
|
*
|
|
584
601
|
* An edit is a DRAFT: it moves no version and reaches no treedb
|
|
585
602
|
* until the host saves it (C_TREEDB's save-schema, which raises the
|
|
586
|
-
* versions of what changed)
|
|
587
|
-
*
|
|
603
|
+
* versions of what changed). Two sources say a topic is one, and
|
|
604
|
+
* they are kept APART because they are forgotten differently:
|
|
605
|
+
* - `written`: what THIS session wrote since the model was loaded
|
|
606
|
+
* (a reload, the host's EV_REFRESH, forgets it -- the store says
|
|
607
|
+
* what is left);
|
|
608
|
+
* - `host_ids`: what the host said last (EV_DRAFTS), replaced WHOLE
|
|
609
|
+
* by the next EV_DRAFTS. Folded into `written`, a topic the host
|
|
610
|
+
* named once stayed a draft after the Save that published it.
|
|
588
611
|
*
|
|
589
612
|
* ONE predicate, because it is asked in three places -- the topic
|
|
590
613
|
* list draws a chip with it, the column view a banner, the export
|
|
591
614
|
* a warning -- and copies of a rule are how they drift.
|
|
592
615
|
***************************************************************/
|
|
593
|
-
function topic_is_draft(
|
|
616
|
+
function topic_is_draft(priv, topic)
|
|
594
617
|
{
|
|
595
|
-
if(!topic || !
|
|
618
|
+
if(!topic || !priv) {
|
|
596
619
|
return false;
|
|
597
620
|
}
|
|
598
|
-
|
|
621
|
+
if(priv.written && priv.written[topic.id]) {
|
|
622
|
+
return true;
|
|
623
|
+
}
|
|
624
|
+
return (priv.host_ids && priv.host_ids[topic.id])? true: false;
|
|
599
625
|
}
|
|
600
626
|
|
|
601
627
|
/***************************************************************
|
|
@@ -1050,7 +1076,7 @@ function render_topics(gobj, $body)
|
|
|
1050
1076
|
let twice = repeated_names(treedb.topics);
|
|
1051
1077
|
let $rows = [];
|
|
1052
1078
|
for(let topic of treedb.topics) {
|
|
1053
|
-
let pending = topic_is_draft(priv
|
|
1079
|
+
let pending = topic_is_draft(priv, topic);
|
|
1054
1080
|
/* Two topics of one name: the id is what tells them apart, and
|
|
1055
1081
|
* it is what the url has to carry — addressing by name would
|
|
1056
1082
|
* open the first one whichever row was clicked. */
|
|
@@ -1256,7 +1282,7 @@ function draft_banner(gobj, topic)
|
|
|
1256
1282
|
{
|
|
1257
1283
|
let priv = gobj.priv;
|
|
1258
1284
|
|
|
1259
|
-
if(!topic_is_draft(priv
|
|
1285
|
+
if(!topic_is_draft(priv, topic)) {
|
|
1260
1286
|
return null;
|
|
1261
1287
|
}
|
|
1262
1288
|
if(is_readonly(gobj)) {
|
|
@@ -1997,7 +2023,7 @@ function open_report(gobj, findings, treedb_id)
|
|
|
1997
2023
|
function open_export(gobj, treedb)
|
|
1998
2024
|
{
|
|
1999
2025
|
let priv = gobj.priv;
|
|
2000
|
-
let drafts = treedb.topics.some((topic) => topic_is_draft(priv
|
|
2026
|
+
let drafts = treedb.topics.some((topic) => topic_is_draft(priv, topic));
|
|
2001
2027
|
let c_text = schema_to_c(treedb);
|
|
2002
2028
|
let json_text = JSON.stringify(schema_to_json(treedb), null, 4);
|
|
2003
2029
|
|
|
@@ -2347,17 +2373,80 @@ function run_next_write(gobj)
|
|
|
2347
2373
|
let command = write_command(write.op);
|
|
2348
2374
|
let options = write_options(write.op);
|
|
2349
2375
|
|
|
2376
|
+
priv.write_tag++;
|
|
2350
2377
|
if(remote_command(gobj, command, {
|
|
2351
2378
|
topic_name: write.topic_name,
|
|
2352
2379
|
record: write.record,
|
|
2353
2380
|
options: options,
|
|
2354
2381
|
record_id: write.record.id || ""
|
|
2355
|
-
}) < 0) {
|
|
2382
|
+
}, {write: priv.write_tag}) < 0) {
|
|
2356
2383
|
return end_writes(gobj, t("cannot reach the treedb"));
|
|
2357
2384
|
}
|
|
2358
2385
|
return 0;
|
|
2359
2386
|
}
|
|
2360
2387
|
|
|
2388
|
+
/***************************************************************
|
|
2389
|
+
* Does the transport say it is in session? The state the library
|
|
2390
|
+
* reads off every transport (C_IEVENT_CLI and the routing adapters
|
|
2391
|
+
* name it the same).
|
|
2392
|
+
***************************************************************/
|
|
2393
|
+
function transport_in_session(gobj)
|
|
2394
|
+
{
|
|
2395
|
+
let remote = gobj_read_pointer_attr(gobj, "gobj_remote_yuno");
|
|
2396
|
+
|
|
2397
|
+
return !!remote && gobj_current_state(remote) === "ST_SESSION";
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
/***************************************************************
|
|
2401
|
+
* Is this answer of the load in flight, or of the write in
|
|
2402
|
+
* flight? Each request carries its round in `__md_command__`, and
|
|
2403
|
+
* one the drop abandoned (or a deadline settled) can still come
|
|
2404
|
+
* back: counted, it ended the next load with the old data or
|
|
2405
|
+
* moved the next write's queue.
|
|
2406
|
+
***************************************************************/
|
|
2407
|
+
function is_current_load(gobj, md)
|
|
2408
|
+
{
|
|
2409
|
+
return gobj_current_state(gobj) === "ST_LOADING" &&
|
|
2410
|
+
kw_get_int(gobj, md, "round", 0, 0) === gobj.priv.load_round;
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
function is_current_write(gobj, md)
|
|
2414
|
+
{
|
|
2415
|
+
return gobj_current_state(gobj) === "ST_SAVING" &&
|
|
2416
|
+
kw_get_int(gobj, md, "write", 0, 0) === gobj.priv.write_tag;
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
/***************************************************************
|
|
2420
|
+
* The transport dropped under a load or a write: its answers
|
|
2421
|
+
* will never come. The load goes back to the screen it replaced
|
|
2422
|
+
* (or to "not connected" when there was none), the write ends
|
|
2423
|
+
* with the drop said -- whether it landed is unknown -- and both
|
|
2424
|
+
* mark the model to be asked again when the session is back.
|
|
2425
|
+
* Bumping the round makes an answer of what was cut stale.
|
|
2426
|
+
***************************************************************/
|
|
2427
|
+
function transport_dropped(gobj)
|
|
2428
|
+
{
|
|
2429
|
+
let priv = gobj.priv;
|
|
2430
|
+
let state = gobj_current_state(gobj);
|
|
2431
|
+
|
|
2432
|
+
priv.reload_on_open = true;
|
|
2433
|
+
if(state === "ST_SAVING") {
|
|
2434
|
+
priv.write_tag++;
|
|
2435
|
+
return end_writes(gobj, "the connection dropped during the write");
|
|
2436
|
+
}
|
|
2437
|
+
if(state === "ST_LOADING") {
|
|
2438
|
+
priv.load_round++;
|
|
2439
|
+
priv.pending = 0;
|
|
2440
|
+
priv.load_error = "";
|
|
2441
|
+
if(priv.model) {
|
|
2442
|
+
return go(gobj, priv.treedb_id, priv.topic_name, priv.diagram, false);
|
|
2443
|
+
}
|
|
2444
|
+
gobj_change_state(gobj, "ST_IDLE");
|
|
2445
|
+
render(gobj);
|
|
2446
|
+
}
|
|
2447
|
+
return 0;
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2361
2450
|
/***************************************************************
|
|
2362
2451
|
* The queue is done, or gave up. Either way the model is what
|
|
2363
2452
|
* the answers made it, so the screen is redrawn from it and the
|
|
@@ -2612,8 +2701,28 @@ function ac_mt_command_answer(gobj, event, kw, src)
|
|
|
2612
2701
|
return 0; /* another panel's answer on a shared transport */
|
|
2613
2702
|
}
|
|
2614
2703
|
|
|
2704
|
+
/* A failure while the transport is not in session is the DROP, not a
|
|
2705
|
+
* refusal: a routing adapter settles what it had in flight when its
|
|
2706
|
+
* session closes, and that can arrive before the host's
|
|
2707
|
+
* EV_TRANSPORT_STATE. Read as a refusal, the write it ended looked
|
|
2708
|
+
* refused, and the reconnect did not reload a model it may have
|
|
2709
|
+
* changed. */
|
|
2710
|
+
if(result < 0 && !transport_in_session(gobj) &&
|
|
2711
|
+
(gobj_current_state(gobj) === "ST_LOADING" ||
|
|
2712
|
+
gobj_current_state(gobj) === "ST_SAVING")) {
|
|
2713
|
+
if((command === "nodes" && is_current_load(gobj, md)) ||
|
|
2714
|
+
(command !== "nodes" && is_current_write(gobj, md))) {
|
|
2715
|
+
return transport_dropped(gobj);
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
|
|
2615
2719
|
if(command === "nodes") {
|
|
2616
2720
|
let topic_name = kw_get_str(gobj, md, "topic_name", "", 0);
|
|
2721
|
+
if(!is_current_load(gobj, md)) {
|
|
2722
|
+
log_warning(`${gobj_short_name(gobj)}: 'nodes' of '${topic_name}' answered ` +
|
|
2723
|
+
`for a load that is over: ignored`);
|
|
2724
|
+
return 0;
|
|
2725
|
+
}
|
|
2617
2726
|
priv.pending--;
|
|
2618
2727
|
if(result < 0) {
|
|
2619
2728
|
priv.load_error = comment || t("cannot load the schemas");
|
|
@@ -2647,6 +2756,11 @@ function ac_mt_command_answer(gobj, event, kw, src)
|
|
|
2647
2756
|
|
|
2648
2757
|
if(command === "update-node" || command === "delete-node") {
|
|
2649
2758
|
let topic_name = kw_get_str(gobj, md, "topic_name", "", 0);
|
|
2759
|
+
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;
|
|
2763
|
+
}
|
|
2650
2764
|
if(result < 0) {
|
|
2651
2765
|
return end_writes(gobj, comment || t("the treedb refused the write"));
|
|
2652
2766
|
}
|
|
@@ -2728,16 +2842,31 @@ function ac_hide(gobj, event, kw, src)
|
|
|
2728
2842
|
}
|
|
2729
2843
|
|
|
2730
2844
|
/***************************************************************
|
|
2731
|
-
* The session came up or went down. The model stays
|
|
2732
|
-
*
|
|
2733
|
-
*
|
|
2845
|
+
* The session came up or went down. The model stays -- a schema
|
|
2846
|
+
* does not change under you -- unless the drop cut a load or a
|
|
2847
|
+
* write (transport_dropped()): then it is asked again on the way
|
|
2848
|
+
* back, as it is when a first load had no transport to run on.
|
|
2849
|
+
*
|
|
2850
|
+
* Nothing else ends a load or a write the drop cut: their answers
|
|
2851
|
+
* will never come, and waiting in ST_LOADING skipped the reload
|
|
2852
|
+
* itself (M-1 of the independent review of 7.25.4).
|
|
2734
2853
|
***************************************************************/
|
|
2735
2854
|
function ac_transport_state(gobj, event, kw, src)
|
|
2736
2855
|
{
|
|
2737
2856
|
let priv = gobj.priv;
|
|
2857
|
+
let state = gobj_current_state(gobj);
|
|
2738
2858
|
|
|
2739
2859
|
priv.connected = !!(kw && kw.connected);
|
|
2740
|
-
if(priv.connected
|
|
2860
|
+
if(!priv.connected) {
|
|
2861
|
+
if(state === "ST_LOADING" || state === "ST_SAVING") {
|
|
2862
|
+
return transport_dropped(gobj);
|
|
2863
|
+
}
|
|
2864
|
+
render_toolbar(gobj);
|
|
2865
|
+
return 0;
|
|
2866
|
+
}
|
|
2867
|
+
if((!priv.model || priv.reload_on_open) &&
|
|
2868
|
+
state !== "ST_LOADING" && state !== "ST_SAVING") {
|
|
2869
|
+
priv.reload_on_open = false;
|
|
2741
2870
|
return request_model(gobj);
|
|
2742
2871
|
}
|
|
2743
2872
|
render_toolbar(gobj);
|
|
@@ -2762,20 +2891,24 @@ function ac_refresh(gobj, event, kw, src)
|
|
|
2762
2891
|
|
|
2763
2892
|
/***************************************************************
|
|
2764
2893
|
* {drafts: {treedb_name: [topic names]}} -- the host says which
|
|
2765
|
-
* topics hold a draft in __system__ that
|
|
2766
|
-
*
|
|
2767
|
-
*
|
|
2768
|
-
*
|
|
2769
|
-
*
|
|
2770
|
-
*
|
|
2771
|
-
*
|
|
2894
|
+
* topics hold a draft in __system__ that is not saved (C_TREEDB's
|
|
2895
|
+
* saved-schema, `draft_changed`). The mark of a write lived in this
|
|
2896
|
+
* session's memory only, so a reload of the page, a reconnect or a
|
|
2897
|
+
* refresh showed no draft while __system__ still differed (N13 of
|
|
2898
|
+
* the 2026-09-22 review).
|
|
2899
|
+
*
|
|
2900
|
+
* REPLACES what the host said before -- the host is the truth after
|
|
2901
|
+
* a reload, and a Save is exactly the host saying "none any more".
|
|
2902
|
+
* It only ever added, and a topic stayed a draft after the Save that
|
|
2903
|
+
* published it (M1 of the 2026-09-23 review). What this session wrote
|
|
2904
|
+
* stays marked: it is kept apart (see topic_is_draft()).
|
|
2772
2905
|
***************************************************************/
|
|
2773
2906
|
function ac_drafts(gobj, event, kw, src)
|
|
2774
2907
|
{
|
|
2775
2908
|
let priv = gobj.priv;
|
|
2776
2909
|
|
|
2777
2910
|
priv.host_drafts = (kw && kw.drafts && typeof kw.drafts === "object")? kw.drafts: {};
|
|
2778
|
-
|
|
2911
|
+
priv.host_ids = host_draft_ids(priv.model, priv.host_drafts);
|
|
2779
2912
|
if(priv.model) {
|
|
2780
2913
|
render(gobj);
|
|
2781
2914
|
}
|
package/src/c_yui_shell.js
CHANGED
|
@@ -43,6 +43,8 @@ import {
|
|
|
43
43
|
createElement2, empty_string, is_object, is_array, is_string,
|
|
44
44
|
refresh_language,
|
|
45
45
|
is_gobj,
|
|
46
|
+
gobj_has_event,
|
|
47
|
+
gobj_is_destroying,
|
|
46
48
|
} from "@yuneta/gobj-js";
|
|
47
49
|
|
|
48
50
|
import {
|
|
@@ -174,6 +176,10 @@ let PRIVATE_DATA = {
|
|
|
174
176
|
* backend-connected state. State is host/event-driven (unlike
|
|
175
177
|
* the avatar provider it is a setter, not a pull callback). */
|
|
176
178
|
conn_nodes: [],
|
|
179
|
+
/* The last state yui_shell_set_connection_state() was told, null
|
|
180
|
+
* while nobody told it anything: EV_CONNECTION_STATE goes out on an
|
|
181
|
+
* EDGE, not on every call. */
|
|
182
|
+
connected: null,
|
|
177
183
|
/* Currently open toolbar dropdown panel, if any. Tracked here
|
|
178
184
|
* so a second click on any trigger (or programmatic close) can
|
|
179
185
|
* tear down the previous one through the same code path. */
|
|
@@ -3033,11 +3039,34 @@ function yui_shell_set_submenu(shell_gobj, parent_item_id, items)
|
|
|
3033
3039
|
/*---------------------------------------------*
|
|
3034
3040
|
* Global methods table
|
|
3035
3041
|
*---------------------------------------------*/
|
|
3042
|
+
/***************************************************************
|
|
3043
|
+
* EV_CONNECTION_STATE is an OPTIONAL fact: a view that waits for a
|
|
3044
|
+
* backend answer subscribes to it by name. An app that subscribed
|
|
3045
|
+
* to EVERY event of its shell with one attr (`subscriber`) did not
|
|
3046
|
+
* ask for it and does not declare it -- published to it anyway, the
|
|
3047
|
+
* event would be an FSM error in the app on every connection edge,
|
|
3048
|
+
* and adding it to the shell a breaking change for every such app.
|
|
3049
|
+
* So it reaches only the subscribers that declare it. Every other
|
|
3050
|
+
* event of the shell goes out as before.
|
|
3051
|
+
***************************************************************/
|
|
3052
|
+
function mt_publication_pre_filter(gobj, subs, event, kw)
|
|
3053
|
+
{
|
|
3054
|
+
if(event !== "EV_CONNECTION_STATE") {
|
|
3055
|
+
return 1;
|
|
3056
|
+
}
|
|
3057
|
+
let subscriber = subs && subs.subscriber;
|
|
3058
|
+
if(!subscriber || !gobj_has_event(subscriber, event, 0)) {
|
|
3059
|
+
return 0;
|
|
3060
|
+
}
|
|
3061
|
+
return 1;
|
|
3062
|
+
}
|
|
3063
|
+
|
|
3036
3064
|
const gmt = {
|
|
3037
|
-
mt_create:
|
|
3038
|
-
mt_start:
|
|
3039
|
-
mt_stop:
|
|
3040
|
-
mt_destroy:
|
|
3065
|
+
mt_create: mt_create,
|
|
3066
|
+
mt_start: mt_start,
|
|
3067
|
+
mt_stop: mt_stop,
|
|
3068
|
+
mt_destroy: mt_destroy,
|
|
3069
|
+
mt_publication_pre_filter: mt_publication_pre_filter
|
|
3041
3070
|
};
|
|
3042
3071
|
|
|
3043
3072
|
function create_gclass(gclass_name)
|
|
@@ -3088,6 +3117,13 @@ function create_gclass(gclass_name)
|
|
|
3088
3117
|
* re-renders its own translated parts. */
|
|
3089
3118
|
["EV_LANGUAGE_CHANGED", event_flag_t.EVF_OUTPUT_EVENT
|
|
3090
3119
|
|event_flag_t.EVF_PUBLIC_EVENT
|
|
3120
|
+
|event_flag_t.EVF_NO_WARN_SUBS],
|
|
3121
|
+
|
|
3122
|
+
/* The app's backend connection went up or down
|
|
3123
|
+
* (yui_shell_set_connection_state), {connected}. Delivered only
|
|
3124
|
+
* to the subscribers that declare it: see
|
|
3125
|
+
* mt_publication_pre_filter(). */
|
|
3126
|
+
["EV_CONNECTION_STATE", event_flag_t.EVF_OUTPUT_EVENT
|
|
3091
3127
|
|event_flag_t.EVF_NO_WARN_SUBS]
|
|
3092
3128
|
];
|
|
3093
3129
|
|
|
@@ -3552,14 +3588,31 @@ function yui_shell_language_changed(shell_gobj)
|
|
|
3552
3588
|
************************************************************/
|
|
3553
3589
|
function yui_shell_set_connection_state(shell_gobj, connected)
|
|
3554
3590
|
{
|
|
3591
|
+
if(!shell_gobj || !is_gobj(shell_gobj) || gobj_is_destroying(shell_gobj)) {
|
|
3592
|
+
return;
|
|
3593
|
+
}
|
|
3555
3594
|
let priv = shell_gobj.priv;
|
|
3556
|
-
if(!priv
|
|
3595
|
+
if(!priv) {
|
|
3557
3596
|
return;
|
|
3558
3597
|
}
|
|
3559
3598
|
let on = !!connected;
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3599
|
+
let changed = (priv.connected !== on);
|
|
3600
|
+
priv.connected = on;
|
|
3601
|
+
if(is_array(priv.conn_nodes)) {
|
|
3602
|
+
for(let $n of priv.conn_nodes) {
|
|
3603
|
+
$n.classList.toggle("is-connected", on);
|
|
3604
|
+
$n.classList.toggle("is-disconnected", !on);
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3607
|
+
/* The fact goes out as well, for the views: a write a view sent
|
|
3608
|
+
* before the backend dropped is never answered, and the view that
|
|
3609
|
+
* waits for it (a topic form's Save) learns the drop from HERE --
|
|
3610
|
+
* it holds no subscription of its own to the transport, because
|
|
3611
|
+
* subscribing to a C_IEVENT_CLI forwards the subscription to the
|
|
3612
|
+
* backend. Only an EDGE is published: the app calls this on every
|
|
3613
|
+
* reconnect attempt too. */
|
|
3614
|
+
if(changed) {
|
|
3615
|
+
gobj_publish_event(shell_gobj, "EV_CONNECTION_STATE", {connected: on});
|
|
3563
3616
|
}
|
|
3564
3617
|
}
|
|
3565
3618
|
|
|
@@ -99,7 +99,8 @@ import {t} from "i18next";
|
|
|
99
99
|
import {yui_toolbar_icon} from "./yui_toolbar.js";
|
|
100
100
|
|
|
101
101
|
import {
|
|
102
|
-
plan_treedb_writes, READONLY_FORM_TOOLBAR, col_goes_back_to_treedb
|
|
102
|
+
plan_treedb_writes, READONLY_FORM_TOOLBAR, col_goes_back_to_treedb,
|
|
103
|
+
instance_keys_of
|
|
103
104
|
} from "./treedb_write_plan.js";
|
|
104
105
|
import {
|
|
105
106
|
yui_file_read, yui_files_manifest, yui_file_id_label, yui_file_size_label
|
|
@@ -2395,6 +2396,10 @@ function open_form_dialog(gobj, mode, record)
|
|
|
2395
2396
|
style: 'display:flex; flex-direction:column; height:100%;'}, []]
|
|
2396
2397
|
);
|
|
2397
2398
|
|
|
2399
|
+
/* The instance this form writes to, kept as the record HAD it: the
|
|
2400
|
+
* widget is not trusted with an address (instance_keys_of()). */
|
|
2401
|
+
priv.form_instance = (form_mode === "update")? instance_keys_of(record, desc) : null;
|
|
2402
|
+
|
|
2398
2403
|
let form_plan = write_plan(gobj);
|
|
2399
2404
|
let form = gobj_create_pure_child(
|
|
2400
2405
|
"form_" + clean_name(gobj_name(gobj)),
|
|
@@ -2508,6 +2513,7 @@ function teardown_form_child(gobj)
|
|
|
2508
2513
|
priv.reading_files = 0; // a read in flight lands on nobody
|
|
2509
2514
|
priv.awaiting_write = 0; // and so does the answer of a write
|
|
2510
2515
|
priv.form_busy = {busy: false}; // its toolbar goes with it
|
|
2516
|
+
priv.form_instance = null;
|
|
2511
2517
|
if(priv.form) {
|
|
2512
2518
|
if(gobj_is_running(priv.form)) {
|
|
2513
2519
|
gobj_stop(priv.form);
|
|
@@ -4313,6 +4319,11 @@ function publish_treedb_write(gobj, mode, record, jn_files, form_write)
|
|
|
4313
4319
|
topic_name: gobj_read_str_attr(gobj, "topic_name"),
|
|
4314
4320
|
record: strip_read_only_cols(gobj, record)
|
|
4315
4321
|
};
|
|
4322
|
+
if(mode !== "create" && gobj.priv.form_instance) {
|
|
4323
|
+
/* The pkey2 names the instance: the value the record had, not
|
|
4324
|
+
* the one that came back through a datetime-local. */
|
|
4325
|
+
Object.assign(kw_write.record, gobj.priv.form_instance);
|
|
4326
|
+
}
|
|
4316
4327
|
if(form_write) {
|
|
4317
4328
|
/* The serial the host echoes back with the answer */
|
|
4318
4329
|
kw_write.form_write = form_write;
|
|
@@ -156,10 +156,11 @@ let __gclass__ = null;
|
|
|
156
156
|
***************************************************************/
|
|
157
157
|
function mt_create(gobj)
|
|
158
158
|
{
|
|
159
|
-
/* The writes of topic forms sent and not yet answered, by
|
|
160
|
-
* (form_writes_in_flight): answered refused when the
|
|
161
|
-
* closes under them. */
|
|
159
|
+
/* The writes of topic forms sent and not yet answered, by topic
|
|
160
|
+
* and serial (form_writes_in_flight): answered refused when the
|
|
161
|
+
* transport closes under them. */
|
|
162
162
|
gobj.priv.form_writes = {};
|
|
163
|
+
gobj.priv.conn_shell = null; /* the shell whose EV_CONNECTION_STATE we hear */
|
|
163
164
|
|
|
164
165
|
build_ui(gobj);
|
|
165
166
|
|
|
@@ -178,6 +179,17 @@ function mt_create(gobj)
|
|
|
178
179
|
***************************************************************/
|
|
179
180
|
function mt_start(gobj)
|
|
180
181
|
{
|
|
182
|
+
/* The app's backend connection, as the app tells its shell
|
|
183
|
+
* (yui_shell_set_connection_state). A write in flight when it drops
|
|
184
|
+
* is never answered, and its form waited for ever in every host
|
|
185
|
+
* that did not forward EV_TRANSPORT_STATE -- wattyzer and yunovatios
|
|
186
|
+
* among them (M8 of the 2026-09-23 review). The transport itself is
|
|
187
|
+
* not subscribed to: see ac_transport_state(). */
|
|
188
|
+
let shell = yui_shell_of(gobj);
|
|
189
|
+
if(shell) {
|
|
190
|
+
gobj_subscribe_event(shell, "EV_CONNECTION_STATE", {}, gobj);
|
|
191
|
+
gobj.priv.conn_shell = shell;
|
|
192
|
+
}
|
|
181
193
|
request_treedb_descs(gobj);
|
|
182
194
|
}
|
|
183
195
|
|
|
@@ -186,6 +198,14 @@ function mt_start(gobj)
|
|
|
186
198
|
***************************************************************/
|
|
187
199
|
function mt_stop(gobj)
|
|
188
200
|
{
|
|
201
|
+
let priv = gobj.priv;
|
|
202
|
+
if(priv.conn_shell) {
|
|
203
|
+
if(!gobj_is_destroying(priv.conn_shell)) {
|
|
204
|
+
gobj_unsubscribe_event(priv.conn_shell, "EV_CONNECTION_STATE", {}, gobj);
|
|
205
|
+
}
|
|
206
|
+
priv.conn_shell = null;
|
|
207
|
+
}
|
|
208
|
+
|
|
189
209
|
/* Retire our site-map sub-routes so a torn-down view leaves no
|
|
190
210
|
* stale children in the map (ROUTING.md contributor). */
|
|
191
211
|
let shell = yui_shell_of(gobj);
|
|
@@ -2077,7 +2097,7 @@ function ac_mt_command_answer(gobj, event, kw, src)
|
|
|
2077
2097
|
{topic_name: failed_topic}, gobj);
|
|
2078
2098
|
}
|
|
2079
2099
|
settle_form_write(gobj.priv.form_writes,
|
|
2080
|
-
kw_get_int(gobj, kw_command, "form_write", 0, 0));
|
|
2100
|
+
kw_get_int(gobj, kw_command, "form_write", 0, 0), failed_topic);
|
|
2081
2101
|
answer_form_write(
|
|
2082
2102
|
gobj,
|
|
2083
2103
|
get_gobj_formtable(gobj, failed_topic),
|
|
@@ -2177,7 +2197,8 @@ function ac_mt_command_answer(gobj, event, kw, src)
|
|
|
2177
2197
|
command: command
|
|
2178
2198
|
});
|
|
2179
2199
|
settle_form_write(gobj.priv.form_writes,
|
|
2180
|
-
kw_get_int(gobj, kw_command, "form_write", 0, 0)
|
|
2200
|
+
kw_get_int(gobj, kw_command, "form_write", 0, 0),
|
|
2201
|
+
kw_get_str(gobj, kw_command, "topic_name", "", 0));
|
|
2181
2202
|
answer_form_write(
|
|
2182
2203
|
gobj,
|
|
2183
2204
|
get_gobj_formtable(gobj, kw_get_str(gobj, kw_command, "topic_name", "", 0)),
|
|
@@ -2413,11 +2434,19 @@ function ac_back_to_topics(gobj, event, kw, src)
|
|
|
2413
2434
|
}
|
|
2414
2435
|
|
|
2415
2436
|
/************************************************************
|
|
2416
|
-
*
|
|
2417
|
-
*
|
|
2418
|
-
*
|
|
2419
|
-
*
|
|
2420
|
-
*
|
|
2437
|
+
* A backend transport edge, from either of two places:
|
|
2438
|
+
* - EV_TRANSPORT_STATE, forwarded by a host that keeps one
|
|
2439
|
+
* transport per view (gui_treedb's C_TREEDB_VIEW, gui_agent's
|
|
2440
|
+
* C_AGENT_TREEDB_VIEW);
|
|
2441
|
+
* - EV_CONNECTION_STATE, published by the SHELL when the app tells
|
|
2442
|
+
* it its backend went up or down (yui_shell_set_connection_state),
|
|
2443
|
+
* which is what every single-backend app already does for the
|
|
2444
|
+
* toolbar dot. No host has to remember anything for this one.
|
|
2445
|
+
* Both can arrive for one edge; what is done here is idempotent.
|
|
2446
|
+
* The toolbar disables the JSON viewers the moment the session drops
|
|
2447
|
+
* (and re-enables them on reconnect). The library view must not
|
|
2448
|
+
* subscribe to the C_IEVENT_CLI itself: that forwards the subscription
|
|
2449
|
+
* upstream and breaks the session.
|
|
2421
2450
|
************************************************************/
|
|
2422
2451
|
function ac_transport_state(gobj, event, kw, src)
|
|
2423
2452
|
{
|
|
@@ -3042,6 +3071,7 @@ function create_gclass(gclass_name)
|
|
|
3042
3071
|
["EV_HIDE", ac_hide, null],
|
|
3043
3072
|
["EV_BACK_TO_TOPICS", ac_back_to_topics, null],
|
|
3044
3073
|
["EV_TRANSPORT_STATE", ac_transport_state, null],
|
|
3074
|
+
["EV_CONNECTION_STATE", ac_transport_state, null],
|
|
3045
3075
|
]]
|
|
3046
3076
|
];
|
|
3047
3077
|
|
|
@@ -3077,6 +3107,7 @@ function create_gclass(gclass_name)
|
|
|
3077
3107
|
["EV_HIDE", 0],
|
|
3078
3108
|
["EV_BACK_TO_TOPICS", 0],
|
|
3079
3109
|
["EV_TRANSPORT_STATE", 0],
|
|
3110
|
+
["EV_CONNECTION_STATE", 0],
|
|
3080
3111
|
["EV_TOPIC_SELECTED",
|
|
3081
3112
|
event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_NO_WARN_SUBS]
|
|
3082
3113
|
];
|