@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 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.
@@ -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
- delete in_flight[form_write_key(topic_name, form_write)];
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
- if (failed_topic) (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_REFRESH_TOPIC", { topic_name: failed_topic }, gobj);
25185
- settle_form_write(gobj.priv.form_writes, (0, _yuneta_gobj_js.kw_get_int)(gobj, kw_command, "form_write", 0, 0), failed_topic);
25186
- answer_form_write(gobj, get_gobj_formtable(gobj, failed_topic), (0, _yuneta_gobj_js.kw_get_int)(gobj, kw_command, "form_write", 0, 0), false);
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) abandon_form_writes(gobj.priv.form_writes).forEach((w) => {
25385
- (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`);
25386
- answer_form_write(gobj, get_gobj_formtable(gobj, w.topic_name), w.form_write, false);
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) {
@@ -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
- delete in_flight[form_write_key(topic_name, form_write)];
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
- if (failed_topic) gobj_send_event(gobj, "EV_REFRESH_TOPIC", { topic_name: failed_topic }, gobj);
25180
- settle_form_write(gobj.priv.form_writes, kw_get_int(gobj, kw_command, "form_write", 0, 0), failed_topic);
25181
- answer_form_write(gobj, get_gobj_formtable(gobj, failed_topic), kw_get_int(gobj, kw_command, "form_write", 0, 0), false);
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) abandon_form_writes(gobj.priv.form_writes).forEach((w) => {
25380
- log_warning(`${gobj_short_name(gobj)}: transport closed, the write ${w.form_write} of '${w.topic_name}' is lost`);
25381
- answer_form_write(gobj, get_gobj_formtable(gobj, w.topic_name), w.form_write, false);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.25.12",
3
+ "version": "7.25.14",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
@@ -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
- gobj_send_event(gobj, "EV_REFRESH_TOPIC",
2097
- {topic_name: failed_topic}, gobj);
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[form_write_key(topic_name, form_write)];
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
- [SDATA(data_type_t.DTP_STRING, "topic_name", 0, "", "topic"), SDATA_END()],
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[topic] = gobj_create(`${name}_form_${topic}`, "C_YUI_TREEDB_TOPIC_WITH_FORM",
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
+ });