@yuneta/gobj-ui 7.25.6 → 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.
- package/README.md +22 -1
- package/dist/gobj-ui.cjs.js +155 -17
- package/dist/gobj-ui.es.js +155 -17
- package/package.json +1 -1
- package/src/c_yui_schema_editor.js +158 -21
- package/src/schema_editor_transport.wiring.test.js +174 -1
- package/src/shell_modals.js +32 -4
- package/src/shell_toast_once.test.js +64 -0
package/README.md
CHANGED
|
@@ -2161,7 +2161,28 @@ else.
|
|
|
2161
2161
|
`the connection dropped during the write` (an i18n key the consumer's
|
|
2162
2162
|
locales carry). Each request carries its round in `__md_command__`
|
|
2163
2163
|
(`round` / `write`), so an answer of what the drop cut is ignored with a
|
|
2164
|
-
warning.
|
|
2164
|
+
warning. The drop itself is logged ONCE; the failures a routing adapter
|
|
2165
|
+
settles for the same drop are that one event and are not logged again.
|
|
2166
|
+
- **Nothing moves the editor out of `ST_LOADING` or `ST_SAVING` but the end of
|
|
2167
|
+
what is in flight** (7.25.7). An `EV_SHOW` that arrives meanwhile waits, the
|
|
2168
|
+
last one wins, and it is applied when the load lands, fails or is cut, or
|
|
2169
|
+
when the write ends. 7.25.6 let it move a RELOAD out of `ST_LOADING`: every
|
|
2170
|
+
answer of that load was then "for a load that is over", and the next write
|
|
2171
|
+
patched the records the reload had emptied into a model with no treedb.
|
|
2172
|
+
With no model and no load, `EV_SHOW` keeps the position and stays in
|
|
2173
|
+
`ST_IDLE`; the next load goes there.
|
|
2174
|
+
|
|
2175
|
+
```js
|
|
2176
|
+
gobj_send_event(editor, "EV_REFRESH", {}, host); // ST_LOADING
|
|
2177
|
+
gobj_send_event(editor, "EV_SHOW", {subpath: "db"}, host); // waits
|
|
2178
|
+
// ... the three `nodes` answers land -> ST_TOPICS of `db`
|
|
2179
|
+
```
|
|
2180
|
+
- A write this view gave up on (a drop, a deadline) whose answer still says it
|
|
2181
|
+
was DONE makes the editor read the model again (at once, after the write in
|
|
2182
|
+
flight, or on the reconnect). Through a routing adapter that settled the
|
|
2183
|
+
write on its OWN deadline the late answer does not reach the view -- the
|
|
2184
|
+
adapter echoes a node event instead, which this view does not hear -- and
|
|
2185
|
+
Refresh is what reads the store.
|
|
2165
2186
|
|
|
2166
2187
|
```js
|
|
2167
2188
|
// the host forwards its session edges; nothing else is needed
|
package/dist/gobj-ui.cjs.js
CHANGED
|
@@ -3025,6 +3025,14 @@ function maybe_apply_translator($node, opts) {
|
|
|
3025
3025
|
* Non-blocking, auto-dismiss after `opts.timeout` ms (default
|
|
3026
3026
|
* 5000). `opts.timeout = 0` disables auto-dismiss.
|
|
3027
3027
|
* Returns `{ close() }` so callers can dismiss programmatically.
|
|
3028
|
+
*
|
|
3029
|
+
* ONE toast per message on screen: a string message of the same
|
|
3030
|
+
* kind as a toast still showing is not stacked under it -- that
|
|
3031
|
+
* toast is kept, its time starts again, and its handle is
|
|
3032
|
+
* returned. One close of a transport settles every request it
|
|
3033
|
+
* cut, each view shows its failure, and the operator got a
|
|
3034
|
+
* column of identical "the connection dropped". A message built
|
|
3035
|
+
* of nodes is never merged: its text is not a key.
|
|
3028
3036
|
***************************************************************/
|
|
3029
3037
|
function show_notification(shell, kind, message, opts) {
|
|
3030
3038
|
let $layer = notification_layer(shell);
|
|
@@ -3032,6 +3040,13 @@ function show_notification(shell, kind, message, opts) {
|
|
|
3032
3040
|
(0, _yuneta_gobj_js.log_warning)("yui_shell_show_*: shell has no notification layer");
|
|
3033
3041
|
return { close: () => {} };
|
|
3034
3042
|
}
|
|
3043
|
+
let key = typeof message === "string" ? `${kind}\u0000${message}` : null;
|
|
3044
|
+
if (key !== null) {
|
|
3045
|
+
for (let $shown of Array.from($layer.children)) if ($shown.__toast_key__ === key && $shown.__toast__) {
|
|
3046
|
+
$shown.__toast__.restart();
|
|
3047
|
+
return $shown.__toast__.handle;
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3035
3050
|
let p_attrs = typeof message === "string" ? { i18n: message } : {};
|
|
3036
3051
|
let $note = (0, _yuneta_gobj_js.createElement2)([
|
|
3037
3052
|
"div",
|
|
@@ -3068,8 +3083,21 @@ function show_notification(shell, kind, message, opts) {
|
|
|
3068
3083
|
let $del = $note.querySelector(".delete");
|
|
3069
3084
|
if ($del) $del.addEventListener("click", close);
|
|
3070
3085
|
let timeout = opts && opts.timeout != null ? opts.timeout : 5e3;
|
|
3071
|
-
|
|
3072
|
-
|
|
3086
|
+
let restart = function() {
|
|
3087
|
+
if (timeout_id) {
|
|
3088
|
+
clearTimeout(timeout_id);
|
|
3089
|
+
timeout_id = null;
|
|
3090
|
+
}
|
|
3091
|
+
if (timeout > 0) timeout_id = setTimeout(close, timeout);
|
|
3092
|
+
};
|
|
3093
|
+
restart();
|
|
3094
|
+
let handle = { close };
|
|
3095
|
+
$note.__toast_key__ = key;
|
|
3096
|
+
$note.__toast__ = {
|
|
3097
|
+
handle,
|
|
3098
|
+
restart
|
|
3099
|
+
};
|
|
3100
|
+
return handle;
|
|
3073
3101
|
}
|
|
3074
3102
|
function yui_shell_show_info(shell, message, opts) {
|
|
3075
3103
|
return show_notification(shell, "info", message, opts);
|
|
@@ -72156,6 +72184,9 @@ var PRIVATE_DATA$1 = {
|
|
|
72156
72184
|
write_tag: 0,
|
|
72157
72185
|
connected: false,
|
|
72158
72186
|
reload_on_open: false,
|
|
72187
|
+
dropped_round: 0,
|
|
72188
|
+
dropped_write: 0,
|
|
72189
|
+
reload_after_write: false,
|
|
72159
72190
|
pending_seg: null,
|
|
72160
72191
|
import_plan: null,
|
|
72161
72192
|
import_pane: null,
|
|
@@ -74624,6 +74655,15 @@ function transport_in_session(gobj) {
|
|
|
74624
74655
|
* one the drop abandoned (or a deadline settled) can still come
|
|
74625
74656
|
* back: counted, it ended the next load with the old data or
|
|
74626
74657
|
* moved the next write's queue.
|
|
74658
|
+
*
|
|
74659
|
+
* The state is part of the question, and that is only true
|
|
74660
|
+
* because NOTHING moves the editor out of ST_LOADING or ST_SAVING
|
|
74661
|
+
* but the end of what is in flight: a position the host sends
|
|
74662
|
+
* meanwhile waits in `pending_seg` (ac_show()). 7.25.6 let EV_SHOW
|
|
74663
|
+
* move a RELOAD out of ST_LOADING, and every answer of it was then
|
|
74664
|
+
* "for a load that is over": the records it had emptied stayed
|
|
74665
|
+
* empty, and the next write patched them into a model with no
|
|
74666
|
+
* treedb at all.
|
|
74627
74667
|
***************************************************************/
|
|
74628
74668
|
function is_current_load(gobj, md) {
|
|
74629
74669
|
return (0, _yuneta_gobj_js.gobj_current_state)(gobj) === "ST_LOADING" && (0, _yuneta_gobj_js.kw_get_int)(gobj, md, "round", 0, 0) === gobj.priv.load_round;
|
|
@@ -74644,17 +74684,39 @@ function transport_dropped(gobj) {
|
|
|
74644
74684
|
let state = (0, _yuneta_gobj_js.gobj_current_state)(gobj);
|
|
74645
74685
|
priv.reload_on_open = true;
|
|
74646
74686
|
if (state === "ST_SAVING") {
|
|
74687
|
+
(0, _yuneta_gobj_js.log_warning)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: the transport dropped during a write: the model is read again on the reconnect`);
|
|
74688
|
+
priv.dropped_write = priv.write_tag;
|
|
74647
74689
|
priv.write_tag++;
|
|
74648
74690
|
return end_writes(gobj, "the connection dropped during the write");
|
|
74649
74691
|
}
|
|
74650
74692
|
if (state === "ST_LOADING") {
|
|
74693
|
+
(0, _yuneta_gobj_js.log_warning)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: the transport dropped during the load: the model is read again on the reconnect`);
|
|
74694
|
+
priv.dropped_round = priv.load_round;
|
|
74651
74695
|
priv.load_round++;
|
|
74652
74696
|
priv.pending = 0;
|
|
74653
74697
|
priv.load_error = "";
|
|
74654
|
-
|
|
74655
|
-
|
|
74656
|
-
|
|
74698
|
+
return end_load(gobj);
|
|
74699
|
+
}
|
|
74700
|
+
return 0;
|
|
74701
|
+
}
|
|
74702
|
+
/***************************************************************
|
|
74703
|
+
* A load is over -- landed, failed or cut -- and the editor goes
|
|
74704
|
+
* where the operator is: the position the host sent while it was
|
|
74705
|
+
* in flight (ac_show() kept it), or the one it had. Without a
|
|
74706
|
+
* model there is no screen to go to: the position is kept for the
|
|
74707
|
+
* next load, and the editor says why it is empty.
|
|
74708
|
+
***************************************************************/
|
|
74709
|
+
function end_load(gobj) {
|
|
74710
|
+
let priv = gobj.priv;
|
|
74711
|
+
let seg = priv.pending_seg;
|
|
74712
|
+
priv.pending_seg = null;
|
|
74713
|
+
if (priv.model) {
|
|
74714
|
+
if (seg !== null) return apply_seg(gobj, seg);
|
|
74715
|
+
return go(gobj, priv.treedb_id, priv.topic_name, priv.diagram, false);
|
|
74657
74716
|
}
|
|
74717
|
+
if (seg !== null) adopt_seg(gobj, seg);
|
|
74718
|
+
(0, _yuneta_gobj_js.gobj_change_state)(gobj, "ST_IDLE");
|
|
74719
|
+
render(gobj);
|
|
74658
74720
|
return 0;
|
|
74659
74721
|
}
|
|
74660
74722
|
/***************************************************************
|
|
@@ -74677,7 +74739,7 @@ function end_writes(gobj, error) {
|
|
|
74677
74739
|
apply_seg(gobj, seg);
|
|
74678
74740
|
if (priv.save_wrote) (0, _yuneta_gobj_js.gobj_publish_event)(gobj, "EV_RECORD_WRITTEN", { treedb_name: (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "treedb_name") });
|
|
74679
74741
|
priv.save_wrote = false;
|
|
74680
|
-
return
|
|
74742
|
+
return reload_if_owed(gobj);
|
|
74681
74743
|
}
|
|
74682
74744
|
}
|
|
74683
74745
|
if (error) yui_shell_show_error(yui_shell_of(gobj), error, { t: i18next.t });
|
|
@@ -74685,9 +74747,25 @@ function end_writes(gobj, error) {
|
|
|
74685
74747
|
priv.save_wrote = false;
|
|
74686
74748
|
render(gobj);
|
|
74687
74749
|
publish_check(gobj);
|
|
74750
|
+
reload_if_owed(gobj);
|
|
74688
74751
|
return error ? -1 : 0;
|
|
74689
74752
|
}
|
|
74690
74753
|
/***************************************************************
|
|
74754
|
+
* A write given up earlier turned out DONE while another one was
|
|
74755
|
+
* in flight (see ac_mt_command_answer()): the model is read again
|
|
74756
|
+
* now that nothing is.
|
|
74757
|
+
***************************************************************/
|
|
74758
|
+
function reload_if_owed(gobj) {
|
|
74759
|
+
let priv = gobj.priv;
|
|
74760
|
+
if (!priv.reload_after_write) return 0;
|
|
74761
|
+
priv.reload_after_write = false;
|
|
74762
|
+
if (!transport_in_session(gobj)) {
|
|
74763
|
+
priv.reload_on_open = true;
|
|
74764
|
+
return 0;
|
|
74765
|
+
}
|
|
74766
|
+
return request_model(gobj);
|
|
74767
|
+
}
|
|
74768
|
+
/***************************************************************
|
|
74691
74769
|
* The record a column form produces.
|
|
74692
74770
|
*
|
|
74693
74771
|
* Everything the store needs and the operator did not type is
|
|
@@ -74755,7 +74833,7 @@ function go(gobj, treedb_id, topic_name, diagram, mirror) {
|
|
|
74755
74833
|
priv.topic_name = topic_name || "";
|
|
74756
74834
|
priv.diagram = !!diagram;
|
|
74757
74835
|
let state = "ST_TREEDBS";
|
|
74758
|
-
if (!priv.model) state = "
|
|
74836
|
+
if (!priv.model) state = "ST_IDLE";
|
|
74759
74837
|
else if (priv.model.treedbs.length === 0) state = "ST_EMPTY";
|
|
74760
74838
|
else if (priv.treedb_id && priv.diagram) state = "ST_DIAGRAM";
|
|
74761
74839
|
else if (priv.treedb_id && priv.topic_name) state = "ST_COLUMNS";
|
|
@@ -74782,12 +74860,39 @@ function position_seg(gobj) {
|
|
|
74782
74860
|
* empty screen: a shared link outlives the schema it points at.
|
|
74783
74861
|
***************************************************************/
|
|
74784
74862
|
function apply_seg(gobj, seg) {
|
|
74863
|
+
let pos = seg_position(seg);
|
|
74864
|
+
return go(gobj, pos.treedb_id, pos.topic_name, pos.diagram, false);
|
|
74865
|
+
}
|
|
74866
|
+
function seg_position(seg) {
|
|
74785
74867
|
let parts = String(seg || "").split("/").filter((x) => x.length > 0);
|
|
74786
74868
|
let treedb_id = parts[0] || "";
|
|
74787
74869
|
let tail = parts[1] || "";
|
|
74788
|
-
if (!treedb_id) return
|
|
74789
|
-
|
|
74790
|
-
|
|
74870
|
+
if (!treedb_id) return {
|
|
74871
|
+
treedb_id: "",
|
|
74872
|
+
topic_name: "",
|
|
74873
|
+
diagram: false
|
|
74874
|
+
};
|
|
74875
|
+
if (tail === DIAGRAM_SEG) return {
|
|
74876
|
+
treedb_id,
|
|
74877
|
+
topic_name: "",
|
|
74878
|
+
diagram: true
|
|
74879
|
+
};
|
|
74880
|
+
return {
|
|
74881
|
+
treedb_id,
|
|
74882
|
+
topic_name: tail,
|
|
74883
|
+
diagram: false
|
|
74884
|
+
};
|
|
74885
|
+
}
|
|
74886
|
+
/***************************************************************
|
|
74887
|
+
* Where the operator is, taken WITHOUT a screen: there is no model
|
|
74888
|
+
* to draw it on yet, and the load that brings one goes there.
|
|
74889
|
+
***************************************************************/
|
|
74890
|
+
function adopt_seg(gobj, seg) {
|
|
74891
|
+
let priv = gobj.priv;
|
|
74892
|
+
let pos = seg_position(seg);
|
|
74893
|
+
priv.treedb_id = pos.treedb_id;
|
|
74894
|
+
priv.topic_name = pos.topic_name;
|
|
74895
|
+
priv.diagram = pos.diagram;
|
|
74791
74896
|
}
|
|
74792
74897
|
/***************************
|
|
74793
74898
|
* Actions
|
|
@@ -74815,6 +74920,7 @@ function ac_mt_command_answer(gobj, event, kw, src) {
|
|
|
74815
74920
|
if (command === "nodes") {
|
|
74816
74921
|
let topic_name = (0, _yuneta_gobj_js.kw_get_str)(gobj, md, "topic_name", "", 0);
|
|
74817
74922
|
if (!is_current_load(gobj, md)) {
|
|
74923
|
+
if (result < 0 && (0, _yuneta_gobj_js.kw_get_int)(gobj, md, "round", 0, 0) === priv.dropped_round) return 0;
|
|
74818
74924
|
(0, _yuneta_gobj_js.log_warning)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: 'nodes' of '${topic_name}' answered for a load that is over: ignored`);
|
|
74819
74925
|
return 0;
|
|
74820
74926
|
}
|
|
@@ -74824,20 +74930,22 @@ function ac_mt_command_answer(gobj, event, kw, src) {
|
|
|
74824
74930
|
if (priv.pending > 0) return 0;
|
|
74825
74931
|
if (priv.load_error) {
|
|
74826
74932
|
priv.model = null;
|
|
74827
|
-
|
|
74828
|
-
render(gobj);
|
|
74829
|
-
return 0;
|
|
74933
|
+
return end_load(gobj);
|
|
74830
74934
|
}
|
|
74831
74935
|
priv.order_undo = {};
|
|
74832
74936
|
build_model(gobj);
|
|
74833
74937
|
start_measuring(gobj);
|
|
74834
|
-
return
|
|
74938
|
+
return end_load(gobj);
|
|
74835
74939
|
}
|
|
74836
74940
|
if (command === "update-node" || command === "delete-node") {
|
|
74837
74941
|
let topic_name = (0, _yuneta_gobj_js.kw_get_str)(gobj, md, "topic_name", "", 0);
|
|
74838
74942
|
if (!is_current_write(gobj, md)) {
|
|
74839
|
-
(0
|
|
74840
|
-
|
|
74943
|
+
if (result < 0 && (0, _yuneta_gobj_js.kw_get_int)(gobj, md, "write", 0, 0) === priv.dropped_write) return 0;
|
|
74944
|
+
if (result < 0) {
|
|
74945
|
+
(0, _yuneta_gobj_js.log_warning)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: '${command}' of '${topic_name}' answered for a write that is over (result ${result}): ignored`);
|
|
74946
|
+
return 0;
|
|
74947
|
+
}
|
|
74948
|
+
return late_write_done(gobj, command, topic_name);
|
|
74841
74949
|
}
|
|
74842
74950
|
if (result < 0) return end_writes(gobj, comment || (0, i18next.t)("the treedb refused the write"));
|
|
74843
74951
|
priv.save_wrote = true;
|
|
@@ -74863,6 +74971,35 @@ function ac_mt_command_answer(gobj, event, kw, src) {
|
|
|
74863
74971
|
return 0;
|
|
74864
74972
|
}
|
|
74865
74973
|
/***************************************************************
|
|
74974
|
+
* A write this view gave up on (a drop, a deadline) answers that
|
|
74975
|
+
* it was DONE: the store holds a change the model does not show.
|
|
74976
|
+
* The model is read again -- now, or when what is in flight ends,
|
|
74977
|
+
* or on the reconnect. A load in flight needs nothing: it was
|
|
74978
|
+
* asked after the write, on the same transport, and reads it.
|
|
74979
|
+
*
|
|
74980
|
+
* Only when the answer reaches this view. A routing adapter that
|
|
74981
|
+
* settled the write on its own deadline (gui_agent's
|
|
74982
|
+
* C_AGENT_TREEDB_LINK) does not deliver its late answer again --
|
|
74983
|
+
* the view was answered once, as failed -- and echoes the node
|
|
74984
|
+
* event instead, which this view does not hear: there, Refresh is
|
|
74985
|
+
* what reads the store.
|
|
74986
|
+
***************************************************************/
|
|
74987
|
+
function late_write_done(gobj, command, topic_name) {
|
|
74988
|
+
let priv = gobj.priv;
|
|
74989
|
+
let state = (0, _yuneta_gobj_js.gobj_current_state)(gobj);
|
|
74990
|
+
(0, _yuneta_gobj_js.log_warning)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: '${command}' of '${topic_name}' was given up, and it was DONE: the model is read again`);
|
|
74991
|
+
if (state === "ST_LOADING") return 0;
|
|
74992
|
+
if (state === "ST_SAVING") {
|
|
74993
|
+
priv.reload_after_write = true;
|
|
74994
|
+
return 0;
|
|
74995
|
+
}
|
|
74996
|
+
if (!transport_in_session(gobj)) {
|
|
74997
|
+
priv.reload_on_open = true;
|
|
74998
|
+
return 0;
|
|
74999
|
+
}
|
|
75000
|
+
return request_model(gobj);
|
|
75001
|
+
}
|
|
75002
|
+
/***************************************************************
|
|
74866
75003
|
* Remember which topic this write belongs to, which is what
|
|
74867
75004
|
* "its version still has to move" is asked about.
|
|
74868
75005
|
***************************************************************/
|
|
@@ -74882,7 +75019,8 @@ function mark_written(gobj, topic_name, record) {
|
|
|
74882
75019
|
function ac_show(gobj, event, kw, src) {
|
|
74883
75020
|
let priv = gobj.priv;
|
|
74884
75021
|
let seg = kw && kw.subpath || "";
|
|
74885
|
-
|
|
75022
|
+
let state = (0, _yuneta_gobj_js.gobj_current_state)(gobj);
|
|
75023
|
+
if (state === "ST_SAVING" || state === "ST_LOADING") {
|
|
74886
75024
|
priv.pending_seg = seg;
|
|
74887
75025
|
return 0;
|
|
74888
75026
|
}
|
package/dist/gobj-ui.es.js
CHANGED
|
@@ -3020,6 +3020,14 @@ function maybe_apply_translator($node, opts) {
|
|
|
3020
3020
|
* Non-blocking, auto-dismiss after `opts.timeout` ms (default
|
|
3021
3021
|
* 5000). `opts.timeout = 0` disables auto-dismiss.
|
|
3022
3022
|
* Returns `{ close() }` so callers can dismiss programmatically.
|
|
3023
|
+
*
|
|
3024
|
+
* ONE toast per message on screen: a string message of the same
|
|
3025
|
+
* kind as a toast still showing is not stacked under it -- that
|
|
3026
|
+
* toast is kept, its time starts again, and its handle is
|
|
3027
|
+
* returned. One close of a transport settles every request it
|
|
3028
|
+
* cut, each view shows its failure, and the operator got a
|
|
3029
|
+
* column of identical "the connection dropped". A message built
|
|
3030
|
+
* of nodes is never merged: its text is not a key.
|
|
3023
3031
|
***************************************************************/
|
|
3024
3032
|
function show_notification(shell, kind, message, opts) {
|
|
3025
3033
|
let $layer = notification_layer(shell);
|
|
@@ -3027,6 +3035,13 @@ function show_notification(shell, kind, message, opts) {
|
|
|
3027
3035
|
log_warning("yui_shell_show_*: shell has no notification layer");
|
|
3028
3036
|
return { close: () => {} };
|
|
3029
3037
|
}
|
|
3038
|
+
let key = typeof message === "string" ? `${kind}\u0000${message}` : null;
|
|
3039
|
+
if (key !== null) {
|
|
3040
|
+
for (let $shown of Array.from($layer.children)) if ($shown.__toast_key__ === key && $shown.__toast__) {
|
|
3041
|
+
$shown.__toast__.restart();
|
|
3042
|
+
return $shown.__toast__.handle;
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3030
3045
|
let p_attrs = typeof message === "string" ? { i18n: message } : {};
|
|
3031
3046
|
let $note = createElement2([
|
|
3032
3047
|
"div",
|
|
@@ -3063,8 +3078,21 @@ function show_notification(shell, kind, message, opts) {
|
|
|
3063
3078
|
let $del = $note.querySelector(".delete");
|
|
3064
3079
|
if ($del) $del.addEventListener("click", close);
|
|
3065
3080
|
let timeout = opts && opts.timeout != null ? opts.timeout : 5e3;
|
|
3066
|
-
|
|
3067
|
-
|
|
3081
|
+
let restart = function() {
|
|
3082
|
+
if (timeout_id) {
|
|
3083
|
+
clearTimeout(timeout_id);
|
|
3084
|
+
timeout_id = null;
|
|
3085
|
+
}
|
|
3086
|
+
if (timeout > 0) timeout_id = setTimeout(close, timeout);
|
|
3087
|
+
};
|
|
3088
|
+
restart();
|
|
3089
|
+
let handle = { close };
|
|
3090
|
+
$note.__toast_key__ = key;
|
|
3091
|
+
$note.__toast__ = {
|
|
3092
|
+
handle,
|
|
3093
|
+
restart
|
|
3094
|
+
};
|
|
3095
|
+
return handle;
|
|
3068
3096
|
}
|
|
3069
3097
|
function yui_shell_show_info(shell, message, opts) {
|
|
3070
3098
|
return show_notification(shell, "info", message, opts);
|
|
@@ -72151,6 +72179,9 @@ var PRIVATE_DATA$1 = {
|
|
|
72151
72179
|
write_tag: 0,
|
|
72152
72180
|
connected: false,
|
|
72153
72181
|
reload_on_open: false,
|
|
72182
|
+
dropped_round: 0,
|
|
72183
|
+
dropped_write: 0,
|
|
72184
|
+
reload_after_write: false,
|
|
72154
72185
|
pending_seg: null,
|
|
72155
72186
|
import_plan: null,
|
|
72156
72187
|
import_pane: null,
|
|
@@ -74619,6 +74650,15 @@ function transport_in_session(gobj) {
|
|
|
74619
74650
|
* one the drop abandoned (or a deadline settled) can still come
|
|
74620
74651
|
* back: counted, it ended the next load with the old data or
|
|
74621
74652
|
* moved the next write's queue.
|
|
74653
|
+
*
|
|
74654
|
+
* The state is part of the question, and that is only true
|
|
74655
|
+
* because NOTHING moves the editor out of ST_LOADING or ST_SAVING
|
|
74656
|
+
* but the end of what is in flight: a position the host sends
|
|
74657
|
+
* meanwhile waits in `pending_seg` (ac_show()). 7.25.6 let EV_SHOW
|
|
74658
|
+
* move a RELOAD out of ST_LOADING, and every answer of it was then
|
|
74659
|
+
* "for a load that is over": the records it had emptied stayed
|
|
74660
|
+
* empty, and the next write patched them into a model with no
|
|
74661
|
+
* treedb at all.
|
|
74622
74662
|
***************************************************************/
|
|
74623
74663
|
function is_current_load(gobj, md) {
|
|
74624
74664
|
return gobj_current_state(gobj) === "ST_LOADING" && kw_get_int(gobj, md, "round", 0, 0) === gobj.priv.load_round;
|
|
@@ -74639,17 +74679,39 @@ function transport_dropped(gobj) {
|
|
|
74639
74679
|
let state = gobj_current_state(gobj);
|
|
74640
74680
|
priv.reload_on_open = true;
|
|
74641
74681
|
if (state === "ST_SAVING") {
|
|
74682
|
+
log_warning(`${gobj_short_name(gobj)}: the transport dropped during a write: the model is read again on the reconnect`);
|
|
74683
|
+
priv.dropped_write = priv.write_tag;
|
|
74642
74684
|
priv.write_tag++;
|
|
74643
74685
|
return end_writes(gobj, "the connection dropped during the write");
|
|
74644
74686
|
}
|
|
74645
74687
|
if (state === "ST_LOADING") {
|
|
74688
|
+
log_warning(`${gobj_short_name(gobj)}: the transport dropped during the load: the model is read again on the reconnect`);
|
|
74689
|
+
priv.dropped_round = priv.load_round;
|
|
74646
74690
|
priv.load_round++;
|
|
74647
74691
|
priv.pending = 0;
|
|
74648
74692
|
priv.load_error = "";
|
|
74649
|
-
|
|
74650
|
-
|
|
74651
|
-
|
|
74693
|
+
return end_load(gobj);
|
|
74694
|
+
}
|
|
74695
|
+
return 0;
|
|
74696
|
+
}
|
|
74697
|
+
/***************************************************************
|
|
74698
|
+
* A load is over -- landed, failed or cut -- and the editor goes
|
|
74699
|
+
* where the operator is: the position the host sent while it was
|
|
74700
|
+
* in flight (ac_show() kept it), or the one it had. Without a
|
|
74701
|
+
* model there is no screen to go to: the position is kept for the
|
|
74702
|
+
* next load, and the editor says why it is empty.
|
|
74703
|
+
***************************************************************/
|
|
74704
|
+
function end_load(gobj) {
|
|
74705
|
+
let priv = gobj.priv;
|
|
74706
|
+
let seg = priv.pending_seg;
|
|
74707
|
+
priv.pending_seg = null;
|
|
74708
|
+
if (priv.model) {
|
|
74709
|
+
if (seg !== null) return apply_seg(gobj, seg);
|
|
74710
|
+
return go(gobj, priv.treedb_id, priv.topic_name, priv.diagram, false);
|
|
74652
74711
|
}
|
|
74712
|
+
if (seg !== null) adopt_seg(gobj, seg);
|
|
74713
|
+
gobj_change_state(gobj, "ST_IDLE");
|
|
74714
|
+
render(gobj);
|
|
74653
74715
|
return 0;
|
|
74654
74716
|
}
|
|
74655
74717
|
/***************************************************************
|
|
@@ -74672,7 +74734,7 @@ function end_writes(gobj, error) {
|
|
|
74672
74734
|
apply_seg(gobj, seg);
|
|
74673
74735
|
if (priv.save_wrote) gobj_publish_event(gobj, "EV_RECORD_WRITTEN", { treedb_name: gobj_read_str_attr(gobj, "treedb_name") });
|
|
74674
74736
|
priv.save_wrote = false;
|
|
74675
|
-
return
|
|
74737
|
+
return reload_if_owed(gobj);
|
|
74676
74738
|
}
|
|
74677
74739
|
}
|
|
74678
74740
|
if (error) yui_shell_show_error(yui_shell_of(gobj), error, { t });
|
|
@@ -74680,9 +74742,25 @@ function end_writes(gobj, error) {
|
|
|
74680
74742
|
priv.save_wrote = false;
|
|
74681
74743
|
render(gobj);
|
|
74682
74744
|
publish_check(gobj);
|
|
74745
|
+
reload_if_owed(gobj);
|
|
74683
74746
|
return error ? -1 : 0;
|
|
74684
74747
|
}
|
|
74685
74748
|
/***************************************************************
|
|
74749
|
+
* A write given up earlier turned out DONE while another one was
|
|
74750
|
+
* in flight (see ac_mt_command_answer()): the model is read again
|
|
74751
|
+
* now that nothing is.
|
|
74752
|
+
***************************************************************/
|
|
74753
|
+
function reload_if_owed(gobj) {
|
|
74754
|
+
let priv = gobj.priv;
|
|
74755
|
+
if (!priv.reload_after_write) return 0;
|
|
74756
|
+
priv.reload_after_write = false;
|
|
74757
|
+
if (!transport_in_session(gobj)) {
|
|
74758
|
+
priv.reload_on_open = true;
|
|
74759
|
+
return 0;
|
|
74760
|
+
}
|
|
74761
|
+
return request_model(gobj);
|
|
74762
|
+
}
|
|
74763
|
+
/***************************************************************
|
|
74686
74764
|
* The record a column form produces.
|
|
74687
74765
|
*
|
|
74688
74766
|
* Everything the store needs and the operator did not type is
|
|
@@ -74750,7 +74828,7 @@ function go(gobj, treedb_id, topic_name, diagram, mirror) {
|
|
|
74750
74828
|
priv.topic_name = topic_name || "";
|
|
74751
74829
|
priv.diagram = !!diagram;
|
|
74752
74830
|
let state = "ST_TREEDBS";
|
|
74753
|
-
if (!priv.model) state = "
|
|
74831
|
+
if (!priv.model) state = "ST_IDLE";
|
|
74754
74832
|
else if (priv.model.treedbs.length === 0) state = "ST_EMPTY";
|
|
74755
74833
|
else if (priv.treedb_id && priv.diagram) state = "ST_DIAGRAM";
|
|
74756
74834
|
else if (priv.treedb_id && priv.topic_name) state = "ST_COLUMNS";
|
|
@@ -74777,12 +74855,39 @@ function position_seg(gobj) {
|
|
|
74777
74855
|
* empty screen: a shared link outlives the schema it points at.
|
|
74778
74856
|
***************************************************************/
|
|
74779
74857
|
function apply_seg(gobj, seg) {
|
|
74858
|
+
let pos = seg_position(seg);
|
|
74859
|
+
return go(gobj, pos.treedb_id, pos.topic_name, pos.diagram, false);
|
|
74860
|
+
}
|
|
74861
|
+
function seg_position(seg) {
|
|
74780
74862
|
let parts = String(seg || "").split("/").filter((x) => x.length > 0);
|
|
74781
74863
|
let treedb_id = parts[0] || "";
|
|
74782
74864
|
let tail = parts[1] || "";
|
|
74783
|
-
if (!treedb_id) return
|
|
74784
|
-
|
|
74785
|
-
|
|
74865
|
+
if (!treedb_id) return {
|
|
74866
|
+
treedb_id: "",
|
|
74867
|
+
topic_name: "",
|
|
74868
|
+
diagram: false
|
|
74869
|
+
};
|
|
74870
|
+
if (tail === DIAGRAM_SEG) return {
|
|
74871
|
+
treedb_id,
|
|
74872
|
+
topic_name: "",
|
|
74873
|
+
diagram: true
|
|
74874
|
+
};
|
|
74875
|
+
return {
|
|
74876
|
+
treedb_id,
|
|
74877
|
+
topic_name: tail,
|
|
74878
|
+
diagram: false
|
|
74879
|
+
};
|
|
74880
|
+
}
|
|
74881
|
+
/***************************************************************
|
|
74882
|
+
* Where the operator is, taken WITHOUT a screen: there is no model
|
|
74883
|
+
* to draw it on yet, and the load that brings one goes there.
|
|
74884
|
+
***************************************************************/
|
|
74885
|
+
function adopt_seg(gobj, seg) {
|
|
74886
|
+
let priv = gobj.priv;
|
|
74887
|
+
let pos = seg_position(seg);
|
|
74888
|
+
priv.treedb_id = pos.treedb_id;
|
|
74889
|
+
priv.topic_name = pos.topic_name;
|
|
74890
|
+
priv.diagram = pos.diagram;
|
|
74786
74891
|
}
|
|
74787
74892
|
/***************************
|
|
74788
74893
|
* Actions
|
|
@@ -74810,6 +74915,7 @@ function ac_mt_command_answer(gobj, event, kw, src) {
|
|
|
74810
74915
|
if (command === "nodes") {
|
|
74811
74916
|
let topic_name = kw_get_str(gobj, md, "topic_name", "", 0);
|
|
74812
74917
|
if (!is_current_load(gobj, md)) {
|
|
74918
|
+
if (result < 0 && kw_get_int(gobj, md, "round", 0, 0) === priv.dropped_round) return 0;
|
|
74813
74919
|
log_warning(`${gobj_short_name(gobj)}: 'nodes' of '${topic_name}' answered for a load that is over: ignored`);
|
|
74814
74920
|
return 0;
|
|
74815
74921
|
}
|
|
@@ -74819,20 +74925,22 @@ function ac_mt_command_answer(gobj, event, kw, src) {
|
|
|
74819
74925
|
if (priv.pending > 0) return 0;
|
|
74820
74926
|
if (priv.load_error) {
|
|
74821
74927
|
priv.model = null;
|
|
74822
|
-
|
|
74823
|
-
render(gobj);
|
|
74824
|
-
return 0;
|
|
74928
|
+
return end_load(gobj);
|
|
74825
74929
|
}
|
|
74826
74930
|
priv.order_undo = {};
|
|
74827
74931
|
build_model(gobj);
|
|
74828
74932
|
start_measuring(gobj);
|
|
74829
|
-
return
|
|
74933
|
+
return end_load(gobj);
|
|
74830
74934
|
}
|
|
74831
74935
|
if (command === "update-node" || command === "delete-node") {
|
|
74832
74936
|
let topic_name = kw_get_str(gobj, md, "topic_name", "", 0);
|
|
74833
74937
|
if (!is_current_write(gobj, md)) {
|
|
74834
|
-
|
|
74835
|
-
|
|
74938
|
+
if (result < 0 && kw_get_int(gobj, md, "write", 0, 0) === priv.dropped_write) return 0;
|
|
74939
|
+
if (result < 0) {
|
|
74940
|
+
log_warning(`${gobj_short_name(gobj)}: '${command}' of '${topic_name}' answered for a write that is over (result ${result}): ignored`);
|
|
74941
|
+
return 0;
|
|
74942
|
+
}
|
|
74943
|
+
return late_write_done(gobj, command, topic_name);
|
|
74836
74944
|
}
|
|
74837
74945
|
if (result < 0) return end_writes(gobj, comment || t("the treedb refused the write"));
|
|
74838
74946
|
priv.save_wrote = true;
|
|
@@ -74858,6 +74966,35 @@ function ac_mt_command_answer(gobj, event, kw, src) {
|
|
|
74858
74966
|
return 0;
|
|
74859
74967
|
}
|
|
74860
74968
|
/***************************************************************
|
|
74969
|
+
* A write this view gave up on (a drop, a deadline) answers that
|
|
74970
|
+
* it was DONE: the store holds a change the model does not show.
|
|
74971
|
+
* The model is read again -- now, or when what is in flight ends,
|
|
74972
|
+
* or on the reconnect. A load in flight needs nothing: it was
|
|
74973
|
+
* asked after the write, on the same transport, and reads it.
|
|
74974
|
+
*
|
|
74975
|
+
* Only when the answer reaches this view. A routing adapter that
|
|
74976
|
+
* settled the write on its own deadline (gui_agent's
|
|
74977
|
+
* C_AGENT_TREEDB_LINK) does not deliver its late answer again --
|
|
74978
|
+
* the view was answered once, as failed -- and echoes the node
|
|
74979
|
+
* event instead, which this view does not hear: there, Refresh is
|
|
74980
|
+
* what reads the store.
|
|
74981
|
+
***************************************************************/
|
|
74982
|
+
function late_write_done(gobj, command, topic_name) {
|
|
74983
|
+
let priv = gobj.priv;
|
|
74984
|
+
let state = gobj_current_state(gobj);
|
|
74985
|
+
log_warning(`${gobj_short_name(gobj)}: '${command}' of '${topic_name}' was given up, and it was DONE: the model is read again`);
|
|
74986
|
+
if (state === "ST_LOADING") return 0;
|
|
74987
|
+
if (state === "ST_SAVING") {
|
|
74988
|
+
priv.reload_after_write = true;
|
|
74989
|
+
return 0;
|
|
74990
|
+
}
|
|
74991
|
+
if (!transport_in_session(gobj)) {
|
|
74992
|
+
priv.reload_on_open = true;
|
|
74993
|
+
return 0;
|
|
74994
|
+
}
|
|
74995
|
+
return request_model(gobj);
|
|
74996
|
+
}
|
|
74997
|
+
/***************************************************************
|
|
74861
74998
|
* Remember which topic this write belongs to, which is what
|
|
74862
74999
|
* "its version still has to move" is asked about.
|
|
74863
75000
|
***************************************************************/
|
|
@@ -74877,7 +75014,8 @@ function mark_written(gobj, topic_name, record) {
|
|
|
74877
75014
|
function ac_show(gobj, event, kw, src) {
|
|
74878
75015
|
let priv = gobj.priv;
|
|
74879
75016
|
let seg = kw && kw.subpath || "";
|
|
74880
|
-
|
|
75017
|
+
let state = gobj_current_state(gobj);
|
|
75018
|
+
if (state === "ST_SAVING" || state === "ST_LOADING") {
|
|
74881
75019
|
priv.pending_seg = seg;
|
|
74882
75020
|
return 0;
|
|
74883
75021
|
}
|
package/package.json
CHANGED
|
@@ -231,8 +231,11 @@ let PRIVATE_DATA = {
|
|
|
231
231
|
write_tag: 0, /* which write an answer belongs to: echoed as `write` */
|
|
232
232
|
connected: false,
|
|
233
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 */
|
|
234
237
|
|
|
235
|
-
/* A position that arrived while a write was in flight */
|
|
238
|
+
/* A position that arrived while a load or a write was in flight */
|
|
236
239
|
pending_seg: null,
|
|
237
240
|
|
|
238
241
|
/* The import dialog's plan and the two nodes it draws into */
|
|
@@ -2403,6 +2406,15 @@ function transport_in_session(gobj)
|
|
|
2403
2406
|
* one the drop abandoned (or a deadline settled) can still come
|
|
2404
2407
|
* back: counted, it ended the next load with the old data or
|
|
2405
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.
|
|
2406
2418
|
***************************************************************/
|
|
2407
2419
|
function is_current_load(gobj, md)
|
|
2408
2420
|
{
|
|
@@ -2431,19 +2443,51 @@ function transport_dropped(gobj)
|
|
|
2431
2443
|
|
|
2432
2444
|
priv.reload_on_open = true;
|
|
2433
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;
|
|
2434
2449
|
priv.write_tag++;
|
|
2435
2450
|
return end_writes(gobj, "the connection dropped during the write");
|
|
2436
2451
|
}
|
|
2437
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;
|
|
2438
2459
|
priv.load_round++;
|
|
2439
2460
|
priv.pending = 0;
|
|
2440
2461
|
priv.load_error = "";
|
|
2441
|
-
|
|
2442
|
-
|
|
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);
|
|
2443
2483
|
}
|
|
2444
|
-
|
|
2445
|
-
|
|
2484
|
+
return go(gobj, priv.treedb_id, priv.topic_name, priv.diagram, false);
|
|
2485
|
+
}
|
|
2486
|
+
if(seg !== null) {
|
|
2487
|
+
adopt_seg(gobj, seg);
|
|
2446
2488
|
}
|
|
2489
|
+
gobj_change_state(gobj, "ST_IDLE");
|
|
2490
|
+
render(gobj);
|
|
2447
2491
|
return 0;
|
|
2448
2492
|
}
|
|
2449
2493
|
|
|
@@ -2476,7 +2520,7 @@ function end_writes(gobj, error)
|
|
|
2476
2520
|
});
|
|
2477
2521
|
}
|
|
2478
2522
|
priv.save_wrote = false;
|
|
2479
|
-
return
|
|
2523
|
+
return reload_if_owed(gobj);
|
|
2480
2524
|
}
|
|
2481
2525
|
}
|
|
2482
2526
|
|
|
@@ -2491,9 +2535,30 @@ function end_writes(gobj, error)
|
|
|
2491
2535
|
priv.save_wrote = false;
|
|
2492
2536
|
render(gobj);
|
|
2493
2537
|
publish_check(gobj);
|
|
2538
|
+
reload_if_owed(gobj);
|
|
2494
2539
|
return error ? -1 : 0;
|
|
2495
2540
|
}
|
|
2496
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
|
+
|
|
2497
2562
|
/***************************************************************
|
|
2498
2563
|
* The record a column form produces.
|
|
2499
2564
|
*
|
|
@@ -2599,9 +2664,13 @@ function go(gobj, treedb_id, topic_name, diagram, mirror)
|
|
|
2599
2664
|
priv.topic_name = topic_name || "";
|
|
2600
2665
|
priv.diagram = !!diagram;
|
|
2601
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. */
|
|
2602
2671
|
let state = "ST_TREEDBS";
|
|
2603
2672
|
if(!priv.model) {
|
|
2604
|
-
state = "
|
|
2673
|
+
state = "ST_IDLE";
|
|
2605
2674
|
} else if(priv.model.treedbs.length === 0) {
|
|
2606
2675
|
state = "ST_EMPTY";
|
|
2607
2676
|
} else if(priv.treedb_id && priv.diagram) {
|
|
@@ -2647,18 +2716,39 @@ function position_seg(gobj)
|
|
|
2647
2716
|
* empty screen: a shared link outlives the schema it points at.
|
|
2648
2717
|
***************************************************************/
|
|
2649
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)
|
|
2650
2726
|
{
|
|
2651
2727
|
let parts = String(seg || "").split("/").filter(x => x.length > 0);
|
|
2652
2728
|
let treedb_id = parts[0] || "";
|
|
2653
2729
|
let tail = parts[1] || "";
|
|
2654
2730
|
|
|
2655
2731
|
if(!treedb_id) {
|
|
2656
|
-
return
|
|
2732
|
+
return {treedb_id: "", topic_name: "", diagram: false};
|
|
2657
2733
|
}
|
|
2658
2734
|
if(tail === DIAGRAM_SEG) {
|
|
2659
|
-
return
|
|
2735
|
+
return {treedb_id: treedb_id, topic_name: "", diagram: true};
|
|
2660
2736
|
}
|
|
2661
|
-
return
|
|
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;
|
|
2662
2752
|
}
|
|
2663
2753
|
|
|
2664
2754
|
|
|
@@ -2719,6 +2809,9 @@ function ac_mt_command_answer(gobj, event, kw, src)
|
|
|
2719
2809
|
if(command === "nodes") {
|
|
2720
2810
|
let topic_name = kw_get_str(gobj, md, "topic_name", "", 0);
|
|
2721
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
|
+
}
|
|
2722
2815
|
log_warning(`${gobj_short_name(gobj)}: 'nodes' of '${topic_name}' answered ` +
|
|
2723
2816
|
`for a load that is over: ignored`);
|
|
2724
2817
|
return 0;
|
|
@@ -2734,9 +2827,7 @@ function ac_mt_command_answer(gobj, event, kw, src)
|
|
|
2734
2827
|
}
|
|
2735
2828
|
if(priv.load_error) {
|
|
2736
2829
|
priv.model = null;
|
|
2737
|
-
|
|
2738
|
-
render(gobj);
|
|
2739
|
-
return 0;
|
|
2830
|
+
return end_load(gobj);
|
|
2740
2831
|
}
|
|
2741
2832
|
/* Records straight from the store: what an undo would name is the
|
|
2742
2833
|
* order that was ON SCREEN, and this is the stored one arriving.
|
|
@@ -2749,17 +2840,24 @@ function ac_mt_command_answer(gobj, event, kw, src)
|
|
|
2749
2840
|
* against. Taken on the write patches instead, it measured every
|
|
2750
2841
|
* version against itself. */
|
|
2751
2842
|
start_measuring(gobj);
|
|
2752
|
-
/* Back to where the operator was
|
|
2753
|
-
* the answer to a deep link that
|
|
2754
|
-
|
|
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);
|
|
2755
2847
|
}
|
|
2756
2848
|
|
|
2757
2849
|
if(command === "update-node" || command === "delete-node") {
|
|
2758
2850
|
let topic_name = kw_get_str(gobj, md, "topic_name", "", 0);
|
|
2759
2851
|
if(!is_current_write(gobj, md)) {
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
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);
|
|
2763
2861
|
}
|
|
2764
2862
|
if(result < 0) {
|
|
2765
2863
|
return end_writes(gobj, comment || t("the treedb refused the write"));
|
|
@@ -2796,6 +2894,41 @@ function ac_mt_command_answer(gobj, event, kw, src)
|
|
|
2796
2894
|
return 0;
|
|
2797
2895
|
}
|
|
2798
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
|
+
|
|
2799
2932
|
/***************************************************************
|
|
2800
2933
|
* Remember which topic this write belongs to, which is what
|
|
2801
2934
|
* "its version still has to move" is asked about.
|
|
@@ -2822,10 +2955,14 @@ function ac_show(gobj, event, kw, src)
|
|
|
2822
2955
|
{
|
|
2823
2956
|
let priv = gobj.priv;
|
|
2824
2957
|
let seg = (kw && kw.subpath) || "";
|
|
2958
|
+
let state = gobj_current_state(gobj);
|
|
2825
2959
|
|
|
2826
|
-
|
|
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") {
|
|
2827
2964
|
priv.pending_seg = seg;
|
|
2828
|
-
return 0; /* applied when the write lands (end_writes) */
|
|
2965
|
+
return 0; /* applied when the write lands (end_writes) or the load (end_load) */
|
|
2829
2966
|
}
|
|
2830
2967
|
return apply_seg(gobj, seg);
|
|
2831
2968
|
}
|
|
@@ -56,8 +56,13 @@ const RECORDS = {
|
|
|
56
56
|
],
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
+
/* A transport out of session refuses in the RETURN, as C_IEVENT_CLI and
|
|
60
|
+
* the routing adapters do: a request that did not leave is not waited for. */
|
|
59
61
|
function remote_command_parser(gobj, command, kw, src)
|
|
60
62
|
{
|
|
63
|
+
if(gobj_current_state(gobj) !== "ST_SESSION") {
|
|
64
|
+
return `${command}: not in session`;
|
|
65
|
+
}
|
|
61
66
|
commands.push({command, kw, src});
|
|
62
67
|
return null;
|
|
63
68
|
}
|
|
@@ -123,7 +128,10 @@ function answer_the_load(editor, remote)
|
|
|
123
128
|
{
|
|
124
129
|
const asked = take("nodes");
|
|
125
130
|
for(const c of asked) {
|
|
126
|
-
|
|
131
|
+
/* A copy, as a transport hands it: the editor patches what it is
|
|
132
|
+
* given, and a shared fixture would carry one test's write into
|
|
133
|
+
* the next. */
|
|
134
|
+
answer(editor, remote, c, 0, JSON.parse(JSON.stringify(RECORDS[c.kw.topic_name])));
|
|
127
135
|
}
|
|
128
136
|
return asked.length;
|
|
129
137
|
}
|
|
@@ -183,6 +191,11 @@ function errors()
|
|
|
183
191
|
return logged.filter((l) => l.level === "error").map((l) => l.msg);
|
|
184
192
|
}
|
|
185
193
|
|
|
194
|
+
function warnings()
|
|
195
|
+
{
|
|
196
|
+
return logged.filter((l) => l.level === "warning").map((l) => l.msg);
|
|
197
|
+
}
|
|
198
|
+
|
|
186
199
|
describe("a drop while LOADING", () => {
|
|
187
200
|
|
|
188
201
|
test("the editor leaves ST_LOADING, and the reconnect loads again", () => {
|
|
@@ -276,9 +289,169 @@ describe("a drop while SAVING", () => {
|
|
|
276
289
|
topics: ["topics^db.users^cols"]});
|
|
277
290
|
expect(gobj_current_state(editor)).toBe("ST_SAVING");
|
|
278
291
|
|
|
292
|
+
/* ...and it says the first one was DONE, so once the second ends
|
|
293
|
+
* the model is read again (late_write_done()). */
|
|
279
294
|
answer(editor, remote, second, 0, {id: "db.users.name", value: "name", order: 2,
|
|
280
295
|
type: "string", topics: ["topics^db.users^cols"]});
|
|
296
|
+
expect(gobj_current_state(editor)).toBe("ST_LOADING");
|
|
297
|
+
expect(answer_the_load(editor, remote)).toBe(3);
|
|
298
|
+
expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
|
|
299
|
+
expect(errors()).toEqual([]);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
/*
|
|
304
|
+
* The regression of 7.25.6: a navigation DURING a reload. The load was
|
|
305
|
+
* matched by its round AND by the state being ST_LOADING, and EV_SHOW
|
|
306
|
+
* moved the editor out of ST_LOADING while its three `nodes` were in
|
|
307
|
+
* flight. Every answer was then "for a load that is over", the reload
|
|
308
|
+
* never landed, and request_model() had already emptied the records: the
|
|
309
|
+
* next write patched an empty store and the model lost every treedb.
|
|
310
|
+
*/
|
|
311
|
+
describe("a navigation DURING a reload", () => {
|
|
312
|
+
|
|
313
|
+
test("refresh, move, write: the model keeps every treedb", () => {
|
|
314
|
+
const {editor, remote, host} = build("n1", true);
|
|
315
|
+
gobj_send_event(editor, "EV_REFRESH", {}, host);
|
|
316
|
+
gobj_send_event(editor, "EV_SHOW", {subpath: "db"}, host);
|
|
317
|
+
gobj_send_event(editor, "EV_SHOW", {subpath: "db/users"}, host);
|
|
318
|
+
expect(answer_the_load(editor, remote)).toBe(3);
|
|
319
|
+
|
|
320
|
+
expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
|
|
321
|
+
expect(editor.priv.pending).toBe(0);
|
|
322
|
+
|
|
323
|
+
const write = start_a_write(editor, host);
|
|
324
|
+
answer(editor, remote, write, 0, {id: "db.users.name", value: "name", order: 2,
|
|
325
|
+
type: "string", topics: ["topics^db.users^cols"]});
|
|
326
|
+
expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
|
|
327
|
+
expect(editor.priv.model.treedbs.length).toBe(1);
|
|
328
|
+
expect(editor.priv.model.treedbs[0].topics[0].cols.length).toBe(2);
|
|
329
|
+
expect(warnings()).toEqual([]);
|
|
330
|
+
expect(errors()).toEqual([]);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test("the editor stays in ST_LOADING, and lands where the host said last", () => {
|
|
334
|
+
const {editor, remote, host} = build("n2", true);
|
|
335
|
+
gobj_send_event(editor, "EV_REFRESH", {}, host);
|
|
336
|
+
gobj_send_event(editor, "EV_SHOW", {subpath: "db"}, host);
|
|
337
|
+
expect(gobj_current_state(editor)).toBe("ST_LOADING");
|
|
338
|
+
|
|
339
|
+
expect(answer_the_load(editor, remote)).toBe(3);
|
|
340
|
+
expect(gobj_current_state(editor)).toBe("ST_TOPICS");
|
|
341
|
+
expect(editor.priv.treedb_id).toBe("db");
|
|
342
|
+
expect(editor.priv.topic_name).toBe("");
|
|
343
|
+
expect(warnings()).toEqual([]);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test("the reload of a reconnect, with a move in the middle", () => {
|
|
347
|
+
const {editor, remote, host} = build("n3", true);
|
|
348
|
+
start_a_write(editor, host);
|
|
349
|
+
drop(editor, remote, host); /* the reconnect will reload */
|
|
350
|
+
shown.length = 0;
|
|
351
|
+
reconnect(editor, remote, host);
|
|
352
|
+
expect(gobj_current_state(editor)).toBe("ST_LOADING");
|
|
353
|
+
|
|
354
|
+
gobj_send_event(editor, "EV_SHOW", {subpath: "db"}, host);
|
|
355
|
+
expect(answer_the_load(editor, remote)).toBe(3);
|
|
356
|
+
expect(gobj_current_state(editor)).toBe("ST_TOPICS");
|
|
357
|
+
expect(editor.priv.pending).toBe(0);
|
|
358
|
+
expect(editor.priv.records.cols.length).toBe(1);
|
|
359
|
+
expect(editor.priv.model.treedbs.length).toBe(1);
|
|
360
|
+
expect(errors()).toEqual([]);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test("a drop after the move: the move is applied on the model kept, and the reconnect reloads", () => {
|
|
364
|
+
const {editor, remote, host} = build("n4", true);
|
|
365
|
+
gobj_send_event(editor, "EV_REFRESH", {}, host);
|
|
366
|
+
take("nodes"); /* never answered */
|
|
367
|
+
gobj_send_event(editor, "EV_SHOW", {subpath: "db"}, host);
|
|
368
|
+
|
|
369
|
+
drop(editor, remote, host);
|
|
370
|
+
expect(gobj_current_state(editor)).toBe("ST_TOPICS");
|
|
371
|
+
|
|
372
|
+
reconnect(editor, remote, host);
|
|
373
|
+
expect(answer_the_load(editor, remote)).toBe(3);
|
|
374
|
+
expect(gobj_current_state(editor)).toBe("ST_TOPICS");
|
|
375
|
+
expect(editor.priv.model.treedbs.length).toBe(1);
|
|
376
|
+
expect(errors()).toEqual([]);
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
describe("a position with no model and no load", () => {
|
|
381
|
+
|
|
382
|
+
test("does not claim a load, and the session loads it", () => {
|
|
383
|
+
const remote = gobj_create_service("p1_remote", "C_TEST_REMOTE", {}, yuno);
|
|
384
|
+
gobj_change_state(remote, "ST_DISCONNECTED");
|
|
385
|
+
const host = gobj_create("p1_host", "C_TEST_EDITOR_HOST", {}, yuno);
|
|
386
|
+
const editor = gobj_create("p1_editor", "C_YUI_SCHEMA_EDITOR", {
|
|
387
|
+
gobj_remote_yuno: remote,
|
|
388
|
+
treedb_name: "treedb_system_schema",
|
|
389
|
+
base_route: "/schemas"
|
|
390
|
+
}, host);
|
|
391
|
+
gobj_start(editor); /* its load cannot leave */
|
|
392
|
+
logged.length = 0;
|
|
393
|
+
expect(gobj_current_state(editor)).toBe("ST_IDLE");
|
|
394
|
+
|
|
395
|
+
gobj_send_event(editor, "EV_SHOW", {subpath: "db/users"}, host);
|
|
396
|
+
expect(gobj_current_state(editor)).toBe("ST_IDLE");
|
|
397
|
+
|
|
398
|
+
reconnect(editor, remote, host);
|
|
399
|
+
expect(answer_the_load(editor, remote)).toBe(3);
|
|
281
400
|
expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
|
|
282
401
|
expect(errors()).toEqual([]);
|
|
283
402
|
});
|
|
284
403
|
});
|
|
404
|
+
|
|
405
|
+
describe("the drop is settled ONCE", () => {
|
|
406
|
+
|
|
407
|
+
test("a load cut by the drop: the transport's three failures are one event", () => {
|
|
408
|
+
const {editor, remote, host} = build("o1", true);
|
|
409
|
+
gobj_send_event(editor, "EV_REFRESH", {}, host);
|
|
410
|
+
const cut = take("nodes");
|
|
411
|
+
expect(cut.length).toBe(3);
|
|
412
|
+
|
|
413
|
+
/* A routing adapter settles all three when its session closes,
|
|
414
|
+
* before (or after) the host's edge. */
|
|
415
|
+
gobj_change_state(remote, "ST_DISCONNECTED");
|
|
416
|
+
for(const c of cut) {
|
|
417
|
+
answer(editor, remote, c, -1, null, "the connection dropped");
|
|
418
|
+
}
|
|
419
|
+
gobj_send_event(editor, "EV_TRANSPORT_STATE", {connected: false}, host);
|
|
420
|
+
expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
|
|
421
|
+
expect(warnings().length).toBe(1);
|
|
422
|
+
expect(warnings()[0]).toMatch(/dropped during the load/);
|
|
423
|
+
expect(errors()).toEqual([]);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
test("the host's edge first: the failures that follow are not said again", () => {
|
|
427
|
+
const {editor, remote, host} = build("o2", true);
|
|
428
|
+
gobj_send_event(editor, "EV_REFRESH", {}, host);
|
|
429
|
+
const cut = take("nodes");
|
|
430
|
+
|
|
431
|
+
drop(editor, remote, host);
|
|
432
|
+
for(const c of cut) {
|
|
433
|
+
answer(editor, remote, c, -1, null, "the connection dropped");
|
|
434
|
+
}
|
|
435
|
+
expect(warnings().length).toBe(1);
|
|
436
|
+
expect(errors()).toEqual([]);
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
describe("a write answered after it was given up", () => {
|
|
441
|
+
|
|
442
|
+
test("it was DONE: the model is read again", () => {
|
|
443
|
+
const {editor, remote, host} = build("w1", true);
|
|
444
|
+
const write = start_a_write(editor, host);
|
|
445
|
+
drop(editor, remote, host);
|
|
446
|
+
reconnect(editor, remote, host);
|
|
447
|
+
answer_the_load(editor, remote);
|
|
448
|
+
expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
|
|
449
|
+
|
|
450
|
+
/* The treedb says it wrote it, after all. */
|
|
451
|
+
answer(editor, remote, write, 0, {id: "db.users.name", value: "name",
|
|
452
|
+
topics: ["topics^db.users^cols"]});
|
|
453
|
+
expect(gobj_current_state(editor)).toBe("ST_LOADING");
|
|
454
|
+
expect(take("nodes").length).toBe(3);
|
|
455
|
+
expect(errors()).toEqual([]);
|
|
456
|
+
});
|
|
457
|
+
});
|
package/src/shell_modals.js
CHANGED
|
@@ -111,6 +111,14 @@ function maybe_apply_translator($node, opts)
|
|
|
111
111
|
* Non-blocking, auto-dismiss after `opts.timeout` ms (default
|
|
112
112
|
* 5000). `opts.timeout = 0` disables auto-dismiss.
|
|
113
113
|
* Returns `{ close() }` so callers can dismiss programmatically.
|
|
114
|
+
*
|
|
115
|
+
* ONE toast per message on screen: a string message of the same
|
|
116
|
+
* kind as a toast still showing is not stacked under it -- that
|
|
117
|
+
* toast is kept, its time starts again, and its handle is
|
|
118
|
+
* returned. One close of a transport settles every request it
|
|
119
|
+
* cut, each view shows its failure, and the operator got a
|
|
120
|
+
* column of identical "the connection dropped". A message built
|
|
121
|
+
* of nodes is never merged: its text is not a key.
|
|
114
122
|
***************************************************************/
|
|
115
123
|
function show_notification(shell, kind, message, opts)
|
|
116
124
|
{
|
|
@@ -120,6 +128,16 @@ function show_notification(shell, kind, message, opts)
|
|
|
120
128
|
return { close: () => {} };
|
|
121
129
|
}
|
|
122
130
|
|
|
131
|
+
let key = (typeof message === "string") ? `${kind}\u0000${message}` : null;
|
|
132
|
+
if(key !== null) {
|
|
133
|
+
for(let $shown of Array.from($layer.children)) {
|
|
134
|
+
if($shown.__toast_key__ === key && $shown.__toast__) {
|
|
135
|
+
$shown.__toast__.restart();
|
|
136
|
+
return $shown.__toast__.handle;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
123
141
|
let p_attrs = (typeof message === "string")
|
|
124
142
|
? {i18n: message}
|
|
125
143
|
: {};
|
|
@@ -158,11 +176,21 @@ function show_notification(shell, kind, message, opts)
|
|
|
158
176
|
}
|
|
159
177
|
|
|
160
178
|
let timeout = (opts && opts.timeout != null) ? opts.timeout : 5000;
|
|
161
|
-
|
|
162
|
-
timeout_id
|
|
163
|
-
|
|
179
|
+
let restart = function() {
|
|
180
|
+
if(timeout_id) {
|
|
181
|
+
clearTimeout(timeout_id);
|
|
182
|
+
timeout_id = null;
|
|
183
|
+
}
|
|
184
|
+
if(timeout > 0) {
|
|
185
|
+
timeout_id = setTimeout(close, timeout);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
restart();
|
|
164
189
|
|
|
165
|
-
|
|
190
|
+
let handle = { close };
|
|
191
|
+
$note.__toast_key__ = key;
|
|
192
|
+
$note.__toast__ = {handle: handle, restart: restart};
|
|
193
|
+
return handle;
|
|
166
194
|
}
|
|
167
195
|
|
|
168
196
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* shell_toast_once.test.js
|
|
3
|
+
*
|
|
4
|
+
* A toast that says what one already on screen says is not
|
|
5
|
+
* stacked under it: one close of the transport settles every
|
|
6
|
+
* request it cut, each view that asked shows its failure, and
|
|
7
|
+
* the operator got a column of identical "the connection
|
|
8
|
+
* dropped" (low of the independent review of the 2nd round).
|
|
9
|
+
*
|
|
10
|
+
* Copyright (c) 2026, ArtGins.
|
|
11
|
+
* All Rights Reserved.
|
|
12
|
+
***********************************************************************/
|
|
13
|
+
import {describe, test, expect, beforeEach} from "vitest";
|
|
14
|
+
import {install_dom_double} from "../test/dom_double.js";
|
|
15
|
+
|
|
16
|
+
install_dom_double();
|
|
17
|
+
|
|
18
|
+
const {
|
|
19
|
+
yui_shell_show_error,
|
|
20
|
+
yui_shell_show_info,
|
|
21
|
+
} = await import("./shell_modals.js");
|
|
22
|
+
|
|
23
|
+
let shell = null;
|
|
24
|
+
|
|
25
|
+
function toasts()
|
|
26
|
+
{
|
|
27
|
+
return shell.priv.layers.notification.children;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
shell = {priv: {layers: {notification: document.createElement("div")}}};
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("one toast per message on screen", () => {
|
|
35
|
+
|
|
36
|
+
test("the same error twice is ONE toast", () => {
|
|
37
|
+
yui_shell_show_error(shell, "the connection dropped");
|
|
38
|
+
yui_shell_show_error(shell, "the connection dropped");
|
|
39
|
+
yui_shell_show_error(shell, "the connection dropped");
|
|
40
|
+
expect(toasts().length).toBe(1);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("different messages, or kinds, still stack", () => {
|
|
44
|
+
yui_shell_show_error(shell, "the connection dropped");
|
|
45
|
+
yui_shell_show_error(shell, "the connection dropped during the write");
|
|
46
|
+
yui_shell_show_info(shell, "the connection dropped");
|
|
47
|
+
expect(toasts().length).toBe(3);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("closing the one on screen: the next one is shown", () => {
|
|
51
|
+
const first = yui_shell_show_error(shell, "the connection dropped");
|
|
52
|
+
first.close();
|
|
53
|
+
expect(toasts().length).toBe(0);
|
|
54
|
+
yui_shell_show_error(shell, "the connection dropped");
|
|
55
|
+
expect(toasts().length).toBe(1);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("the handle of the repeat closes the toast on screen", () => {
|
|
59
|
+
yui_shell_show_error(shell, "the connection dropped");
|
|
60
|
+
const again = yui_shell_show_error(shell, "the connection dropped");
|
|
61
|
+
again.close();
|
|
62
|
+
expect(toasts().length).toBe(0);
|
|
63
|
+
});
|
|
64
|
+
});
|