@yuneta/gobj-ui 7.25.12 → 7.25.14
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 +25 -0
- package/dist/gobj-ui.cjs.js +47 -10
- package/dist/gobj-ui.es.js +47 -10
- package/package.json +1 -1
- package/src/c_yui_treedb_topics.js +66 -10
- package/src/form_writes_in_flight.js +11 -3
- package/src/treedb_topics_transport.wiring.test.js +187 -6
package/README.md
CHANGED
|
@@ -819,6 +819,31 @@ A write is known by its topic AND its serial (`<topic>^<form_write>`): each
|
|
|
819
819
|
topic's form counts its serials from 1, and keyed by the serial alone two
|
|
820
820
|
forms saving at once shared one entry.
|
|
821
821
|
|
|
822
|
+
**A drop is followed by a read of every open table** (7.25.13, 7.25.14). A
|
|
823
|
+
refused write reads its topic again at once, so a cell edited in place goes
|
|
824
|
+
back to what the store has. A write that fails while the transport is NOT in
|
|
825
|
+
session is the drop, not a refusal: a routing adapter (gui_agent's
|
|
826
|
+
`C_AGENT_TREEDB_LINK`) settles what it had in flight when its session closes.
|
|
827
|
+
The topic cannot be read then — the transport refuses the read (*"cannot route
|
|
828
|
+
'nodes' -- not in session"*) — so the view logs a warning and waits.
|
|
829
|
+
|
|
830
|
+
A drop also hides the node events published while it lasts: another writer's
|
|
831
|
+
create, update or delete reaches no table. So the disconnect edge marks the
|
|
832
|
+
view, and the first edge that finds the transport in session again reads
|
|
833
|
+
EVERY open table once — as the schema editor reloads its model on the
|
|
834
|
+
reconnect. An `EV_CONNECTION_STATE` "up" that comes before the view's own
|
|
835
|
+
transport is in session does not read; the read waits for an edge that finds
|
|
836
|
+
the transport in session. An "up" with no drop before it reads nothing. A form
|
|
837
|
+
is answered once, also when the edge answered it before the failure arrived.
|
|
838
|
+
|
|
839
|
+
```js
|
|
840
|
+
// Session up, tables `users` and `roles` open.
|
|
841
|
+
// Session drops -> writes in flight answered refused
|
|
842
|
+
// (another writer creates a user; its EV_TREEDB_NODE_CREATED is lost)
|
|
843
|
+
// EV_TRANSPORT_STATE {connected: true}, transport in session
|
|
844
|
+
// -> `nodes` of users and of roles, once each
|
|
845
|
+
```
|
|
846
|
+
|
|
822
847
|
**The pkey2 of an update goes back as the record had it** (7.25.5). It names
|
|
823
848
|
the instance the write is for, and a `time` pkey2 that crossed the form's
|
|
824
849
|
`datetime-local` came back without its seconds — another instance, or none.
|
package/dist/gobj-ui.cjs.js
CHANGED
|
@@ -23160,11 +23160,17 @@ function track_form_write(in_flight, form_write, topic_name) {
|
|
|
23160
23160
|
};
|
|
23161
23161
|
}
|
|
23162
23162
|
/************************************************************
|
|
23163
|
-
* A write was answered, one way or the other
|
|
23163
|
+
* A write was answered, one way or the other. True when it was
|
|
23164
|
+
* still in flight: false when something else answered it already
|
|
23165
|
+
* (the transport closing, abandon_form_writes), so its form must
|
|
23166
|
+
* not be answered twice.
|
|
23164
23167
|
************************************************************/
|
|
23165
23168
|
function settle_form_write(in_flight, form_write, topic_name) {
|
|
23166
|
-
if (!in_flight || !form_write) return;
|
|
23167
|
-
|
|
23169
|
+
if (!in_flight || !form_write) return false;
|
|
23170
|
+
const key = form_write_key(topic_name, form_write);
|
|
23171
|
+
if (!Object.prototype.hasOwnProperty.call(in_flight, key)) return false;
|
|
23172
|
+
delete in_flight[key];
|
|
23173
|
+
return true;
|
|
23168
23174
|
}
|
|
23169
23175
|
/************************************************************
|
|
23170
23176
|
* Nothing in flight can be answered any more: take them all,
|
|
@@ -23751,6 +23757,7 @@ var __gclass__$6 = null;
|
|
|
23751
23757
|
***************************************************************/
|
|
23752
23758
|
function mt_create$6(gobj) {
|
|
23753
23759
|
gobj.priv.form_writes = {};
|
|
23760
|
+
gobj.priv.reread_on_session = false;
|
|
23754
23761
|
gobj.priv.conn_shell = null;
|
|
23755
23762
|
build_ui$6(gobj);
|
|
23756
23763
|
let subscriber = (0, _yuneta_gobj_js.gobj_read_pointer_attr)(gobj, "subscriber");
|
|
@@ -25181,9 +25188,15 @@ function ac_mt_command_answer$2(gobj, event, kw, src) {
|
|
|
25181
25188
|
yui_shell_show_error(yui_shell_of(gobj), comment, { t: i18next.t });
|
|
25182
25189
|
if (command === "update-node" || command === "create-node") {
|
|
25183
25190
|
let failed_topic = (0, _yuneta_gobj_js.kw_get_str)(gobj, kw_command, "topic_name", "", 0);
|
|
25184
|
-
|
|
25185
|
-
|
|
25186
|
-
|
|
25191
|
+
let form_write = (0, _yuneta_gobj_js.kw_get_int)(gobj, kw_command, "form_write", 0, 0);
|
|
25192
|
+
if (failed_topic) {
|
|
25193
|
+
if (is_connected$1(gobj)) (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_REFRESH_TOPIC", { topic_name: failed_topic }, gobj);
|
|
25194
|
+
else {
|
|
25195
|
+
(0, _yuneta_gobj_js.log_warning)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: the write of '${failed_topic}' was cut by the drop: the topic is read again when the session is back`);
|
|
25196
|
+
gobj.priv.reread_on_session = true;
|
|
25197
|
+
}
|
|
25198
|
+
}
|
|
25199
|
+
if (settle_form_write(gobj.priv.form_writes, form_write, failed_topic)) answer_form_write(gobj, get_gobj_formtable(gobj, failed_topic), form_write, false);
|
|
25187
25200
|
}
|
|
25188
25201
|
}
|
|
25189
25202
|
return 0;
|
|
@@ -25381,13 +25394,37 @@ function ac_back_to_topics(gobj, event, kw, src) {
|
|
|
25381
25394
|
function ac_transport_state$2(gobj, event, kw, src) {
|
|
25382
25395
|
let connected = !!(kw && kw.connected);
|
|
25383
25396
|
refresh_toolbar_buttons(gobj, connected);
|
|
25384
|
-
if (!connected)
|
|
25385
|
-
|
|
25386
|
-
|
|
25387
|
-
|
|
25397
|
+
if (!connected) {
|
|
25398
|
+
gobj.priv.reread_on_session = true;
|
|
25399
|
+
abandon_form_writes(gobj.priv.form_writes).forEach((w) => {
|
|
25400
|
+
(0, _yuneta_gobj_js.log_warning)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: transport closed, the write ${w.form_write} of '${w.topic_name}' is lost`);
|
|
25401
|
+
answer_form_write(gobj, get_gobj_formtable(gobj, w.topic_name), w.form_write, false);
|
|
25402
|
+
});
|
|
25403
|
+
} else reread_open_topics(gobj);
|
|
25388
25404
|
return 0;
|
|
25389
25405
|
}
|
|
25390
25406
|
/************************************************************
|
|
25407
|
+
* The session is back after a drop: read every open table again,
|
|
25408
|
+
* as the schema editor reloads its model. A drop cuts the writes
|
|
25409
|
+
* in flight AND the node events published meanwhile, so any
|
|
25410
|
+
* table may be stale, not only the one a cut write left.
|
|
25411
|
+
*
|
|
25412
|
+
* Only when the TRANSPORT says so too -- the app's connection
|
|
25413
|
+
* (EV_CONNECTION_STATE) can report "up" before the transport of
|
|
25414
|
+
* this view is in session, and a read asked then is refused. The
|
|
25415
|
+
* read waits for an edge that finds the transport in session, and
|
|
25416
|
+
* is done once, though both edges report the same "up".
|
|
25417
|
+
************************************************************/
|
|
25418
|
+
function reread_open_topics(gobj) {
|
|
25419
|
+
let priv = gobj.priv;
|
|
25420
|
+
if (!priv.reread_on_session || !is_connected$1(gobj)) return;
|
|
25421
|
+
priv.reread_on_session = false;
|
|
25422
|
+
(0, _yuneta_gobj_js.gobj_match_children)(gobj, { __gclass_name__: "C_YUI_TREEDB_TOPIC_WITH_FORM" }).forEach((form) => {
|
|
25423
|
+
let topic_name = (0, _yuneta_gobj_js.gobj_read_attr)(form, "topic_name");
|
|
25424
|
+
if (topic_name) (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_REFRESH_TOPIC", { topic_name }, gobj);
|
|
25425
|
+
});
|
|
25426
|
+
}
|
|
25427
|
+
/************************************************************
|
|
25391
25428
|
* Parent (routing) inform us that we go hidden
|
|
25392
25429
|
************************************************************/
|
|
25393
25430
|
function ac_hide$4(gobj, event, kw, src) {
|
package/dist/gobj-ui.es.js
CHANGED
|
@@ -23155,11 +23155,17 @@ function track_form_write(in_flight, form_write, topic_name) {
|
|
|
23155
23155
|
};
|
|
23156
23156
|
}
|
|
23157
23157
|
/************************************************************
|
|
23158
|
-
* A write was answered, one way or the other
|
|
23158
|
+
* A write was answered, one way or the other. True when it was
|
|
23159
|
+
* still in flight: false when something else answered it already
|
|
23160
|
+
* (the transport closing, abandon_form_writes), so its form must
|
|
23161
|
+
* not be answered twice.
|
|
23159
23162
|
************************************************************/
|
|
23160
23163
|
function settle_form_write(in_flight, form_write, topic_name) {
|
|
23161
|
-
if (!in_flight || !form_write) return;
|
|
23162
|
-
|
|
23164
|
+
if (!in_flight || !form_write) return false;
|
|
23165
|
+
const key = form_write_key(topic_name, form_write);
|
|
23166
|
+
if (!Object.prototype.hasOwnProperty.call(in_flight, key)) return false;
|
|
23167
|
+
delete in_flight[key];
|
|
23168
|
+
return true;
|
|
23163
23169
|
}
|
|
23164
23170
|
/************************************************************
|
|
23165
23171
|
* Nothing in flight can be answered any more: take them all,
|
|
@@ -23746,6 +23752,7 @@ var __gclass__$6 = null;
|
|
|
23746
23752
|
***************************************************************/
|
|
23747
23753
|
function mt_create$6(gobj) {
|
|
23748
23754
|
gobj.priv.form_writes = {};
|
|
23755
|
+
gobj.priv.reread_on_session = false;
|
|
23749
23756
|
gobj.priv.conn_shell = null;
|
|
23750
23757
|
build_ui$6(gobj);
|
|
23751
23758
|
let subscriber = gobj_read_pointer_attr(gobj, "subscriber");
|
|
@@ -25176,9 +25183,15 @@ function ac_mt_command_answer$2(gobj, event, kw, src) {
|
|
|
25176
25183
|
yui_shell_show_error(yui_shell_of(gobj), comment, { t });
|
|
25177
25184
|
if (command === "update-node" || command === "create-node") {
|
|
25178
25185
|
let failed_topic = kw_get_str(gobj, kw_command, "topic_name", "", 0);
|
|
25179
|
-
|
|
25180
|
-
|
|
25181
|
-
|
|
25186
|
+
let form_write = kw_get_int(gobj, kw_command, "form_write", 0, 0);
|
|
25187
|
+
if (failed_topic) {
|
|
25188
|
+
if (is_connected$1(gobj)) gobj_send_event(gobj, "EV_REFRESH_TOPIC", { topic_name: failed_topic }, gobj);
|
|
25189
|
+
else {
|
|
25190
|
+
log_warning(`${gobj_short_name(gobj)}: the write of '${failed_topic}' was cut by the drop: the topic is read again when the session is back`);
|
|
25191
|
+
gobj.priv.reread_on_session = true;
|
|
25192
|
+
}
|
|
25193
|
+
}
|
|
25194
|
+
if (settle_form_write(gobj.priv.form_writes, form_write, failed_topic)) answer_form_write(gobj, get_gobj_formtable(gobj, failed_topic), form_write, false);
|
|
25182
25195
|
}
|
|
25183
25196
|
}
|
|
25184
25197
|
return 0;
|
|
@@ -25376,13 +25389,37 @@ function ac_back_to_topics(gobj, event, kw, src) {
|
|
|
25376
25389
|
function ac_transport_state$2(gobj, event, kw, src) {
|
|
25377
25390
|
let connected = !!(kw && kw.connected);
|
|
25378
25391
|
refresh_toolbar_buttons(gobj, connected);
|
|
25379
|
-
if (!connected)
|
|
25380
|
-
|
|
25381
|
-
|
|
25382
|
-
|
|
25392
|
+
if (!connected) {
|
|
25393
|
+
gobj.priv.reread_on_session = true;
|
|
25394
|
+
abandon_form_writes(gobj.priv.form_writes).forEach((w) => {
|
|
25395
|
+
log_warning(`${gobj_short_name(gobj)}: transport closed, the write ${w.form_write} of '${w.topic_name}' is lost`);
|
|
25396
|
+
answer_form_write(gobj, get_gobj_formtable(gobj, w.topic_name), w.form_write, false);
|
|
25397
|
+
});
|
|
25398
|
+
} else reread_open_topics(gobj);
|
|
25383
25399
|
return 0;
|
|
25384
25400
|
}
|
|
25385
25401
|
/************************************************************
|
|
25402
|
+
* The session is back after a drop: read every open table again,
|
|
25403
|
+
* as the schema editor reloads its model. A drop cuts the writes
|
|
25404
|
+
* in flight AND the node events published meanwhile, so any
|
|
25405
|
+
* table may be stale, not only the one a cut write left.
|
|
25406
|
+
*
|
|
25407
|
+
* Only when the TRANSPORT says so too -- the app's connection
|
|
25408
|
+
* (EV_CONNECTION_STATE) can report "up" before the transport of
|
|
25409
|
+
* this view is in session, and a read asked then is refused. The
|
|
25410
|
+
* read waits for an edge that finds the transport in session, and
|
|
25411
|
+
* is done once, though both edges report the same "up".
|
|
25412
|
+
************************************************************/
|
|
25413
|
+
function reread_open_topics(gobj) {
|
|
25414
|
+
let priv = gobj.priv;
|
|
25415
|
+
if (!priv.reread_on_session || !is_connected$1(gobj)) return;
|
|
25416
|
+
priv.reread_on_session = false;
|
|
25417
|
+
gobj_match_children(gobj, { __gclass_name__: "C_YUI_TREEDB_TOPIC_WITH_FORM" }).forEach((form) => {
|
|
25418
|
+
let topic_name = gobj_read_attr(form, "topic_name");
|
|
25419
|
+
if (topic_name) gobj_send_event(gobj, "EV_REFRESH_TOPIC", { topic_name }, gobj);
|
|
25420
|
+
});
|
|
25421
|
+
}
|
|
25422
|
+
/************************************************************
|
|
25386
25423
|
* Parent (routing) inform us that we go hidden
|
|
25387
25424
|
************************************************************/
|
|
25388
25425
|
function ac_hide$4(gobj, event, kw, src) {
|
package/package.json
CHANGED
|
@@ -160,6 +160,11 @@ function mt_create(gobj)
|
|
|
160
160
|
* and serial (form_writes_in_flight): answered refused when the
|
|
161
161
|
* transport closes under them. */
|
|
162
162
|
gobj.priv.form_writes = {};
|
|
163
|
+
/* The session dropped since the tables were read: every open table
|
|
164
|
+
* may be stale (a write the drop cut, node events published while
|
|
165
|
+
* nobody heard them). Read again when the session is back
|
|
166
|
+
* (reread_open_topics), never out of session. */
|
|
167
|
+
gobj.priv.reread_on_session = false;
|
|
163
168
|
gobj.priv.conn_shell = null; /* the shell whose EV_CONNECTION_STATE we hear */
|
|
164
169
|
|
|
165
170
|
build_ui(gobj);
|
|
@@ -2092,18 +2097,34 @@ function ac_mt_command_answer(gobj, event, kw, src)
|
|
|
2092
2097
|
* topic back to what the treedb actually has. */
|
|
2093
2098
|
if(command === "update-node" || command === "create-node") {
|
|
2094
2099
|
let failed_topic = kw_get_str(gobj, kw_command, "topic_name", "", 0);
|
|
2100
|
+
let form_write = kw_get_int(gobj, kw_command, "form_write", 0, 0);
|
|
2095
2101
|
if(failed_topic) {
|
|
2096
|
-
|
|
2097
|
-
|
|
2102
|
+
if(is_connected(gobj)) {
|
|
2103
|
+
gobj_send_event(gobj, "EV_REFRESH_TOPIC",
|
|
2104
|
+
{topic_name: failed_topic}, gobj);
|
|
2105
|
+
} else {
|
|
2106
|
+
/* A failure out of session is the DROP, not a
|
|
2107
|
+
* refusal: a routing adapter settles what it had
|
|
2108
|
+
* in flight when its session closes. The topic
|
|
2109
|
+
* cannot be read now -- asked, the transport
|
|
2110
|
+
* refuses it ("cannot route 'nodes' -- not in
|
|
2111
|
+
* session"). The reconnect reads it. */
|
|
2112
|
+
log_warning(`${gobj_short_name(gobj)}: the write of ` +
|
|
2113
|
+
`'${failed_topic}' was cut by the drop: the topic is ` +
|
|
2114
|
+
`read again when the session is back`);
|
|
2115
|
+
gobj.priv.reread_on_session = true;
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
/* Answered once: the transport closing may have answered
|
|
2119
|
+
* this form already (ac_transport_state). */
|
|
2120
|
+
if(settle_form_write(gobj.priv.form_writes, form_write, failed_topic)) {
|
|
2121
|
+
answer_form_write(
|
|
2122
|
+
gobj,
|
|
2123
|
+
get_gobj_formtable(gobj, failed_topic),
|
|
2124
|
+
form_write,
|
|
2125
|
+
false
|
|
2126
|
+
);
|
|
2098
2127
|
}
|
|
2099
|
-
settle_form_write(gobj.priv.form_writes,
|
|
2100
|
-
kw_get_int(gobj, kw_command, "form_write", 0, 0), failed_topic);
|
|
2101
|
-
answer_form_write(
|
|
2102
|
-
gobj,
|
|
2103
|
-
get_gobj_formtable(gobj, failed_topic),
|
|
2104
|
-
kw_get_int(gobj, kw_command, "form_write", 0, 0),
|
|
2105
|
-
false
|
|
2106
|
-
);
|
|
2107
2128
|
}
|
|
2108
2129
|
}
|
|
2109
2130
|
return 0;
|
|
@@ -2460,6 +2481,10 @@ function ac_transport_state(gobj, event, kw, src)
|
|
|
2460
2481
|
* was typed, and the person saves again when the session is back.
|
|
2461
2482
|
*/
|
|
2462
2483
|
if(!connected) {
|
|
2484
|
+
/* What happens in the store while the session is down reaches
|
|
2485
|
+
* no table: its node events are published to nobody. Every open
|
|
2486
|
+
* table is read again when the session is back. */
|
|
2487
|
+
gobj.priv.reread_on_session = true;
|
|
2463
2488
|
abandon_form_writes(gobj.priv.form_writes).forEach((w) => {
|
|
2464
2489
|
log_warning(`${gobj_short_name(gobj)}: transport closed, the write ${w.form_write} of '${w.topic_name}' is lost`);
|
|
2465
2490
|
answer_form_write(
|
|
@@ -2469,10 +2494,41 @@ function ac_transport_state(gobj, event, kw, src)
|
|
|
2469
2494
|
false
|
|
2470
2495
|
);
|
|
2471
2496
|
});
|
|
2497
|
+
} else {
|
|
2498
|
+
reread_open_topics(gobj);
|
|
2472
2499
|
}
|
|
2473
2500
|
return 0;
|
|
2474
2501
|
}
|
|
2475
2502
|
|
|
2503
|
+
/************************************************************
|
|
2504
|
+
* The session is back after a drop: read every open table again,
|
|
2505
|
+
* as the schema editor reloads its model. A drop cuts the writes
|
|
2506
|
+
* in flight AND the node events published meanwhile, so any
|
|
2507
|
+
* table may be stale, not only the one a cut write left.
|
|
2508
|
+
*
|
|
2509
|
+
* Only when the TRANSPORT says so too -- the app's connection
|
|
2510
|
+
* (EV_CONNECTION_STATE) can report "up" before the transport of
|
|
2511
|
+
* this view is in session, and a read asked then is refused. The
|
|
2512
|
+
* read waits for an edge that finds the transport in session, and
|
|
2513
|
+
* is done once, though both edges report the same "up".
|
|
2514
|
+
************************************************************/
|
|
2515
|
+
function reread_open_topics(gobj)
|
|
2516
|
+
{
|
|
2517
|
+
let priv = gobj.priv;
|
|
2518
|
+
if(!priv.reread_on_session || !is_connected(gobj)) {
|
|
2519
|
+
return;
|
|
2520
|
+
}
|
|
2521
|
+
priv.reread_on_session = false;
|
|
2522
|
+
gobj_match_children(gobj, {
|
|
2523
|
+
__gclass_name__: "C_YUI_TREEDB_TOPIC_WITH_FORM"
|
|
2524
|
+
}).forEach((form) => {
|
|
2525
|
+
let topic_name = gobj_read_attr(form, "topic_name");
|
|
2526
|
+
if(topic_name) {
|
|
2527
|
+
gobj_send_event(gobj, "EV_REFRESH_TOPIC", {topic_name: topic_name}, gobj);
|
|
2528
|
+
}
|
|
2529
|
+
});
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2476
2532
|
/************************************************************
|
|
2477
2533
|
* Parent (routing) inform us that we go hidden
|
|
2478
2534
|
************************************************************/
|
|
@@ -44,14 +44,22 @@ function track_form_write(in_flight, form_write, topic_name)
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/************************************************************
|
|
47
|
-
* A write was answered, one way or the other
|
|
47
|
+
* A write was answered, one way or the other. True when it was
|
|
48
|
+
* still in flight: false when something else answered it already
|
|
49
|
+
* (the transport closing, abandon_form_writes), so its form must
|
|
50
|
+
* not be answered twice.
|
|
48
51
|
************************************************************/
|
|
49
52
|
function settle_form_write(in_flight, form_write, topic_name)
|
|
50
53
|
{
|
|
51
54
|
if(!in_flight || !form_write) {
|
|
52
|
-
return;
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
const key = form_write_key(topic_name, form_write);
|
|
58
|
+
if(!Object.prototype.hasOwnProperty.call(in_flight, key)) {
|
|
59
|
+
return false;
|
|
53
60
|
}
|
|
54
|
-
delete in_flight[
|
|
61
|
+
delete in_flight[key];
|
|
62
|
+
return true;
|
|
55
63
|
}
|
|
56
64
|
|
|
57
65
|
/************************************************************
|
|
@@ -22,7 +22,7 @@ const {
|
|
|
22
22
|
gclass_create,
|
|
23
23
|
gobj_start_up, gobj_create_yuno, gobj_create, gobj_create_service,
|
|
24
24
|
gobj_start, gobj_send_event,
|
|
25
|
-
gobj_change_state,
|
|
25
|
+
gobj_change_state, gobj_current_state,
|
|
26
26
|
set_log_callback,
|
|
27
27
|
} = gobj_js;
|
|
28
28
|
const {register_c_yui_shell, yui_shell_set_connection_state} = await import("./c_yui_shell.js");
|
|
@@ -30,15 +30,21 @@ const {register_c_yui_treedb_topics} = await import("./c_yui_treedb_topics.js");
|
|
|
30
30
|
|
|
31
31
|
const logged = [];
|
|
32
32
|
const answers = []; /* what the fake forms were told */
|
|
33
|
+
const loaded = []; /* the rows each fake form was loaded with */
|
|
33
34
|
const commands = []; /* what the fake transport was asked */
|
|
34
35
|
|
|
35
36
|
/*
|
|
36
37
|
* The transport: answers nothing, like a C_IEVENT_CLI whose socket is
|
|
37
|
-
* about to drop. Its state is what `is_connected()` reads.
|
|
38
|
+
* about to drop. Its state is what `is_connected()` reads. Out of
|
|
39
|
+
* session it refuses what it is asked, as gui_agent's routing adapter
|
|
40
|
+
* (C_AGENT_TREEDB_LINK) does.
|
|
38
41
|
*/
|
|
39
42
|
function remote_command_parser(gobj, command, kw, src)
|
|
40
43
|
{
|
|
41
44
|
commands.push({command, kw});
|
|
45
|
+
if(gobj_current_state(gobj) !== "ST_SESSION") {
|
|
46
|
+
return `cannot route '${command}' -- not in session`;
|
|
47
|
+
}
|
|
42
48
|
return null;
|
|
43
49
|
}
|
|
44
50
|
|
|
@@ -52,6 +58,12 @@ function ac_form_answer(gobj, event, kw, src)
|
|
|
52
58
|
return 0;
|
|
53
59
|
}
|
|
54
60
|
|
|
61
|
+
function ac_form_load(gobj, event, kw, src)
|
|
62
|
+
{
|
|
63
|
+
loaded.push({topic: gobj.priv_topic, rows: kw});
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
55
67
|
let yuno = null;
|
|
56
68
|
let remote = null;
|
|
57
69
|
|
|
@@ -71,14 +83,19 @@ beforeAll(() => {
|
|
|
71
83
|
);
|
|
72
84
|
gclass_create(
|
|
73
85
|
"C_YUI_TREEDB_TOPIC_WITH_FORM",
|
|
74
|
-
[["EV_WRITE_DONE", 0], ["EV_WRITE_REFUSED", 0]],
|
|
86
|
+
[["EV_WRITE_DONE", 0], ["EV_WRITE_REFUSED", 0], ["EV_LOAD_NODES", 0]],
|
|
75
87
|
[["ST_IDLE", [
|
|
76
88
|
["EV_WRITE_DONE", ac_form_answer, null],
|
|
77
|
-
["EV_WRITE_REFUSED", ac_form_answer, null]
|
|
89
|
+
["EV_WRITE_REFUSED", ac_form_answer, null],
|
|
90
|
+
["EV_LOAD_NODES", ac_form_load, null]
|
|
78
91
|
]]],
|
|
79
92
|
{},
|
|
80
93
|
0,
|
|
81
|
-
[
|
|
94
|
+
[
|
|
95
|
+
SDATA(data_type_t.DTP_STRING, "topic_name", 0, "", "topic"),
|
|
96
|
+
SDATA(data_type_t.DTP_BOOLEAN, "with_remote_paging", 0, false, "pulls its pages"),
|
|
97
|
+
SDATA_END()
|
|
98
|
+
],
|
|
82
99
|
{}, 0, 0, 0, 0
|
|
83
100
|
);
|
|
84
101
|
/* The host of the view: it hears what a CHILD view publishes. */
|
|
@@ -106,6 +123,7 @@ beforeEach(() => {
|
|
|
106
123
|
logged.length = 0;
|
|
107
124
|
answers.length = 0;
|
|
108
125
|
commands.length = 0;
|
|
126
|
+
loaded.length = 0;
|
|
109
127
|
});
|
|
110
128
|
|
|
111
129
|
function build(name)
|
|
@@ -127,7 +145,9 @@ function build(name)
|
|
|
127
145
|
|
|
128
146
|
const forms = {};
|
|
129
147
|
for(const topic of ["users", "roles"]) {
|
|
130
|
-
forms
|
|
148
|
+
/* Named as the view names its forms: the answer of a `nodes`
|
|
149
|
+
* finds its table by that name. */
|
|
150
|
+
forms[topic] = gobj_create(`${name}_topics?${topic}`, "C_YUI_TREEDB_TOPIC_WITH_FORM",
|
|
131
151
|
{topic_name: topic}, topics);
|
|
132
152
|
forms[topic].priv_topic = topic;
|
|
133
153
|
}
|
|
@@ -226,3 +246,164 @@ describe("a Save in flight when the backend drops (M8)", () => {
|
|
|
226
246
|
]);
|
|
227
247
|
});
|
|
228
248
|
});
|
|
249
|
+
|
|
250
|
+
/*
|
|
251
|
+
* The answer the routing adapter gives a write it had in flight when
|
|
252
|
+
* its session closed: a failure, settled by the adapter itself.
|
|
253
|
+
*/
|
|
254
|
+
function failed_write(topic_name, form_write)
|
|
255
|
+
{
|
|
256
|
+
const md = {topic_name: topic_name, treedb_name: "treedb_test", record: {id: "x"}};
|
|
257
|
+
if(form_write) {
|
|
258
|
+
md.form_write = form_write;
|
|
259
|
+
}
|
|
260
|
+
return {result: -1, comment: "the session closed", data: null, __md_iev__: {
|
|
261
|
+
command_stack: [{command: "update-node", kw: md}]
|
|
262
|
+
}};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function nodes_asked()
|
|
266
|
+
{
|
|
267
|
+
return commands.filter((c) => c.command === "nodes").map((c) => c.kw.topic_name);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
describe("a write cut by the drop does not reload out of session", () => {
|
|
271
|
+
|
|
272
|
+
test("the failure lands before the edge: no reload now, one on the reconnect", () => {
|
|
273
|
+
const {shell, topics} = build("t6");
|
|
274
|
+
yui_shell_set_connection_state(shell, true);
|
|
275
|
+
|
|
276
|
+
/* A cell edited in place: it looks saved until the topic is read again. */
|
|
277
|
+
gobj_send_event(topics, "EV_UPDATE_FIELD",
|
|
278
|
+
{topic_name: "users", id: "x", field: "name", value: "typed"}, topics);
|
|
279
|
+
expect(commands.filter((c) => c.command === "update-node").length).toBe(1);
|
|
280
|
+
|
|
281
|
+
/* The adapter leaves ST_SESSION and settles what it had in flight. */
|
|
282
|
+
gobj_change_state(remote, "ST_DISCONNECTED");
|
|
283
|
+
gobj_send_event(topics, "EV_MT_COMMAND_ANSWER", failed_write("users", 0), remote);
|
|
284
|
+
|
|
285
|
+
expect(nodes_asked()).toEqual([]);
|
|
286
|
+
expect(errors().filter((m) => m.includes("not in session"))).toEqual([]);
|
|
287
|
+
|
|
288
|
+
gobj_send_event(topics, "EV_TRANSPORT_STATE", {connected: false}, topics);
|
|
289
|
+
expect(nodes_asked()).toEqual([]);
|
|
290
|
+
|
|
291
|
+
/* The reconnect reads every open table -- once, though two edges
|
|
292
|
+
* report it. */
|
|
293
|
+
gobj_change_state(remote, "ST_SESSION");
|
|
294
|
+
gobj_send_event(topics, "EV_TRANSPORT_STATE", {connected: true}, topics);
|
|
295
|
+
yui_shell_set_connection_state(shell, true);
|
|
296
|
+
expect(nodes_asked().sort()).toEqual(["roles", "users"]);
|
|
297
|
+
expect(errors()).toEqual([]);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("the edge lands first: the form is answered once, the reload waits", () => {
|
|
301
|
+
const {shell, topics, forms} = build("t7");
|
|
302
|
+
yui_shell_set_connection_state(shell, true);
|
|
303
|
+
save(topics, forms.users, 1);
|
|
304
|
+
|
|
305
|
+
gobj_change_state(remote, "ST_DISCONNECTED");
|
|
306
|
+
yui_shell_set_connection_state(shell, false);
|
|
307
|
+
gobj_send_event(topics, "EV_MT_COMMAND_ANSWER", failed_write("users", 1), remote);
|
|
308
|
+
|
|
309
|
+
expect(answers).toEqual([
|
|
310
|
+
{topic: "users", event: "EV_WRITE_REFUSED", form_write: 1},
|
|
311
|
+
]);
|
|
312
|
+
expect(nodes_asked()).toEqual([]);
|
|
313
|
+
|
|
314
|
+
gobj_change_state(remote, "ST_SESSION");
|
|
315
|
+
yui_shell_set_connection_state(shell, true);
|
|
316
|
+
expect(nodes_asked().sort()).toEqual(["roles", "users"]);
|
|
317
|
+
expect(errors()).toEqual([]);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test("an edge that says 'up' before the transport is: the reload still waits", () => {
|
|
321
|
+
const {shell, topics} = build("t8");
|
|
322
|
+
yui_shell_set_connection_state(shell, true);
|
|
323
|
+
gobj_send_event(topics, "EV_UPDATE_FIELD",
|
|
324
|
+
{topic_name: "roles", id: "x", field: "name", value: "typed"}, topics);
|
|
325
|
+
gobj_change_state(remote, "ST_DISCONNECTED");
|
|
326
|
+
gobj_send_event(topics, "EV_MT_COMMAND_ANSWER", failed_write("roles", 0), remote);
|
|
327
|
+
yui_shell_set_connection_state(shell, false);
|
|
328
|
+
|
|
329
|
+
/* The app's connection is back, the view's transport is not yet. */
|
|
330
|
+
yui_shell_set_connection_state(shell, true);
|
|
331
|
+
expect(nodes_asked()).toEqual([]);
|
|
332
|
+
|
|
333
|
+
gobj_change_state(remote, "ST_SESSION");
|
|
334
|
+
gobj_send_event(topics, "EV_TRANSPORT_STATE", {connected: true}, topics);
|
|
335
|
+
expect(nodes_asked().sort()).toEqual(["roles", "users"]);
|
|
336
|
+
expect(errors().filter((m) => m.includes("not in session"))).toEqual([]);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("a write refused IN session reloads its topic at once", () => {
|
|
340
|
+
const {shell, topics} = build("t9");
|
|
341
|
+
yui_shell_set_connection_state(shell, true);
|
|
342
|
+
gobj_send_event(topics, "EV_UPDATE_FIELD",
|
|
343
|
+
{topic_name: "users", id: "x", field: "name", value: "typed"}, topics);
|
|
344
|
+
gobj_send_event(topics, "EV_MT_COMMAND_ANSWER", failed_write("users", 0), remote);
|
|
345
|
+
expect(nodes_asked()).toEqual(["users"]);
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
/*
|
|
350
|
+
* The answer of a `nodes`, the way C_IEVENT_CLI delivers it.
|
|
351
|
+
*/
|
|
352
|
+
function nodes_answer_of(topic_name, rows)
|
|
353
|
+
{
|
|
354
|
+
return {result: 0, comment: "", data: rows, __md_iev__: {
|
|
355
|
+
command_stack: [{command: "nodes", kw: {topic_name: topic_name}}]
|
|
356
|
+
}};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
describe("what the drop hid is read again on the reconnect", () => {
|
|
360
|
+
|
|
361
|
+
test("a node created during the drop is in its table after the reconnect", () => {
|
|
362
|
+
const {shell, topics} = build("t10");
|
|
363
|
+
yui_shell_set_connection_state(shell, true);
|
|
364
|
+
|
|
365
|
+
/* No write in flight: the session just drops. While it is down,
|
|
366
|
+
* another writer creates a user; its EV_TREEDB_NODE_CREATED is
|
|
367
|
+
* published to nobody. */
|
|
368
|
+
gobj_change_state(remote, "ST_DISCONNECTED");
|
|
369
|
+
gobj_send_event(topics, "EV_TRANSPORT_STATE", {connected: false}, topics);
|
|
370
|
+
yui_shell_set_connection_state(shell, false);
|
|
371
|
+
expect(nodes_asked()).toEqual([]);
|
|
372
|
+
|
|
373
|
+
gobj_change_state(remote, "ST_SESSION");
|
|
374
|
+
gobj_send_event(topics, "EV_TRANSPORT_STATE", {connected: true}, topics);
|
|
375
|
+
yui_shell_set_connection_state(shell, true);
|
|
376
|
+
|
|
377
|
+
/* Every open table is read once, though two edges said "up". */
|
|
378
|
+
expect(nodes_asked().sort()).toEqual(["roles", "users"]);
|
|
379
|
+
|
|
380
|
+
gobj_send_event(topics, "EV_MT_COMMAND_ANSWER",
|
|
381
|
+
nodes_answer_of("users", [{id: "x"}, {id: "created_during_the_drop"}]), remote);
|
|
382
|
+
expect(loaded).toEqual([
|
|
383
|
+
{topic: "users", rows: [{id: "x"}, {id: "created_during_the_drop"}]}
|
|
384
|
+
]);
|
|
385
|
+
expect(errors()).toEqual([]);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test("an 'up' with no drop before it reads nothing", () => {
|
|
389
|
+
const {shell, topics} = build("t11");
|
|
390
|
+
yui_shell_set_connection_state(shell, true);
|
|
391
|
+
gobj_send_event(topics, "EV_TRANSPORT_STATE", {connected: true}, topics);
|
|
392
|
+
expect(nodes_asked()).toEqual([]);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
test("an edge that says 'up' before the transport is in session waits", () => {
|
|
396
|
+
const {shell, topics} = build("t12");
|
|
397
|
+
yui_shell_set_connection_state(shell, true);
|
|
398
|
+
gobj_change_state(remote, "ST_DISCONNECTED");
|
|
399
|
+
yui_shell_set_connection_state(shell, false);
|
|
400
|
+
|
|
401
|
+
yui_shell_set_connection_state(shell, true);
|
|
402
|
+
expect(nodes_asked()).toEqual([]);
|
|
403
|
+
expect(errors().filter((m) => m.includes("not in session"))).toEqual([]);
|
|
404
|
+
|
|
405
|
+
gobj_change_state(remote, "ST_SESSION");
|
|
406
|
+
gobj_send_event(topics, "EV_TRANSPORT_STATE", {connected: true}, topics);
|
|
407
|
+
expect(nodes_asked().sort()).toEqual(["roles", "users"]);
|
|
408
|
+
});
|
|
409
|
+
});
|