@yuneta/gobj-ui 7.25.12 → 7.25.13

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,26 @@ 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 write cut by the drop is not a refusal** (7.25.13). A refused write
823
+ reads its topic again at once, so a cell edited in place goes back to what the
824
+ store has. A write that fails while the transport is NOT in session is the
825
+ drop: a routing adapter (gui_agent's `C_AGENT_TREEDB_LINK`) settles what it
826
+ had in flight when its session closes. The topic cannot be read then — the
827
+ transport refuses the read (*"cannot route 'nodes' -- not in session"*) — so
828
+ the view logs a warning and owes the read. Writes in flight on the
829
+ disconnect edge are owed the same way. The first edge that finds the
830
+ transport in session again pays it: each owed topic whose table is open is
831
+ read once. An `EV_CONNECTION_STATE` "up" that comes before the view's own
832
+ transport is in session leaves the read owed. A form is answered once, also
833
+ when the edge answered it before the failure arrived.
834
+
835
+ ```js
836
+ // Session up: a cell edit goes out as update-node.
837
+ // Session drops; the adapter answers it {result: -1} -> no read, a warning
838
+ // EV_TRANSPORT_STATE {connected: true}, transport in session
839
+ // -> `nodes` of that topic, once
840
+ ```
841
+
822
842
  **The pkey2 of an update goes back as the record had it** (7.25.5). It names
823
843
  the instance the write is for, and a `time` pkey2 that crossed the form's
824
844
  `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.reload_on_session = {};
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.reload_on_session[failed_topic] = 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;
@@ -25383,11 +25396,31 @@ function ac_transport_state$2(gobj, event, kw, src) {
25383
25396
  refresh_toolbar_buttons(gobj, connected);
25384
25397
  if (!connected) abandon_form_writes(gobj.priv.form_writes).forEach((w) => {
25385
25398
  (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`);
25399
+ if (w.topic_name) gobj.priv.reload_on_session[w.topic_name] = true;
25386
25400
  answer_form_write(gobj, get_gobj_formtable(gobj, w.topic_name), w.form_write, false);
25387
25401
  });
25402
+ else reload_owed_topics(gobj);
25388
25403
  return 0;
25389
25404
  }
25390
25405
  /************************************************************
25406
+ * The session is back: read again the topics a drop left
25407
+ * unknown. Only when the TRANSPORT says so too -- the app's
25408
+ * connection (EV_CONNECTION_STATE) can report "up" before the
25409
+ * transport of this view is in session, and a read asked then is
25410
+ * refused. What is owed stays owed until an edge finds the
25411
+ * transport in session; each topic is read once.
25412
+ ************************************************************/
25413
+ function reload_owed_topics(gobj) {
25414
+ let priv = gobj.priv;
25415
+ if (!is_connected$1(gobj)) return;
25416
+ let topics = Object.keys(priv.reload_on_session);
25417
+ priv.reload_on_session = {};
25418
+ topics.forEach((topic_name) => {
25419
+ if (!get_gobj_formtable(gobj, topic_name)) return;
25420
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_REFRESH_TOPIC", { topic_name }, gobj);
25421
+ });
25422
+ }
25423
+ /************************************************************
25391
25424
  * Parent (routing) inform us that we go hidden
25392
25425
  ************************************************************/
25393
25426
  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.reload_on_session = {};
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.reload_on_session[failed_topic] = 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;
@@ -25378,11 +25391,31 @@ function ac_transport_state$2(gobj, event, kw, src) {
25378
25391
  refresh_toolbar_buttons(gobj, connected);
25379
25392
  if (!connected) abandon_form_writes(gobj.priv.form_writes).forEach((w) => {
25380
25393
  log_warning(`${gobj_short_name(gobj)}: transport closed, the write ${w.form_write} of '${w.topic_name}' is lost`);
25394
+ if (w.topic_name) gobj.priv.reload_on_session[w.topic_name] = true;
25381
25395
  answer_form_write(gobj, get_gobj_formtable(gobj, w.topic_name), w.form_write, false);
25382
25396
  });
25397
+ else reload_owed_topics(gobj);
25383
25398
  return 0;
25384
25399
  }
25385
25400
  /************************************************************
25401
+ * The session is back: read again the topics a drop left
25402
+ * unknown. Only when the TRANSPORT says so too -- the app's
25403
+ * connection (EV_CONNECTION_STATE) can report "up" before the
25404
+ * transport of this view is in session, and a read asked then is
25405
+ * refused. What is owed stays owed until an edge finds the
25406
+ * transport in session; each topic is read once.
25407
+ ************************************************************/
25408
+ function reload_owed_topics(gobj) {
25409
+ let priv = gobj.priv;
25410
+ if (!is_connected$1(gobj)) return;
25411
+ let topics = Object.keys(priv.reload_on_session);
25412
+ priv.reload_on_session = {};
25413
+ topics.forEach((topic_name) => {
25414
+ if (!get_gobj_formtable(gobj, topic_name)) return;
25415
+ gobj_send_event(gobj, "EV_REFRESH_TOPIC", { topic_name }, gobj);
25416
+ });
25417
+ }
25418
+ /************************************************************
25386
25419
  * Parent (routing) inform us that we go hidden
25387
25420
  ************************************************************/
25388
25421
  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.13",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
@@ -160,6 +160,10 @@ 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 topics a write cut by a DROP left unknown: the table may show
164
+ * what was typed and the store what it had. Read again when the
165
+ * session is back (reload_owed_topics), never out of session. */
166
+ gobj.priv.reload_on_session = {};
163
167
  gobj.priv.conn_shell = null; /* the shell whose EV_CONNECTION_STATE we hear */
164
168
 
165
169
  build_ui(gobj);
@@ -2092,18 +2096,34 @@ function ac_mt_command_answer(gobj, event, kw, src)
2092
2096
  * topic back to what the treedb actually has. */
2093
2097
  if(command === "update-node" || command === "create-node") {
2094
2098
  let failed_topic = kw_get_str(gobj, kw_command, "topic_name", "", 0);
2099
+ let form_write = kw_get_int(gobj, kw_command, "form_write", 0, 0);
2095
2100
  if(failed_topic) {
2096
- gobj_send_event(gobj, "EV_REFRESH_TOPIC",
2097
- {topic_name: failed_topic}, gobj);
2101
+ if(is_connected(gobj)) {
2102
+ gobj_send_event(gobj, "EV_REFRESH_TOPIC",
2103
+ {topic_name: failed_topic}, gobj);
2104
+ } else {
2105
+ /* A failure out of session is the DROP, not a
2106
+ * refusal: a routing adapter settles what it had
2107
+ * in flight when its session closes. The topic
2108
+ * cannot be read now -- asked, the transport
2109
+ * refuses it ("cannot route 'nodes' -- not in
2110
+ * session"). The reconnect reads it. */
2111
+ log_warning(`${gobj_short_name(gobj)}: the write of ` +
2112
+ `'${failed_topic}' was cut by the drop: the topic is ` +
2113
+ `read again when the session is back`);
2114
+ gobj.priv.reload_on_session[failed_topic] = true;
2115
+ }
2116
+ }
2117
+ /* Answered once: the transport closing may have answered
2118
+ * this form already (ac_transport_state). */
2119
+ if(settle_form_write(gobj.priv.form_writes, form_write, failed_topic)) {
2120
+ answer_form_write(
2121
+ gobj,
2122
+ get_gobj_formtable(gobj, failed_topic),
2123
+ form_write,
2124
+ false
2125
+ );
2098
2126
  }
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
2127
  }
2108
2128
  }
2109
2129
  return 0;
@@ -2462,6 +2482,11 @@ function ac_transport_state(gobj, event, kw, src)
2462
2482
  if(!connected) {
2463
2483
  abandon_form_writes(gobj.priv.form_writes).forEach((w) => {
2464
2484
  log_warning(`${gobj_short_name(gobj)}: transport closed, the write ${w.form_write} of '${w.topic_name}' is lost`);
2485
+ /* Whether it reached the store is unknown: read it again
2486
+ * when the session is back. */
2487
+ if(w.topic_name) {
2488
+ gobj.priv.reload_on_session[w.topic_name] = true;
2489
+ }
2465
2490
  answer_form_write(
2466
2491
  gobj,
2467
2492
  get_gobj_formtable(gobj, w.topic_name),
@@ -2469,10 +2494,36 @@ function ac_transport_state(gobj, event, kw, src)
2469
2494
  false
2470
2495
  );
2471
2496
  });
2497
+ } else {
2498
+ reload_owed_topics(gobj);
2472
2499
  }
2473
2500
  return 0;
2474
2501
  }
2475
2502
 
2503
+ /************************************************************
2504
+ * The session is back: read again the topics a drop left
2505
+ * unknown. Only when the TRANSPORT says so too -- the app's
2506
+ * connection (EV_CONNECTION_STATE) can report "up" before the
2507
+ * transport of this view is in session, and a read asked then is
2508
+ * refused. What is owed stays owed until an edge finds the
2509
+ * transport in session; each topic is read once.
2510
+ ************************************************************/
2511
+ function reload_owed_topics(gobj)
2512
+ {
2513
+ let priv = gobj.priv;
2514
+ if(!is_connected(gobj)) {
2515
+ return;
2516
+ }
2517
+ let topics = Object.keys(priv.reload_on_session);
2518
+ priv.reload_on_session = {};
2519
+ topics.forEach((topic_name) => {
2520
+ if(!get_gobj_formtable(gobj, topic_name)) {
2521
+ return; /* its table is closed: opening it reads it */
2522
+ }
2523
+ gobj_send_event(gobj, "EV_REFRESH_TOPIC", {topic_name: topic_name}, gobj);
2524
+ });
2525
+ }
2526
+
2476
2527
  /************************************************************
2477
2528
  * Parent (routing) inform us that we go hidden
2478
2529
  ************************************************************/
@@ -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");
@@ -34,11 +34,16 @@ const commands = []; /* what the fake transport was asked */
34
34
 
35
35
  /*
36
36
  * The transport: answers nothing, like a C_IEVENT_CLI whose socket is
37
- * about to drop. Its state is what `is_connected()` reads.
37
+ * about to drop. Its state is what `is_connected()` reads. Out of
38
+ * session it refuses what it is asked, as gui_agent's routing adapter
39
+ * (C_AGENT_TREEDB_LINK) does.
38
40
  */
39
41
  function remote_command_parser(gobj, command, kw, src)
40
42
  {
41
43
  commands.push({command, kw});
44
+ if(gobj_current_state(gobj) !== "ST_SESSION") {
45
+ return `cannot route '${command}' -- not in session`;
46
+ }
42
47
  return null;
43
48
  }
44
49
 
@@ -78,7 +83,11 @@ beforeAll(() => {
78
83
  ]]],
79
84
  {},
80
85
  0,
81
- [SDATA(data_type_t.DTP_STRING, "topic_name", 0, "", "topic"), SDATA_END()],
86
+ [
87
+ SDATA(data_type_t.DTP_STRING, "topic_name", 0, "", "topic"),
88
+ SDATA(data_type_t.DTP_BOOLEAN, "with_remote_paging", 0, false, "pulls its pages"),
89
+ SDATA_END()
90
+ ],
82
91
  {}, 0, 0, 0, 0
83
92
  );
84
93
  /* The host of the view: it hears what a CHILD view publishes. */
@@ -226,3 +235,107 @@ describe("a Save in flight when the backend drops (M8)", () => {
226
235
  ]);
227
236
  });
228
237
  });
238
+
239
+ /*
240
+ * The answer the routing adapter gives a write it had in flight when
241
+ * its session closed: a failure, settled by the adapter itself.
242
+ */
243
+ function failed_write(topic_name, form_write)
244
+ {
245
+ const md = {topic_name: topic_name, treedb_name: "treedb_test", record: {id: "x"}};
246
+ if(form_write) {
247
+ md.form_write = form_write;
248
+ }
249
+ return {result: -1, comment: "the session closed", data: null, __md_iev__: {
250
+ command_stack: [{command: "update-node", kw: md}]
251
+ }};
252
+ }
253
+
254
+ function nodes_asked()
255
+ {
256
+ return commands.filter((c) => c.command === "nodes").map((c) => c.kw.topic_name);
257
+ }
258
+
259
+ describe("a write cut by the drop does not reload out of session", () => {
260
+
261
+ test("the failure lands before the edge: no reload now, one on the reconnect", () => {
262
+ const {shell, topics} = build("t6");
263
+ yui_shell_set_connection_state(shell, true);
264
+
265
+ /* A cell edited in place: it looks saved until the topic is read again. */
266
+ gobj_send_event(topics, "EV_UPDATE_FIELD",
267
+ {topic_name: "users", id: "x", field: "name", value: "typed"}, topics);
268
+ expect(commands.filter((c) => c.command === "update-node").length).toBe(1);
269
+
270
+ /* The adapter leaves ST_SESSION and settles what it had in flight. */
271
+ gobj_change_state(remote, "ST_DISCONNECTED");
272
+ gobj_send_event(topics, "EV_MT_COMMAND_ANSWER", failed_write("users", 0), remote);
273
+
274
+ expect(nodes_asked()).toEqual([]);
275
+ expect(errors().filter((m) => m.includes("not in session"))).toEqual([]);
276
+
277
+ gobj_send_event(topics, "EV_TRANSPORT_STATE", {connected: false}, topics);
278
+ expect(nodes_asked()).toEqual([]);
279
+
280
+ /* The reconnect reloads what the drop left unknown -- once, though
281
+ * two edges report it. */
282
+ gobj_change_state(remote, "ST_SESSION");
283
+ gobj_send_event(topics, "EV_TRANSPORT_STATE", {connected: true}, topics);
284
+ yui_shell_set_connection_state(shell, false);
285
+ yui_shell_set_connection_state(shell, true);
286
+ expect(nodes_asked()).toEqual(["users"]);
287
+ expect(errors()).toEqual([]);
288
+ });
289
+
290
+ test("the edge lands first: the form is answered once, the reload waits", () => {
291
+ const {shell, topics, forms} = build("t7");
292
+ yui_shell_set_connection_state(shell, true);
293
+ save(topics, forms.users, 1);
294
+
295
+ gobj_change_state(remote, "ST_DISCONNECTED");
296
+ yui_shell_set_connection_state(shell, false);
297
+ gobj_send_event(topics, "EV_MT_COMMAND_ANSWER", failed_write("users", 1), remote);
298
+
299
+ expect(answers).toEqual([
300
+ {topic: "users", event: "EV_WRITE_REFUSED", form_write: 1},
301
+ ]);
302
+ expect(nodes_asked()).toEqual([]);
303
+
304
+ gobj_change_state(remote, "ST_SESSION");
305
+ yui_shell_set_connection_state(shell, true);
306
+ expect(nodes_asked()).toEqual(["users"]);
307
+ expect(errors()).toEqual([]);
308
+ });
309
+
310
+ test("an edge that says 'up' before the transport is: the reload still waits", () => {
311
+ const {shell, topics} = build("t8");
312
+ yui_shell_set_connection_state(shell, true);
313
+ gobj_send_event(topics, "EV_UPDATE_FIELD",
314
+ {topic_name: "roles", id: "x", field: "name", value: "typed"}, topics);
315
+ gobj_change_state(remote, "ST_DISCONNECTED");
316
+ gobj_send_event(topics, "EV_MT_COMMAND_ANSWER", failed_write("roles", 0), remote);
317
+ yui_shell_set_connection_state(shell, false);
318
+
319
+ /* The app's connection is back, the view's transport is not yet. */
320
+ yui_shell_set_connection_state(shell, true);
321
+ expect(nodes_asked()).toEqual([]);
322
+
323
+ gobj_change_state(remote, "ST_SESSION");
324
+ gobj_send_event(topics, "EV_TRANSPORT_STATE", {connected: true}, topics);
325
+ expect(nodes_asked()).toEqual(["roles"]);
326
+ expect(errors().filter((m) => m.includes("not in session"))).toEqual([]);
327
+ });
328
+
329
+ test("a write refused IN session reloads its topic at once, and only then", () => {
330
+ const {shell, topics} = build("t9");
331
+ yui_shell_set_connection_state(shell, true);
332
+ gobj_send_event(topics, "EV_UPDATE_FIELD",
333
+ {topic_name: "users", id: "x", field: "name", value: "typed"}, topics);
334
+ gobj_send_event(topics, "EV_MT_COMMAND_ANSWER", failed_write("users", 0), remote);
335
+ expect(nodes_asked()).toEqual(["users"]);
336
+
337
+ gobj_send_event(topics, "EV_TRANSPORT_STATE", {connected: false}, topics);
338
+ gobj_send_event(topics, "EV_TRANSPORT_STATE", {connected: true}, topics);
339
+ expect(nodes_asked()).toEqual(["users"]);
340
+ });
341
+ });