@yuneta/gobj-ui 7.25.5 → 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 +14 -0
- package/dist/gobj-ui.cjs.js +93 -9
- package/dist/gobj-ui.es.js +93 -9
- package/package.json +1 -1
- package/src/c_yui_schema_editor.js +123 -9
- package/src/schema_editor_transport.wiring.test.js +284 -0
package/README.md
CHANGED
|
@@ -2154,6 +2154,20 @@ else.
|
|
|
2154
2154
|
- In: `EV_SHOW` (`{subpath}` — the tail it owns is `<treedb>[/<topic>]` or
|
|
2155
2155
|
`<treedb>/diagram`), `EV_HIDE`, `EV_TRANSPORT_STATE`, `EV_REFRESH`,
|
|
2156
2156
|
`EV_LANGUAGE_CHANGED`, `EV_MT_COMMAND_ANSWER`.
|
|
2157
|
+
- A **drop** of the transport — `EV_TRANSPORT_STATE {connected: false}`, or a
|
|
2158
|
+
failed answer that arrives while the transport is out of `ST_SESSION` — ends
|
|
2159
|
+
the load or the write it cut, and the model is asked again on the next
|
|
2160
|
+
`{connected: true}`. A write cut this way is shown as
|
|
2161
|
+
`the connection dropped during the write` (an i18n key the consumer's
|
|
2162
|
+
locales carry). Each request carries its round in `__md_command__`
|
|
2163
|
+
(`round` / `write`), so an answer of what the drop cut is ignored with a
|
|
2164
|
+
warning.
|
|
2165
|
+
|
|
2166
|
+
```js
|
|
2167
|
+
// the host forwards its session edges; nothing else is needed
|
|
2168
|
+
gobj_send_event(editor, "EV_TRANSPORT_STATE", {connected: false}, host);
|
|
2169
|
+
gobj_send_event(editor, "EV_TRANSPORT_STATE", {connected: true}, host);
|
|
2170
|
+
```
|
|
2157
2171
|
- Out: `EV_POSITION_CHANGED` (`{subpath}` — the host writes the url; this view
|
|
2158
2172
|
navigates nothing itself), `EV_RECORD_WRITTEN` (whoever owns the Apply needs
|
|
2159
2173
|
to know the yuno has not re-read its schema yet), `EV_SCHEMA_CHECKED`
|
package/dist/gobj-ui.cjs.js
CHANGED
|
@@ -72101,6 +72101,12 @@ function write_options(op) {
|
|
|
72101
72101
|
* `update-node` / `delete-node` on the treedb service, and it may
|
|
72102
72102
|
* be a routing adapter two hops away (the agent console's
|
|
72103
72103
|
* C_AGENT_TREEDB_LINK) — this view cannot tell, and must not.
|
|
72104
|
+
* A DROP of it (the host's EV_TRANSPORT_STATE, or a failure that
|
|
72105
|
+
* arrives while the transport is out of session) ends the load or
|
|
72106
|
+
* the write it cut, and the model is asked again when the session
|
|
72107
|
+
* is back. Every request carries its round (`round` for a load,
|
|
72108
|
+
* `write` for a write) in `__md_command__`, so an answer of what
|
|
72109
|
+
* was cut is ignored instead of counted in the next one.
|
|
72104
72110
|
*
|
|
72105
72111
|
* Copyright (c) 2026, ArtGins.
|
|
72106
72112
|
* All Rights Reserved.
|
|
@@ -72133,6 +72139,7 @@ var PRIVATE_DATA$1 = {
|
|
|
72133
72139
|
model: null,
|
|
72134
72140
|
pending: 0,
|
|
72135
72141
|
load_error: "",
|
|
72142
|
+
load_round: 0,
|
|
72136
72143
|
treedb_id: "",
|
|
72137
72144
|
topic_name: "",
|
|
72138
72145
|
diagram: false,
|
|
@@ -72146,7 +72153,9 @@ var PRIVATE_DATA$1 = {
|
|
|
72146
72153
|
save_queue: null,
|
|
72147
72154
|
save_return: "",
|
|
72148
72155
|
save_wrote: false,
|
|
72156
|
+
write_tag: 0,
|
|
72149
72157
|
connected: false,
|
|
72158
|
+
reload_on_open: false,
|
|
72150
72159
|
pending_seg: null,
|
|
72151
72160
|
import_plan: null,
|
|
72152
72161
|
import_pane: null,
|
|
@@ -72317,6 +72326,7 @@ function request_model(gobj) {
|
|
|
72317
72326
|
};
|
|
72318
72327
|
priv.load_error = "";
|
|
72319
72328
|
priv.pending = 0;
|
|
72329
|
+
priv.load_round++;
|
|
72320
72330
|
(0, _yuneta_gobj_js.gobj_change_state)(gobj, "ST_LOADING");
|
|
72321
72331
|
show_notice(gobj, "loading the schemas");
|
|
72322
72332
|
for (let topic_name of [
|
|
@@ -72326,7 +72336,7 @@ function request_model(gobj) {
|
|
|
72326
72336
|
]) if (remote_command(gobj, "nodes", {
|
|
72327
72337
|
topic_name,
|
|
72328
72338
|
options: { list_dict: true }
|
|
72329
|
-
}) === 0) priv.pending++;
|
|
72339
|
+
}, { round: priv.load_round }) === 0) priv.pending++;
|
|
72330
72340
|
if (priv.pending === 0) {
|
|
72331
72341
|
priv.load_error = (0, i18next.t)("cannot reach the treedb");
|
|
72332
72342
|
(0, _yuneta_gobj_js.gobj_change_state)(gobj, "ST_IDLE");
|
|
@@ -72344,7 +72354,7 @@ function request_model(gobj) {
|
|
|
72344
72354
|
* answer says which topic it is about, since the command is
|
|
72345
72355
|
* `nodes` for all three.
|
|
72346
72356
|
***************************************************************/
|
|
72347
|
-
function remote_command(gobj, command, kw) {
|
|
72357
|
+
function remote_command(gobj, command, kw, tag) {
|
|
72348
72358
|
let remote = (0, _yuneta_gobj_js.gobj_read_pointer_attr)(gobj, "gobj_remote_yuno");
|
|
72349
72359
|
let treedb_name = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "treedb_name");
|
|
72350
72360
|
if (!remote) {
|
|
@@ -72355,11 +72365,11 @@ function remote_command(gobj, command, kw) {
|
|
|
72355
72365
|
service: treedb_name,
|
|
72356
72366
|
treedb_name
|
|
72357
72367
|
}, kw || {});
|
|
72358
|
-
full_kw.__md_command__ = {
|
|
72368
|
+
full_kw.__md_command__ = Object.assign({
|
|
72359
72369
|
purpose: PURPOSE,
|
|
72360
72370
|
topic_name: kw && kw.topic_name || "",
|
|
72361
72371
|
record_id: kw && kw.record_id || ""
|
|
72362
|
-
};
|
|
72372
|
+
}, tag || {});
|
|
72363
72373
|
delete full_kw.record_id;
|
|
72364
72374
|
let ret = (0, _yuneta_gobj_js.gobj_command)(remote, command, full_kw, gobj);
|
|
72365
72375
|
if (ret) {
|
|
@@ -74590,12 +74600,61 @@ function run_next_write(gobj) {
|
|
|
74590
74600
|
let write = priv.save_queue[0];
|
|
74591
74601
|
let command = write_command(write.op);
|
|
74592
74602
|
let options = write_options(write.op);
|
|
74603
|
+
priv.write_tag++;
|
|
74593
74604
|
if (remote_command(gobj, command, {
|
|
74594
74605
|
topic_name: write.topic_name,
|
|
74595
74606
|
record: write.record,
|
|
74596
74607
|
options,
|
|
74597
74608
|
record_id: write.record.id || ""
|
|
74598
|
-
}) < 0) return end_writes(gobj, (0, i18next.t)("cannot reach the treedb"));
|
|
74609
|
+
}, { write: priv.write_tag }) < 0) return end_writes(gobj, (0, i18next.t)("cannot reach the treedb"));
|
|
74610
|
+
return 0;
|
|
74611
|
+
}
|
|
74612
|
+
/***************************************************************
|
|
74613
|
+
* Does the transport say it is in session? The state the library
|
|
74614
|
+
* reads off every transport (C_IEVENT_CLI and the routing adapters
|
|
74615
|
+
* name it the same).
|
|
74616
|
+
***************************************************************/
|
|
74617
|
+
function transport_in_session(gobj) {
|
|
74618
|
+
let remote = (0, _yuneta_gobj_js.gobj_read_pointer_attr)(gobj, "gobj_remote_yuno");
|
|
74619
|
+
return !!remote && (0, _yuneta_gobj_js.gobj_current_state)(remote) === "ST_SESSION";
|
|
74620
|
+
}
|
|
74621
|
+
/***************************************************************
|
|
74622
|
+
* Is this answer of the load in flight, or of the write in
|
|
74623
|
+
* flight? Each request carries its round in `__md_command__`, and
|
|
74624
|
+
* one the drop abandoned (or a deadline settled) can still come
|
|
74625
|
+
* back: counted, it ended the next load with the old data or
|
|
74626
|
+
* moved the next write's queue.
|
|
74627
|
+
***************************************************************/
|
|
74628
|
+
function is_current_load(gobj, md) {
|
|
74629
|
+
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;
|
|
74630
|
+
}
|
|
74631
|
+
function is_current_write(gobj, md) {
|
|
74632
|
+
return (0, _yuneta_gobj_js.gobj_current_state)(gobj) === "ST_SAVING" && (0, _yuneta_gobj_js.kw_get_int)(gobj, md, "write", 0, 0) === gobj.priv.write_tag;
|
|
74633
|
+
}
|
|
74634
|
+
/***************************************************************
|
|
74635
|
+
* The transport dropped under a load or a write: its answers
|
|
74636
|
+
* will never come. The load goes back to the screen it replaced
|
|
74637
|
+
* (or to "not connected" when there was none), the write ends
|
|
74638
|
+
* with the drop said -- whether it landed is unknown -- and both
|
|
74639
|
+
* mark the model to be asked again when the session is back.
|
|
74640
|
+
* Bumping the round makes an answer of what was cut stale.
|
|
74641
|
+
***************************************************************/
|
|
74642
|
+
function transport_dropped(gobj) {
|
|
74643
|
+
let priv = gobj.priv;
|
|
74644
|
+
let state = (0, _yuneta_gobj_js.gobj_current_state)(gobj);
|
|
74645
|
+
priv.reload_on_open = true;
|
|
74646
|
+
if (state === "ST_SAVING") {
|
|
74647
|
+
priv.write_tag++;
|
|
74648
|
+
return end_writes(gobj, "the connection dropped during the write");
|
|
74649
|
+
}
|
|
74650
|
+
if (state === "ST_LOADING") {
|
|
74651
|
+
priv.load_round++;
|
|
74652
|
+
priv.pending = 0;
|
|
74653
|
+
priv.load_error = "";
|
|
74654
|
+
if (priv.model) return go(gobj, priv.treedb_id, priv.topic_name, priv.diagram, false);
|
|
74655
|
+
(0, _yuneta_gobj_js.gobj_change_state)(gobj, "ST_IDLE");
|
|
74656
|
+
render(gobj);
|
|
74657
|
+
}
|
|
74599
74658
|
return 0;
|
|
74600
74659
|
}
|
|
74601
74660
|
/***************************************************************
|
|
@@ -74750,8 +74809,15 @@ function ac_mt_command_answer(gobj, event, kw, src) {
|
|
|
74750
74809
|
let kw_command = (0, _yuneta_gobj_js.kw_get_dict)(gobj, __command__, "kw", {}, 0);
|
|
74751
74810
|
let md = (0, _yuneta_gobj_js.is_object)(kw_command.__md_command__) ? kw_command.__md_command__ : kw_command;
|
|
74752
74811
|
if ((0, _yuneta_gobj_js.kw_get_str)(gobj, md, "purpose", "", 0) !== PURPOSE) return 0;
|
|
74812
|
+
if (result < 0 && !transport_in_session(gobj) && ((0, _yuneta_gobj_js.gobj_current_state)(gobj) === "ST_LOADING" || (0, _yuneta_gobj_js.gobj_current_state)(gobj) === "ST_SAVING")) {
|
|
74813
|
+
if (command === "nodes" && is_current_load(gobj, md) || command !== "nodes" && is_current_write(gobj, md)) return transport_dropped(gobj);
|
|
74814
|
+
}
|
|
74753
74815
|
if (command === "nodes") {
|
|
74754
74816
|
let topic_name = (0, _yuneta_gobj_js.kw_get_str)(gobj, md, "topic_name", "", 0);
|
|
74817
|
+
if (!is_current_load(gobj, md)) {
|
|
74818
|
+
(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
|
+
return 0;
|
|
74820
|
+
}
|
|
74755
74821
|
priv.pending--;
|
|
74756
74822
|
if (result < 0) priv.load_error = comment || (0, i18next.t)("cannot load the schemas");
|
|
74757
74823
|
else if (priv.records && Array.isArray(priv.records[topic_name])) priv.records[topic_name] = Array.isArray(data) ? data : [];
|
|
@@ -74769,6 +74835,10 @@ function ac_mt_command_answer(gobj, event, kw, src) {
|
|
|
74769
74835
|
}
|
|
74770
74836
|
if (command === "update-node" || command === "delete-node") {
|
|
74771
74837
|
let topic_name = (0, _yuneta_gobj_js.kw_get_str)(gobj, md, "topic_name", "", 0);
|
|
74838
|
+
if (!is_current_write(gobj, md)) {
|
|
74839
|
+
(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`);
|
|
74840
|
+
return 0;
|
|
74841
|
+
}
|
|
74772
74842
|
if (result < 0) return end_writes(gobj, comment || (0, i18next.t)("the treedb refused the write"));
|
|
74773
74843
|
priv.save_wrote = true;
|
|
74774
74844
|
if (command === "delete-node") forget_record(gobj, topic_name, (0, _yuneta_gobj_js.kw_get_str)(gobj, md, "record_id", "", 0));
|
|
@@ -74828,14 +74898,28 @@ function ac_hide(gobj, event, kw, src) {
|
|
|
74828
74898
|
return 0;
|
|
74829
74899
|
}
|
|
74830
74900
|
/***************************************************************
|
|
74831
|
-
* The session came up or went down. The model stays
|
|
74832
|
-
*
|
|
74833
|
-
*
|
|
74901
|
+
* The session came up or went down. The model stays -- a schema
|
|
74902
|
+
* does not change under you -- unless the drop cut a load or a
|
|
74903
|
+
* write (transport_dropped()): then it is asked again on the way
|
|
74904
|
+
* back, as it is when a first load had no transport to run on.
|
|
74905
|
+
*
|
|
74906
|
+
* Nothing else ends a load or a write the drop cut: their answers
|
|
74907
|
+
* will never come, and waiting in ST_LOADING skipped the reload
|
|
74908
|
+
* itself (M-1 of the independent review of 7.25.4).
|
|
74834
74909
|
***************************************************************/
|
|
74835
74910
|
function ac_transport_state(gobj, event, kw, src) {
|
|
74836
74911
|
let priv = gobj.priv;
|
|
74912
|
+
let state = (0, _yuneta_gobj_js.gobj_current_state)(gobj);
|
|
74837
74913
|
priv.connected = !!(kw && kw.connected);
|
|
74838
|
-
if (priv.connected
|
|
74914
|
+
if (!priv.connected) {
|
|
74915
|
+
if (state === "ST_LOADING" || state === "ST_SAVING") return transport_dropped(gobj);
|
|
74916
|
+
render_toolbar(gobj);
|
|
74917
|
+
return 0;
|
|
74918
|
+
}
|
|
74919
|
+
if ((!priv.model || priv.reload_on_open) && state !== "ST_LOADING" && state !== "ST_SAVING") {
|
|
74920
|
+
priv.reload_on_open = false;
|
|
74921
|
+
return request_model(gobj);
|
|
74922
|
+
}
|
|
74839
74923
|
render_toolbar(gobj);
|
|
74840
74924
|
return 0;
|
|
74841
74925
|
}
|
package/dist/gobj-ui.es.js
CHANGED
|
@@ -72096,6 +72096,12 @@ function write_options(op) {
|
|
|
72096
72096
|
* `update-node` / `delete-node` on the treedb service, and it may
|
|
72097
72097
|
* be a routing adapter two hops away (the agent console's
|
|
72098
72098
|
* C_AGENT_TREEDB_LINK) — this view cannot tell, and must not.
|
|
72099
|
+
* A DROP of it (the host's EV_TRANSPORT_STATE, or a failure that
|
|
72100
|
+
* arrives while the transport is out of session) ends the load or
|
|
72101
|
+
* the write it cut, and the model is asked again when the session
|
|
72102
|
+
* is back. Every request carries its round (`round` for a load,
|
|
72103
|
+
* `write` for a write) in `__md_command__`, so an answer of what
|
|
72104
|
+
* was cut is ignored instead of counted in the next one.
|
|
72099
72105
|
*
|
|
72100
72106
|
* Copyright (c) 2026, ArtGins.
|
|
72101
72107
|
* All Rights Reserved.
|
|
@@ -72128,6 +72134,7 @@ var PRIVATE_DATA$1 = {
|
|
|
72128
72134
|
model: null,
|
|
72129
72135
|
pending: 0,
|
|
72130
72136
|
load_error: "",
|
|
72137
|
+
load_round: 0,
|
|
72131
72138
|
treedb_id: "",
|
|
72132
72139
|
topic_name: "",
|
|
72133
72140
|
diagram: false,
|
|
@@ -72141,7 +72148,9 @@ var PRIVATE_DATA$1 = {
|
|
|
72141
72148
|
save_queue: null,
|
|
72142
72149
|
save_return: "",
|
|
72143
72150
|
save_wrote: false,
|
|
72151
|
+
write_tag: 0,
|
|
72144
72152
|
connected: false,
|
|
72153
|
+
reload_on_open: false,
|
|
72145
72154
|
pending_seg: null,
|
|
72146
72155
|
import_plan: null,
|
|
72147
72156
|
import_pane: null,
|
|
@@ -72312,6 +72321,7 @@ function request_model(gobj) {
|
|
|
72312
72321
|
};
|
|
72313
72322
|
priv.load_error = "";
|
|
72314
72323
|
priv.pending = 0;
|
|
72324
|
+
priv.load_round++;
|
|
72315
72325
|
gobj_change_state(gobj, "ST_LOADING");
|
|
72316
72326
|
show_notice(gobj, "loading the schemas");
|
|
72317
72327
|
for (let topic_name of [
|
|
@@ -72321,7 +72331,7 @@ function request_model(gobj) {
|
|
|
72321
72331
|
]) if (remote_command(gobj, "nodes", {
|
|
72322
72332
|
topic_name,
|
|
72323
72333
|
options: { list_dict: true }
|
|
72324
|
-
}) === 0) priv.pending++;
|
|
72334
|
+
}, { round: priv.load_round }) === 0) priv.pending++;
|
|
72325
72335
|
if (priv.pending === 0) {
|
|
72326
72336
|
priv.load_error = t("cannot reach the treedb");
|
|
72327
72337
|
gobj_change_state(gobj, "ST_IDLE");
|
|
@@ -72339,7 +72349,7 @@ function request_model(gobj) {
|
|
|
72339
72349
|
* answer says which topic it is about, since the command is
|
|
72340
72350
|
* `nodes` for all three.
|
|
72341
72351
|
***************************************************************/
|
|
72342
|
-
function remote_command(gobj, command, kw) {
|
|
72352
|
+
function remote_command(gobj, command, kw, tag) {
|
|
72343
72353
|
let remote = gobj_read_pointer_attr(gobj, "gobj_remote_yuno");
|
|
72344
72354
|
let treedb_name = gobj_read_str_attr(gobj, "treedb_name");
|
|
72345
72355
|
if (!remote) {
|
|
@@ -72350,11 +72360,11 @@ function remote_command(gobj, command, kw) {
|
|
|
72350
72360
|
service: treedb_name,
|
|
72351
72361
|
treedb_name
|
|
72352
72362
|
}, kw || {});
|
|
72353
|
-
full_kw.__md_command__ = {
|
|
72363
|
+
full_kw.__md_command__ = Object.assign({
|
|
72354
72364
|
purpose: PURPOSE,
|
|
72355
72365
|
topic_name: kw && kw.topic_name || "",
|
|
72356
72366
|
record_id: kw && kw.record_id || ""
|
|
72357
|
-
};
|
|
72367
|
+
}, tag || {});
|
|
72358
72368
|
delete full_kw.record_id;
|
|
72359
72369
|
let ret = gobj_command(remote, command, full_kw, gobj);
|
|
72360
72370
|
if (ret) {
|
|
@@ -74585,12 +74595,61 @@ function run_next_write(gobj) {
|
|
|
74585
74595
|
let write = priv.save_queue[0];
|
|
74586
74596
|
let command = write_command(write.op);
|
|
74587
74597
|
let options = write_options(write.op);
|
|
74598
|
+
priv.write_tag++;
|
|
74588
74599
|
if (remote_command(gobj, command, {
|
|
74589
74600
|
topic_name: write.topic_name,
|
|
74590
74601
|
record: write.record,
|
|
74591
74602
|
options,
|
|
74592
74603
|
record_id: write.record.id || ""
|
|
74593
|
-
}) < 0) return end_writes(gobj, t("cannot reach the treedb"));
|
|
74604
|
+
}, { write: priv.write_tag }) < 0) return end_writes(gobj, t("cannot reach the treedb"));
|
|
74605
|
+
return 0;
|
|
74606
|
+
}
|
|
74607
|
+
/***************************************************************
|
|
74608
|
+
* Does the transport say it is in session? The state the library
|
|
74609
|
+
* reads off every transport (C_IEVENT_CLI and the routing adapters
|
|
74610
|
+
* name it the same).
|
|
74611
|
+
***************************************************************/
|
|
74612
|
+
function transport_in_session(gobj) {
|
|
74613
|
+
let remote = gobj_read_pointer_attr(gobj, "gobj_remote_yuno");
|
|
74614
|
+
return !!remote && gobj_current_state(remote) === "ST_SESSION";
|
|
74615
|
+
}
|
|
74616
|
+
/***************************************************************
|
|
74617
|
+
* Is this answer of the load in flight, or of the write in
|
|
74618
|
+
* flight? Each request carries its round in `__md_command__`, and
|
|
74619
|
+
* one the drop abandoned (or a deadline settled) can still come
|
|
74620
|
+
* back: counted, it ended the next load with the old data or
|
|
74621
|
+
* moved the next write's queue.
|
|
74622
|
+
***************************************************************/
|
|
74623
|
+
function is_current_load(gobj, md) {
|
|
74624
|
+
return gobj_current_state(gobj) === "ST_LOADING" && kw_get_int(gobj, md, "round", 0, 0) === gobj.priv.load_round;
|
|
74625
|
+
}
|
|
74626
|
+
function is_current_write(gobj, md) {
|
|
74627
|
+
return gobj_current_state(gobj) === "ST_SAVING" && kw_get_int(gobj, md, "write", 0, 0) === gobj.priv.write_tag;
|
|
74628
|
+
}
|
|
74629
|
+
/***************************************************************
|
|
74630
|
+
* The transport dropped under a load or a write: its answers
|
|
74631
|
+
* will never come. The load goes back to the screen it replaced
|
|
74632
|
+
* (or to "not connected" when there was none), the write ends
|
|
74633
|
+
* with the drop said -- whether it landed is unknown -- and both
|
|
74634
|
+
* mark the model to be asked again when the session is back.
|
|
74635
|
+
* Bumping the round makes an answer of what was cut stale.
|
|
74636
|
+
***************************************************************/
|
|
74637
|
+
function transport_dropped(gobj) {
|
|
74638
|
+
let priv = gobj.priv;
|
|
74639
|
+
let state = gobj_current_state(gobj);
|
|
74640
|
+
priv.reload_on_open = true;
|
|
74641
|
+
if (state === "ST_SAVING") {
|
|
74642
|
+
priv.write_tag++;
|
|
74643
|
+
return end_writes(gobj, "the connection dropped during the write");
|
|
74644
|
+
}
|
|
74645
|
+
if (state === "ST_LOADING") {
|
|
74646
|
+
priv.load_round++;
|
|
74647
|
+
priv.pending = 0;
|
|
74648
|
+
priv.load_error = "";
|
|
74649
|
+
if (priv.model) return go(gobj, priv.treedb_id, priv.topic_name, priv.diagram, false);
|
|
74650
|
+
gobj_change_state(gobj, "ST_IDLE");
|
|
74651
|
+
render(gobj);
|
|
74652
|
+
}
|
|
74594
74653
|
return 0;
|
|
74595
74654
|
}
|
|
74596
74655
|
/***************************************************************
|
|
@@ -74745,8 +74804,15 @@ function ac_mt_command_answer(gobj, event, kw, src) {
|
|
|
74745
74804
|
let kw_command = kw_get_dict(gobj, __command__, "kw", {}, 0);
|
|
74746
74805
|
let md = is_object(kw_command.__md_command__) ? kw_command.__md_command__ : kw_command;
|
|
74747
74806
|
if (kw_get_str(gobj, md, "purpose", "", 0) !== PURPOSE) return 0;
|
|
74807
|
+
if (result < 0 && !transport_in_session(gobj) && (gobj_current_state(gobj) === "ST_LOADING" || gobj_current_state(gobj) === "ST_SAVING")) {
|
|
74808
|
+
if (command === "nodes" && is_current_load(gobj, md) || command !== "nodes" && is_current_write(gobj, md)) return transport_dropped(gobj);
|
|
74809
|
+
}
|
|
74748
74810
|
if (command === "nodes") {
|
|
74749
74811
|
let topic_name = kw_get_str(gobj, md, "topic_name", "", 0);
|
|
74812
|
+
if (!is_current_load(gobj, md)) {
|
|
74813
|
+
log_warning(`${gobj_short_name(gobj)}: 'nodes' of '${topic_name}' answered for a load that is over: ignored`);
|
|
74814
|
+
return 0;
|
|
74815
|
+
}
|
|
74750
74816
|
priv.pending--;
|
|
74751
74817
|
if (result < 0) priv.load_error = comment || t("cannot load the schemas");
|
|
74752
74818
|
else if (priv.records && Array.isArray(priv.records[topic_name])) priv.records[topic_name] = Array.isArray(data) ? data : [];
|
|
@@ -74764,6 +74830,10 @@ function ac_mt_command_answer(gobj, event, kw, src) {
|
|
|
74764
74830
|
}
|
|
74765
74831
|
if (command === "update-node" || command === "delete-node") {
|
|
74766
74832
|
let topic_name = kw_get_str(gobj, md, "topic_name", "", 0);
|
|
74833
|
+
if (!is_current_write(gobj, md)) {
|
|
74834
|
+
log_warning(`${gobj_short_name(gobj)}: '${command}' of '${topic_name}' answered for a write that is over (result ${result}): ignored`);
|
|
74835
|
+
return 0;
|
|
74836
|
+
}
|
|
74767
74837
|
if (result < 0) return end_writes(gobj, comment || t("the treedb refused the write"));
|
|
74768
74838
|
priv.save_wrote = true;
|
|
74769
74839
|
if (command === "delete-node") forget_record(gobj, topic_name, kw_get_str(gobj, md, "record_id", "", 0));
|
|
@@ -74823,14 +74893,28 @@ function ac_hide(gobj, event, kw, src) {
|
|
|
74823
74893
|
return 0;
|
|
74824
74894
|
}
|
|
74825
74895
|
/***************************************************************
|
|
74826
|
-
* The session came up or went down. The model stays
|
|
74827
|
-
*
|
|
74828
|
-
*
|
|
74896
|
+
* The session came up or went down. The model stays -- a schema
|
|
74897
|
+
* does not change under you -- unless the drop cut a load or a
|
|
74898
|
+
* write (transport_dropped()): then it is asked again on the way
|
|
74899
|
+
* back, as it is when a first load had no transport to run on.
|
|
74900
|
+
*
|
|
74901
|
+
* Nothing else ends a load or a write the drop cut: their answers
|
|
74902
|
+
* will never come, and waiting in ST_LOADING skipped the reload
|
|
74903
|
+
* itself (M-1 of the independent review of 7.25.4).
|
|
74829
74904
|
***************************************************************/
|
|
74830
74905
|
function ac_transport_state(gobj, event, kw, src) {
|
|
74831
74906
|
let priv = gobj.priv;
|
|
74907
|
+
let state = gobj_current_state(gobj);
|
|
74832
74908
|
priv.connected = !!(kw && kw.connected);
|
|
74833
|
-
if (priv.connected
|
|
74909
|
+
if (!priv.connected) {
|
|
74910
|
+
if (state === "ST_LOADING" || state === "ST_SAVING") return transport_dropped(gobj);
|
|
74911
|
+
render_toolbar(gobj);
|
|
74912
|
+
return 0;
|
|
74913
|
+
}
|
|
74914
|
+
if ((!priv.model || priv.reload_on_open) && state !== "ST_LOADING" && state !== "ST_SAVING") {
|
|
74915
|
+
priv.reload_on_open = false;
|
|
74916
|
+
return request_model(gobj);
|
|
74917
|
+
}
|
|
74834
74918
|
render_toolbar(gobj);
|
|
74835
74919
|
return 0;
|
|
74836
74920
|
}
|
package/package.json
CHANGED
|
@@ -70,6 +70,12 @@
|
|
|
70
70
|
* `update-node` / `delete-node` on the treedb service, and it may
|
|
71
71
|
* be a routing adapter two hops away (the agent console's
|
|
72
72
|
* C_AGENT_TREEDB_LINK) — this view cannot tell, and must not.
|
|
73
|
+
* A DROP of it (the host's EV_TRANSPORT_STATE, or a failure that
|
|
74
|
+
* arrives while the transport is out of session) ends the load or
|
|
75
|
+
* the write it cut, and the model is asked again when the session
|
|
76
|
+
* is back. Every request carries its round (`round` for a load,
|
|
77
|
+
* `write` for a write) in `__md_command__`, so an answer of what
|
|
78
|
+
* was cut is ignored instead of counted in the next one.
|
|
73
79
|
*
|
|
74
80
|
* Copyright (c) 2026, ArtGins.
|
|
75
81
|
* All Rights Reserved.
|
|
@@ -108,6 +114,7 @@ import {
|
|
|
108
114
|
clean_name,
|
|
109
115
|
msg_iev_get_stack,
|
|
110
116
|
kw_get_str,
|
|
117
|
+
kw_get_int,
|
|
111
118
|
kw_get_dict,
|
|
112
119
|
is_object,
|
|
113
120
|
is_array,
|
|
@@ -198,6 +205,7 @@ let PRIVATE_DATA = {
|
|
|
198
205
|
model: null, /* build_schema_model() of the above */
|
|
199
206
|
pending: 0, /* `nodes` requests still in flight */
|
|
200
207
|
load_error: "",
|
|
208
|
+
load_round: 0, /* which load an answer belongs to: echoed as `round` */
|
|
201
209
|
|
|
202
210
|
/* Where the operator is. The url carries it; these mirror it. */
|
|
203
211
|
treedb_id: "",
|
|
@@ -220,7 +228,9 @@ let PRIVATE_DATA = {
|
|
|
220
228
|
save_queue: null,
|
|
221
229
|
save_return: "",
|
|
222
230
|
save_wrote: false,
|
|
231
|
+
write_tag: 0, /* which write an answer belongs to: echoed as `write` */
|
|
223
232
|
connected: false,
|
|
233
|
+
reload_on_open: false, /* a drop cut a load or a write: ask the model again */
|
|
224
234
|
|
|
225
235
|
/* A position that arrived while a write was in flight */
|
|
226
236
|
pending_seg: null,
|
|
@@ -458,6 +468,7 @@ function request_model(gobj)
|
|
|
458
468
|
priv.records = {treedbs: [], topics: [], cols: []};
|
|
459
469
|
priv.load_error = "";
|
|
460
470
|
priv.pending = 0;
|
|
471
|
+
priv.load_round++;
|
|
461
472
|
gobj_change_state(gobj, "ST_LOADING");
|
|
462
473
|
show_notice(gobj, "loading the schemas");
|
|
463
474
|
|
|
@@ -465,7 +476,7 @@ function request_model(gobj)
|
|
|
465
476
|
if(remote_command(gobj, "nodes", {
|
|
466
477
|
topic_name: topic_name,
|
|
467
478
|
options: {list_dict: true}
|
|
468
|
-
}) === 0) {
|
|
479
|
+
}, {round: priv.load_round}) === 0) {
|
|
469
480
|
priv.pending++;
|
|
470
481
|
}
|
|
471
482
|
}
|
|
@@ -487,7 +498,7 @@ function request_model(gobj)
|
|
|
487
498
|
* answer says which topic it is about, since the command is
|
|
488
499
|
* `nodes` for all three.
|
|
489
500
|
***************************************************************/
|
|
490
|
-
function remote_command(gobj, command, kw)
|
|
501
|
+
function remote_command(gobj, command, kw, tag)
|
|
491
502
|
{
|
|
492
503
|
let remote = gobj_read_pointer_attr(gobj, "gobj_remote_yuno");
|
|
493
504
|
let treedb_name = gobj_read_str_attr(gobj, "treedb_name");
|
|
@@ -512,11 +523,11 @@ function remote_command(gobj, command, kw)
|
|
|
512
523
|
* whichever one is under this view — and `record_id` is here and
|
|
513
524
|
* NOT at the top level because a delete answers with nothing, and a
|
|
514
525
|
* parameter the treedb does not know has no business travelling. */
|
|
515
|
-
full_kw.__md_command__ = {
|
|
526
|
+
full_kw.__md_command__ = Object.assign({
|
|
516
527
|
purpose: PURPOSE,
|
|
517
528
|
topic_name: (kw && kw.topic_name) || "",
|
|
518
529
|
record_id: (kw && kw.record_id) || ""
|
|
519
|
-
};
|
|
530
|
+
}, tag || {});
|
|
520
531
|
delete full_kw.record_id;
|
|
521
532
|
|
|
522
533
|
let ret = gobj_command(remote, command, full_kw, gobj);
|
|
@@ -2362,17 +2373,80 @@ function run_next_write(gobj)
|
|
|
2362
2373
|
let command = write_command(write.op);
|
|
2363
2374
|
let options = write_options(write.op);
|
|
2364
2375
|
|
|
2376
|
+
priv.write_tag++;
|
|
2365
2377
|
if(remote_command(gobj, command, {
|
|
2366
2378
|
topic_name: write.topic_name,
|
|
2367
2379
|
record: write.record,
|
|
2368
2380
|
options: options,
|
|
2369
2381
|
record_id: write.record.id || ""
|
|
2370
|
-
}) < 0) {
|
|
2382
|
+
}, {write: priv.write_tag}) < 0) {
|
|
2371
2383
|
return end_writes(gobj, t("cannot reach the treedb"));
|
|
2372
2384
|
}
|
|
2373
2385
|
return 0;
|
|
2374
2386
|
}
|
|
2375
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
|
+
|
|
2376
2450
|
/***************************************************************
|
|
2377
2451
|
* The queue is done, or gave up. Either way the model is what
|
|
2378
2452
|
* the answers made it, so the screen is redrawn from it and the
|
|
@@ -2627,8 +2701,28 @@ function ac_mt_command_answer(gobj, event, kw, src)
|
|
|
2627
2701
|
return 0; /* another panel's answer on a shared transport */
|
|
2628
2702
|
}
|
|
2629
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
|
+
|
|
2630
2719
|
if(command === "nodes") {
|
|
2631
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
|
+
}
|
|
2632
2726
|
priv.pending--;
|
|
2633
2727
|
if(result < 0) {
|
|
2634
2728
|
priv.load_error = comment || t("cannot load the schemas");
|
|
@@ -2662,6 +2756,11 @@ function ac_mt_command_answer(gobj, event, kw, src)
|
|
|
2662
2756
|
|
|
2663
2757
|
if(command === "update-node" || command === "delete-node") {
|
|
2664
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
|
+
}
|
|
2665
2764
|
if(result < 0) {
|
|
2666
2765
|
return end_writes(gobj, comment || t("the treedb refused the write"));
|
|
2667
2766
|
}
|
|
@@ -2743,16 +2842,31 @@ function ac_hide(gobj, event, kw, src)
|
|
|
2743
2842
|
}
|
|
2744
2843
|
|
|
2745
2844
|
/***************************************************************
|
|
2746
|
-
* The session came up or went down. The model stays
|
|
2747
|
-
*
|
|
2748
|
-
*
|
|
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).
|
|
2749
2853
|
***************************************************************/
|
|
2750
2854
|
function ac_transport_state(gobj, event, kw, src)
|
|
2751
2855
|
{
|
|
2752
2856
|
let priv = gobj.priv;
|
|
2857
|
+
let state = gobj_current_state(gobj);
|
|
2753
2858
|
|
|
2754
2859
|
priv.connected = !!(kw && kw.connected);
|
|
2755
|
-
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;
|
|
2756
2870
|
return request_model(gobj);
|
|
2757
2871
|
}
|
|
2758
2872
|
render_toolbar(gobj);
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* schema_editor_transport.wiring.test.js
|
|
3
|
+
*
|
|
4
|
+
* C_YUI_SCHEMA_EDITOR when the transport drops (M-1 of the
|
|
5
|
+
* independent review of 7.25.4). A drop used to leave the editor
|
|
6
|
+
* where it was: in ST_SAVING with its body busy, or in ST_LOADING
|
|
7
|
+
* -- and the reconnect skipped the reload BECAUSE the state was
|
|
8
|
+
* ST_LOADING. Only a reload of the page got it out.
|
|
9
|
+
*
|
|
10
|
+
* Driven through the FSM on a document double, with a fake
|
|
11
|
+
* transport whose state is the one the library reads
|
|
12
|
+
* (`ST_SESSION` or not).
|
|
13
|
+
*
|
|
14
|
+
* Copyright (c) 2026, ArtGins.
|
|
15
|
+
* All Rights Reserved.
|
|
16
|
+
***********************************************************************/
|
|
17
|
+
import {describe, test, expect, beforeAll, beforeEach, vi} from "vitest";
|
|
18
|
+
import {install_dom_double} from "../test/dom_double.js";
|
|
19
|
+
|
|
20
|
+
install_dom_double();
|
|
21
|
+
|
|
22
|
+
/* What the editor SAYS is asserted on the call. */
|
|
23
|
+
const shown = [];
|
|
24
|
+
vi.mock("./shell_modals.js", () => ({
|
|
25
|
+
yui_shell_show_error: (shell, message) => {
|
|
26
|
+
shown.push(message);
|
|
27
|
+
return {close() {}};
|
|
28
|
+
},
|
|
29
|
+
yui_shell_show_info: () => ({close() {}}),
|
|
30
|
+
yui_shell_show_modal: () => ({close() {}}),
|
|
31
|
+
yui_shell_confirm_danger: () => ({close() {}}),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
const {
|
|
35
|
+
SDATA_END,
|
|
36
|
+
gclass_create,
|
|
37
|
+
gobj_start_up, gobj_create_yuno, gobj_create, gobj_create_service,
|
|
38
|
+
gobj_start, gobj_send_event, gobj_read_attr,
|
|
39
|
+
gobj_change_state, gobj_current_state,
|
|
40
|
+
set_log_callback,
|
|
41
|
+
} = await import("@yuneta/gobj-js");
|
|
42
|
+
const {register_c_yui_schema_editor} = await import("./c_yui_schema_editor.js");
|
|
43
|
+
|
|
44
|
+
const logged = [];
|
|
45
|
+
const commands = [];
|
|
46
|
+
|
|
47
|
+
const RECORDS = {
|
|
48
|
+
treedbs: [{id: "db", schema_version: 4}],
|
|
49
|
+
topics: [
|
|
50
|
+
{id: "db.users", value: "users", order: 1, pkey: "id", topic_version: 2,
|
|
51
|
+
treedbs: ["treedbs^db^topics"]},
|
|
52
|
+
],
|
|
53
|
+
cols: [
|
|
54
|
+
{id: "db.users.id", value: "id", order: 1, type: "string",
|
|
55
|
+
flag: ["persistent", "required"], topics: ["topics^db.users^cols"]},
|
|
56
|
+
],
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function remote_command_parser(gobj, command, kw, src)
|
|
60
|
+
{
|
|
61
|
+
commands.push({command, kw, src});
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let yuno = null;
|
|
66
|
+
|
|
67
|
+
beforeAll(() => {
|
|
68
|
+
gobj_start_up(null, null, null, null, null, null, null);
|
|
69
|
+
set_log_callback((level, msg) => {
|
|
70
|
+
logged.push({level: String(level), msg: String(msg)});
|
|
71
|
+
});
|
|
72
|
+
gclass_create("C_TEST_HOST", [], [["ST_IDLE", []]], {}, 0, [SDATA_END()], {}, 0, 0, 0, 0);
|
|
73
|
+
gclass_create(
|
|
74
|
+
"C_TEST_EDITOR_HOST",
|
|
75
|
+
[["EV_POSITION_CHANGED", 0], ["EV_RECORD_WRITTEN", 0], ["EV_SCHEMA_CHECKED", 0]],
|
|
76
|
+
[["ST_IDLE", [
|
|
77
|
+
["EV_POSITION_CHANGED", () => 0, null],
|
|
78
|
+
["EV_RECORD_WRITTEN", () => 0, null],
|
|
79
|
+
["EV_SCHEMA_CHECKED", () => 0, null]
|
|
80
|
+
]]],
|
|
81
|
+
{}, 0, [SDATA_END()], {}, 0, 0, 0, 0
|
|
82
|
+
);
|
|
83
|
+
/* The two states the library reads off a transport. */
|
|
84
|
+
gclass_create(
|
|
85
|
+
"C_TEST_REMOTE",
|
|
86
|
+
[],
|
|
87
|
+
[["ST_DISCONNECTED", []], ["ST_SESSION", []]],
|
|
88
|
+
{mt_command_parser: remote_command_parser},
|
|
89
|
+
0, [SDATA_END()], {}, 0, 0, 0, 0
|
|
90
|
+
);
|
|
91
|
+
register_c_yui_schema_editor();
|
|
92
|
+
yuno = gobj_create_yuno("transport_yuno", "C_TEST_HOST", {});
|
|
93
|
+
gobj_start(yuno);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
beforeEach(() => {
|
|
97
|
+
logged.length = 0;
|
|
98
|
+
commands.length = 0;
|
|
99
|
+
shown.length = 0;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
function take(command)
|
|
103
|
+
{
|
|
104
|
+
const out = commands.filter((c) => c.command === command);
|
|
105
|
+
for(const c of out) {
|
|
106
|
+
commands.splice(commands.indexOf(c), 1);
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* The answer, the way a transport gives it: the request's frame on top. */
|
|
112
|
+
function answer(editor, remote, request, result, data, comment)
|
|
113
|
+
{
|
|
114
|
+
gobj_send_event(editor, "EV_MT_COMMAND_ANSWER", {
|
|
115
|
+
result: result,
|
|
116
|
+
comment: comment || "",
|
|
117
|
+
data: data,
|
|
118
|
+
__md_iev__: {command_stack: [{command: request.command, kw: request.kw}]}
|
|
119
|
+
}, remote);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function answer_the_load(editor, remote)
|
|
123
|
+
{
|
|
124
|
+
const asked = take("nodes");
|
|
125
|
+
for(const c of asked) {
|
|
126
|
+
answer(editor, remote, c, 0, RECORDS[c.kw.topic_name]);
|
|
127
|
+
}
|
|
128
|
+
return asked.length;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function drop(editor, remote, host)
|
|
132
|
+
{
|
|
133
|
+
gobj_change_state(remote, "ST_DISCONNECTED");
|
|
134
|
+
gobj_send_event(editor, "EV_TRANSPORT_STATE", {connected: false}, host);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function reconnect(editor, remote, host)
|
|
138
|
+
{
|
|
139
|
+
gobj_change_state(remote, "ST_SESSION");
|
|
140
|
+
gobj_send_event(editor, "EV_TRANSPORT_STATE", {connected: true}, host);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function build(name, loaded)
|
|
144
|
+
{
|
|
145
|
+
const remote = gobj_create_service(`${name}_remote`, "C_TEST_REMOTE", {}, yuno);
|
|
146
|
+
gobj_change_state(remote, "ST_SESSION");
|
|
147
|
+
const host = gobj_create(`${name}_host`, "C_TEST_EDITOR_HOST", {}, yuno);
|
|
148
|
+
const editor = gobj_create(`${name}_editor`, "C_YUI_SCHEMA_EDITOR", {
|
|
149
|
+
gobj_remote_yuno: remote,
|
|
150
|
+
treedb_name: "treedb_system_schema",
|
|
151
|
+
base_route: "/schemas"
|
|
152
|
+
}, host);
|
|
153
|
+
gobj_start(editor);
|
|
154
|
+
gobj_send_event(editor, "EV_TRANSPORT_STATE", {connected: true}, host);
|
|
155
|
+
if(loaded) {
|
|
156
|
+
expect(answer_the_load(editor, remote)).toBe(3);
|
|
157
|
+
gobj_send_event(editor, "EV_SHOW", {subpath: "db/users"}, host);
|
|
158
|
+
expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
|
|
159
|
+
}
|
|
160
|
+
return {editor, remote, host};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/* A write in flight: a new column, saved from its form. */
|
|
164
|
+
function start_a_write(editor, host)
|
|
165
|
+
{
|
|
166
|
+
gobj_send_event(editor, "EV_SAVE_COLUMN", {
|
|
167
|
+
creating: true, topic: "users", values: {value: "name", type: "string"}
|
|
168
|
+
}, host);
|
|
169
|
+
expect(gobj_current_state(editor)).toBe("ST_SAVING");
|
|
170
|
+
const [write] = take("update-node");
|
|
171
|
+
expect(write).toBeTruthy();
|
|
172
|
+
return write;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function is_busy(editor)
|
|
176
|
+
{
|
|
177
|
+
const $c = gobj_read_attr(editor, "$container");
|
|
178
|
+
return $c.querySelector(".SCHEMA_BODY").classList.contains("SCHEMA_BUSY");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function errors()
|
|
182
|
+
{
|
|
183
|
+
return logged.filter((l) => l.level === "error").map((l) => l.msg);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
describe("a drop while LOADING", () => {
|
|
187
|
+
|
|
188
|
+
test("the editor leaves ST_LOADING, and the reconnect loads again", () => {
|
|
189
|
+
const {editor, remote, host} = build("l1", false);
|
|
190
|
+
expect(gobj_current_state(editor)).toBe("ST_LOADING");
|
|
191
|
+
expect(take("nodes").length).toBe(3); /* never answered */
|
|
192
|
+
|
|
193
|
+
drop(editor, remote, host);
|
|
194
|
+
expect(gobj_current_state(editor)).toBe("ST_IDLE");
|
|
195
|
+
|
|
196
|
+
reconnect(editor, remote, host);
|
|
197
|
+
expect(gobj_current_state(editor)).toBe("ST_LOADING");
|
|
198
|
+
expect(answer_the_load(editor, remote)).toBe(3);
|
|
199
|
+
expect(gobj_current_state(editor)).toBe("ST_TREEDBS");
|
|
200
|
+
expect(errors()).toEqual([]);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("an answer of the load the drop abandoned is not counted in the next one", () => {
|
|
204
|
+
const {editor, remote, host} = build("l2", false);
|
|
205
|
+
const old = take("nodes");
|
|
206
|
+
drop(editor, remote, host);
|
|
207
|
+
reconnect(editor, remote, host);
|
|
208
|
+
const fresh = take("nodes");
|
|
209
|
+
expect(fresh.length).toBe(3);
|
|
210
|
+
|
|
211
|
+
/* The three OLD answers arrive late: were they counted, the new
|
|
212
|
+
* round would end here with the old data and 3 answers owed. */
|
|
213
|
+
for(const c of old) {
|
|
214
|
+
answer(editor, remote, c, 0, []);
|
|
215
|
+
}
|
|
216
|
+
expect(gobj_current_state(editor)).toBe("ST_LOADING");
|
|
217
|
+
for(const c of fresh) {
|
|
218
|
+
answer(editor, remote, c, 0, RECORDS[c.kw.topic_name]);
|
|
219
|
+
}
|
|
220
|
+
expect(gobj_current_state(editor)).toBe("ST_TREEDBS");
|
|
221
|
+
expect(editor.priv.model.treedbs.length).toBe(1);
|
|
222
|
+
expect(errors()).toEqual([]);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe("a drop while SAVING", () => {
|
|
227
|
+
|
|
228
|
+
test("the write ends, said, the body answers again, and the reconnect reloads", () => {
|
|
229
|
+
const {editor, remote, host} = build("s1", true);
|
|
230
|
+
start_a_write(editor, host);
|
|
231
|
+
expect(is_busy(editor)).toBe(true);
|
|
232
|
+
|
|
233
|
+
drop(editor, remote, host);
|
|
234
|
+
expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
|
|
235
|
+
expect(is_busy(editor)).toBe(false);
|
|
236
|
+
expect(shown).toEqual(["the connection dropped during the write"]);
|
|
237
|
+
|
|
238
|
+
/* Whether the write landed is unknown: the model is asked again. */
|
|
239
|
+
reconnect(editor, remote, host);
|
|
240
|
+
expect(answer_the_load(editor, remote)).toBe(3);
|
|
241
|
+
expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
|
|
242
|
+
expect(errors()).toEqual([]);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("the transport's own failure arriving FIRST is read as the drop it is", () => {
|
|
246
|
+
/* A routing adapter settles what it had in flight when its session
|
|
247
|
+
* closes, and that answer can reach the editor before the host's
|
|
248
|
+
* EV_TRANSPORT_STATE. Read as a refusal, it ended the write and the
|
|
249
|
+
* reconnect did not reload a model the write may have changed. */
|
|
250
|
+
const {editor, remote, host} = build("s2", true);
|
|
251
|
+
const write = start_a_write(editor, host);
|
|
252
|
+
|
|
253
|
+
gobj_change_state(remote, "ST_DISCONNECTED");
|
|
254
|
+
answer(editor, remote, write, -1, null, "the connection dropped");
|
|
255
|
+
expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
|
|
256
|
+
expect(shown).toEqual(["the connection dropped during the write"]);
|
|
257
|
+
|
|
258
|
+
gobj_send_event(editor, "EV_TRANSPORT_STATE", {connected: false}, host);
|
|
259
|
+
expect(shown.length).toBe(1); /* said once */
|
|
260
|
+
|
|
261
|
+
reconnect(editor, remote, host);
|
|
262
|
+
expect(answer_the_load(editor, remote)).toBe(3);
|
|
263
|
+
expect(errors()).toEqual([]);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("the answer of the write the drop ended is not taken for the next one", () => {
|
|
267
|
+
const {editor, remote, host} = build("s3", true);
|
|
268
|
+
const write = start_a_write(editor, host);
|
|
269
|
+
drop(editor, remote, host);
|
|
270
|
+
reconnect(editor, remote, host);
|
|
271
|
+
answer_the_load(editor, remote);
|
|
272
|
+
|
|
273
|
+
const second = start_a_write(editor, host);
|
|
274
|
+
/* The first write's answer, late: it must not end the second. */
|
|
275
|
+
answer(editor, remote, write, 0, {id: "db.users.name", value: "name",
|
|
276
|
+
topics: ["topics^db.users^cols"]});
|
|
277
|
+
expect(gobj_current_state(editor)).toBe("ST_SAVING");
|
|
278
|
+
|
|
279
|
+
answer(editor, remote, second, 0, {id: "db.users.name", value: "name", order: 2,
|
|
280
|
+
type: "string", topics: ["topics^db.users^cols"]});
|
|
281
|
+
expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
|
|
282
|
+
expect(errors()).toEqual([]);
|
|
283
|
+
});
|
|
284
|
+
});
|