@yuneta/gobj-ui 7.25.11 → 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.
@@ -2134,6 +2154,11 @@ And what the toolbar offers, each answering a question the storage could not:
2134
2154
  answer. Escaping crosses two layers and the second is not JSON's:
2135
2155
  `helper_quote2doublequote()` rewrites *every* single quote before the parse,
2136
2156
  so a quote inside a value can only survive as `\u0027`.
2157
+ Its C / JSON switch is two named buttons, and a click on one is an event,
2158
+ `EV_EXPORT_VIEW` (`{pane: "c" | "json"}`), answered in every state: the
2159
+ texts were composed when the dialog opened, and the dialog outlives the
2160
+ screen and the loads under it (7.25.12; before, the switch ran in the DOM
2161
+ handler and the `machine` trace did not show it).
2137
2162
  - **import** — the writes that make the stored schema equal a pasted one, shown
2138
2163
  as a **plan** before it runs. Import is the one operation here that can delete
2139
2164
  a column, so what is confirmed is what runs.
@@ -2201,8 +2226,10 @@ else.
2201
2226
  column form sends every field it shows, so its Save after a reload wrote
2202
2227
  the OLD record over the newer one -- 7.25.8 told the operator to "try
2203
2228
  again", which did exactly that. The import plan is forgotten when a load
2204
- LEAVES (7.25.10: one that could not be sent keeps it), and a Yes that finds
2205
- it gone is said (`the import plan is gone: preview it again`). A load that
2229
+ LANDS, with the model it was computed on (7.25.12; from 7.25.9 to 7.25.11
2230
+ it went when the load LEFT, so a load that then failed left the dialog up
2231
+ with no plan and Import disabled). A load that fails or never leaves keeps
2232
+ it, and Import stays enabled. A Yes that finds it gone is said (`the import plan is gone: preview it again`). A load that
2206
2233
  failed or never left replaced nothing, and the dialog stays with what was
2207
2234
  typed. A confirmation is a shell modal and outlives a load: its Yes in
2208
2235
  `ST_EMPTY` or `ST_IDLE` is refused with the same words, not *"NOT
@@ -2225,7 +2252,12 @@ else.
2225
2252
  orphans -- is refused with
2226
2253
  `the schemas shown may be out of date: they are read again, try again when they are in`
2227
2254
  and the model is read again (keeping the draft chips); a move goes where it
2228
- was going and reads there. Not at once and not on a timer: a load refused
2255
+ was going and reads there -- the editor's own moves, and since 7.25.12 the
2256
+ host's too (`EV_SHOW` from a url or the browser's Back). **Any load that
2257
+ lands pays it** (7.25.12): the operator's Refresh, a late write's reload,
2258
+ the reconnect. Before, only the reload the editor asked for itself cleared
2259
+ it, so after a Refresh that landed the next edit was refused and read the
2260
+ store again. Not at once and not on a timer: a load refused
2229
2261
  on a deadline asked again at once is refused the same way, and one asked
2230
2262
  on a clock is polling. A Refresh asked while a load is in flight keeps the
2231
2263
  records of the model shown (7.25.10), not the half the first load got.
@@ -2235,7 +2267,9 @@ else.
2235
2267
  // the load failed in session: model kept, reload owed
2236
2268
  gobj_send_event(editor, "EV_EDIT_COLUMN", {col: "id"}, host); // refused, said; ST_LOADING
2237
2269
  // ... the load lands: the same click now opens the form
2238
- ``` A write answered with no record reloads the model and
2270
+ ```
2271
+
2272
+ A write answered with no record reloads the model and
2239
2273
  says what it did not send after it
2240
2274
  (`the treedb did not describe a write back: the writes after it were not sent`).
2241
2275
  - The toolbar offers Diagram, Check, Export, Import, New topic and New column
@@ -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) {
@@ -72220,7 +72253,8 @@ var PRIVATE_DATA$1 = {
72220
72253
  pending_seg: null,
72221
72254
  import_plan: null,
72222
72255
  import_pane: null,
72223
- import_run: null
72256
+ import_run: null,
72257
+ export_view: null
72224
72258
  };
72225
72259
  var __gclass__$1 = null;
72226
72260
  /******************************
@@ -72405,7 +72439,12 @@ function built_on_a_replaced_model(gobj, event, kw) {
72405
72439
  * export) -- is refused and said: it would be computed on what
72406
72440
  * the store may no longer hold, and the load that lands closes a
72407
72441
  * dialog built on the old model anyway (end_load()). A move is
72408
- * not refused (reload_after_move()): it goes, and reads there.
72442
+ * not refused (reload_after_move()): it goes, and reads there --
72443
+ * the editor's own moves and the host's alike (ac_show(): a url,
72444
+ * the browser's Back).
72445
+ *
72446
+ * Paid by ANY load that lands, whoever asked it
72447
+ * (ac_mt_command_answer()): the store has just been read whole.
72409
72448
  *
72410
72449
  * Not on a timer, and not at once: a load refused on a deadline
72411
72450
  * asked again at once is refused the same way, and one asked on a
@@ -72447,9 +72486,10 @@ function reload_after_move(gobj) {
72447
72486
  * left open is answered in ST_LOADING instead (ac_wait_for_the_load),
72448
72487
  * and a load that LANDS closes it (end_load()): it was built on the
72449
72488
  * model the load replaced, and its Save sends every field it shows.
72450
- * An import plan is forgotten here for the same reason: it is a list
72451
- * of writes computed against the model going away -- once the load
72452
- * has LEFT (one that could not be sent replaces nothing).
72489
+ * An import plan is forgotten for the same reason, and at the same
72490
+ * moment: when the load LANDS and the model it was computed on goes
72491
+ * (ac_mt_command_answer()). A load that cannot leave, or that fails,
72492
+ * replaces nothing, and the plan stays with the dialog showing it.
72453
72493
  *
72454
72494
  * `keep_written`: a load nobody ASKED for -- the reconnect, a write
72455
72495
  * that turned out done, the reload a refused load owed -- keeps what
@@ -72503,7 +72543,6 @@ function request_model(gobj, keep_written) {
72503
72543
  end_load(gobj);
72504
72544
  return -1;
72505
72545
  }
72506
- forget_import_plan(gobj);
72507
72546
  priv.pending = left;
72508
72547
  return 0;
72509
72548
  }
@@ -73663,6 +73702,7 @@ function close_dialog(gobj) {
73663
73702
  if (!priv.dialog) return;
73664
73703
  let dialog = priv.dialog;
73665
73704
  priv.dialog = null;
73705
+ priv.export_view = null;
73666
73706
  try {
73667
73707
  dialog.close();
73668
73708
  } catch (e) {
@@ -73681,6 +73721,7 @@ function open_dialog(gobj, $content, title, logical, title_prefix) {
73681
73721
  t: i18next.t,
73682
73722
  on_close: () => {
73683
73723
  priv.dialog = null;
73724
+ priv.export_view = null;
73684
73725
  }
73685
73726
  });
73686
73727
  (0, _yuneta_gobj_js.refresh_language)($content, i18next.t);
@@ -74409,17 +74450,43 @@ function open_export(gobj, treedb) {
74409
74450
  let $text = $content.querySelector(".SCHEMA_EXPORT_TEXT");
74410
74451
  for (let $tab of $content.querySelectorAll(".SCHEMA_EXPORT_TAB")) $tab.addEventListener("click", (evt) => {
74411
74452
  evt.stopPropagation();
74412
- for (let $other of $content.querySelectorAll(".SCHEMA_EXPORT_TAB")) {
74413
- $other.classList.toggle("selected_state", $other === $tab);
74414
- $other.setAttribute("aria-pressed", $other === $tab ? "true" : "false");
74415
- }
74416
- $text.value = $tab.dataset.pane === "c" ? c_text : json_text;
74453
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_EXPORT_VIEW", { pane: $tab.dataset.pane }, gobj);
74417
74454
  });
74418
74455
  $content.querySelector(".SCHEMA_EXPORT_COPY").addEventListener("click", (evt) => {
74419
74456
  evt.stopPropagation();
74420
74457
  copy_text(gobj, $text);
74421
74458
  });
74422
74459
  open_dialog(gobj, $content, "export", "SCHEMA_EXPORT_DIALOG", treedb.id);
74460
+ priv.export_view = {
74461
+ $content,
74462
+ texts: {
74463
+ c: c_text,
74464
+ json: json_text
74465
+ }
74466
+ };
74467
+ }
74468
+ /***************************************************************
74469
+ * Show one of the export's two texts, and say which one is shown
74470
+ * (`selected_state` + aria-pressed). Reached only through
74471
+ * EV_EXPORT_VIEW: a click is an action like any other.
74472
+ ***************************************************************/
74473
+ function show_export_view(gobj, pane) {
74474
+ let view = gobj.priv.export_view;
74475
+ if (!view) {
74476
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: export view '${pane}' asked with no export open`);
74477
+ return -1;
74478
+ }
74479
+ if (!Object.prototype.hasOwnProperty.call(view.texts, pane)) {
74480
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: the export has no view '${pane}'`);
74481
+ return -1;
74482
+ }
74483
+ for (let $tab of view.$content.querySelectorAll(".SCHEMA_EXPORT_TAB")) {
74484
+ let shown = $tab.dataset.pane === pane;
74485
+ $tab.classList.toggle("selected_state", shown);
74486
+ $tab.setAttribute("aria-pressed", shown ? "true" : "false");
74487
+ }
74488
+ view.$content.querySelector(".SCHEMA_EXPORT_TEXT").value = view.texts[pane];
74489
+ return 0;
74423
74490
  }
74424
74491
  /***************************************************************
74425
74492
  * Copy through the clipboard when the page is allowed one, and
@@ -74574,9 +74641,9 @@ function open_import(gobj, treedb) {
74574
74641
  }
74575
74642
  /***************************************************************
74576
74643
  * The plan is a list of writes computed against the model shown:
74577
- * a load replaces the model, so the plan goes with it and the
74578
- * dialog, if it stays up (a load that failed keeps the model),
74579
- * asks for a Preview again.
74644
+ * a load that LANDS replaces the model, so the plan goes with it
74645
+ * (and end_load() closes the dialog built on that model). A load
74646
+ * that fails or never leaves keeps the model, and the plan.
74580
74647
  ***************************************************************/
74581
74648
  function forget_import_plan(gobj) {
74582
74649
  let priv = gobj.priv;
@@ -75143,6 +75210,8 @@ function ac_mt_command_answer(gobj, event, kw, src) {
75143
75210
  priv.order_undo = {};
75144
75211
  priv.records_before = null;
75145
75212
  priv.model_gen++;
75213
+ priv.reload_on_open = false;
75214
+ forget_import_plan(gobj);
75146
75215
  build_model(gobj);
75147
75216
  start_measuring(gobj);
75148
75217
  return end_load(gobj);
@@ -75273,7 +75342,8 @@ function ac_show(gobj, event, kw, src) {
75273
75342
  priv.pending_seg = seg;
75274
75343
  return 0;
75275
75344
  }
75276
- return apply_seg(gobj, seg);
75345
+ apply_seg(gobj, seg);
75346
+ return reload_after_move(gobj);
75277
75347
  }
75278
75348
  /***************************************************************
75279
75349
  * Hidden by the host: drop the overlays. A dialog outlives the
@@ -75712,6 +75782,15 @@ function ac_export(gobj, event, kw, src) {
75712
75782
  open_export(gobj, treedb);
75713
75783
  return 0;
75714
75784
  }
75785
+ /***************************************************************
75786
+ * The export dialog's C / JSON switch. Its texts were composed
75787
+ * when it opened, so it reads nothing of the model and is
75788
+ * answered in every state: the dialog stays up through a load
75789
+ * that fails and through a move sent by the host.
75790
+ ***************************************************************/
75791
+ function ac_export_view(gobj, event, kw, src) {
75792
+ return show_export_view(gobj, kw && kw.pane);
75793
+ }
75715
75794
  function ac_import(gobj, event, kw, src) {
75716
75795
  let treedb = current_treedb(gobj);
75717
75796
  if (refuse_if_readonly(gobj, event)) return -1;
@@ -75895,6 +75974,11 @@ function create_gclass$1(gclass_name) {
75895
75974
  "EV_DRAFTS",
75896
75975
  ac_drafts,
75897
75976
  null
75977
+ ],
75978
+ [
75979
+ "EV_EXPORT_VIEW",
75980
+ ac_export_view,
75981
+ null
75898
75982
  ]
75899
75983
  ];
75900
75984
  const TREEDB_WIDE = [
@@ -76102,6 +76186,7 @@ function create_gclass$1(gclass_name) {
76102
76186
  ["EV_UNDO_ORDER", 0],
76103
76187
  ["EV_VALIDATE", 0],
76104
76188
  ["EV_EXPORT", 0],
76189
+ ["EV_EXPORT_VIEW", 0],
76105
76190
  ["EV_IMPORT", 0],
76106
76191
  ["EV_PREVIEW_IMPORT", 0],
76107
76192
  ["EV_APPLY_IMPORT", 0],
@@ -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) {
@@ -72215,7 +72248,8 @@ var PRIVATE_DATA$1 = {
72215
72248
  pending_seg: null,
72216
72249
  import_plan: null,
72217
72250
  import_pane: null,
72218
- import_run: null
72251
+ import_run: null,
72252
+ export_view: null
72219
72253
  };
72220
72254
  var __gclass__$1 = null;
72221
72255
  /******************************
@@ -72400,7 +72434,12 @@ function built_on_a_replaced_model(gobj, event, kw) {
72400
72434
  * export) -- is refused and said: it would be computed on what
72401
72435
  * the store may no longer hold, and the load that lands closes a
72402
72436
  * dialog built on the old model anyway (end_load()). A move is
72403
- * not refused (reload_after_move()): it goes, and reads there.
72437
+ * not refused (reload_after_move()): it goes, and reads there --
72438
+ * the editor's own moves and the host's alike (ac_show(): a url,
72439
+ * the browser's Back).
72440
+ *
72441
+ * Paid by ANY load that lands, whoever asked it
72442
+ * (ac_mt_command_answer()): the store has just been read whole.
72404
72443
  *
72405
72444
  * Not on a timer, and not at once: a load refused on a deadline
72406
72445
  * asked again at once is refused the same way, and one asked on a
@@ -72442,9 +72481,10 @@ function reload_after_move(gobj) {
72442
72481
  * left open is answered in ST_LOADING instead (ac_wait_for_the_load),
72443
72482
  * and a load that LANDS closes it (end_load()): it was built on the
72444
72483
  * model the load replaced, and its Save sends every field it shows.
72445
- * An import plan is forgotten here for the same reason: it is a list
72446
- * of writes computed against the model going away -- once the load
72447
- * has LEFT (one that could not be sent replaces nothing).
72484
+ * An import plan is forgotten for the same reason, and at the same
72485
+ * moment: when the load LANDS and the model it was computed on goes
72486
+ * (ac_mt_command_answer()). A load that cannot leave, or that fails,
72487
+ * replaces nothing, and the plan stays with the dialog showing it.
72448
72488
  *
72449
72489
  * `keep_written`: a load nobody ASKED for -- the reconnect, a write
72450
72490
  * that turned out done, the reload a refused load owed -- keeps what
@@ -72498,7 +72538,6 @@ function request_model(gobj, keep_written) {
72498
72538
  end_load(gobj);
72499
72539
  return -1;
72500
72540
  }
72501
- forget_import_plan(gobj);
72502
72541
  priv.pending = left;
72503
72542
  return 0;
72504
72543
  }
@@ -73658,6 +73697,7 @@ function close_dialog(gobj) {
73658
73697
  if (!priv.dialog) return;
73659
73698
  let dialog = priv.dialog;
73660
73699
  priv.dialog = null;
73700
+ priv.export_view = null;
73661
73701
  try {
73662
73702
  dialog.close();
73663
73703
  } catch (e) {
@@ -73676,6 +73716,7 @@ function open_dialog(gobj, $content, title, logical, title_prefix) {
73676
73716
  t,
73677
73717
  on_close: () => {
73678
73718
  priv.dialog = null;
73719
+ priv.export_view = null;
73679
73720
  }
73680
73721
  });
73681
73722
  refresh_language($content, t);
@@ -74404,17 +74445,43 @@ function open_export(gobj, treedb) {
74404
74445
  let $text = $content.querySelector(".SCHEMA_EXPORT_TEXT");
74405
74446
  for (let $tab of $content.querySelectorAll(".SCHEMA_EXPORT_TAB")) $tab.addEventListener("click", (evt) => {
74406
74447
  evt.stopPropagation();
74407
- for (let $other of $content.querySelectorAll(".SCHEMA_EXPORT_TAB")) {
74408
- $other.classList.toggle("selected_state", $other === $tab);
74409
- $other.setAttribute("aria-pressed", $other === $tab ? "true" : "false");
74410
- }
74411
- $text.value = $tab.dataset.pane === "c" ? c_text : json_text;
74448
+ gobj_send_event(gobj, "EV_EXPORT_VIEW", { pane: $tab.dataset.pane }, gobj);
74412
74449
  });
74413
74450
  $content.querySelector(".SCHEMA_EXPORT_COPY").addEventListener("click", (evt) => {
74414
74451
  evt.stopPropagation();
74415
74452
  copy_text(gobj, $text);
74416
74453
  });
74417
74454
  open_dialog(gobj, $content, "export", "SCHEMA_EXPORT_DIALOG", treedb.id);
74455
+ priv.export_view = {
74456
+ $content,
74457
+ texts: {
74458
+ c: c_text,
74459
+ json: json_text
74460
+ }
74461
+ };
74462
+ }
74463
+ /***************************************************************
74464
+ * Show one of the export's two texts, and say which one is shown
74465
+ * (`selected_state` + aria-pressed). Reached only through
74466
+ * EV_EXPORT_VIEW: a click is an action like any other.
74467
+ ***************************************************************/
74468
+ function show_export_view(gobj, pane) {
74469
+ let view = gobj.priv.export_view;
74470
+ if (!view) {
74471
+ log_error(`${gobj_short_name(gobj)}: export view '${pane}' asked with no export open`);
74472
+ return -1;
74473
+ }
74474
+ if (!Object.prototype.hasOwnProperty.call(view.texts, pane)) {
74475
+ log_error(`${gobj_short_name(gobj)}: the export has no view '${pane}'`);
74476
+ return -1;
74477
+ }
74478
+ for (let $tab of view.$content.querySelectorAll(".SCHEMA_EXPORT_TAB")) {
74479
+ let shown = $tab.dataset.pane === pane;
74480
+ $tab.classList.toggle("selected_state", shown);
74481
+ $tab.setAttribute("aria-pressed", shown ? "true" : "false");
74482
+ }
74483
+ view.$content.querySelector(".SCHEMA_EXPORT_TEXT").value = view.texts[pane];
74484
+ return 0;
74418
74485
  }
74419
74486
  /***************************************************************
74420
74487
  * Copy through the clipboard when the page is allowed one, and
@@ -74569,9 +74636,9 @@ function open_import(gobj, treedb) {
74569
74636
  }
74570
74637
  /***************************************************************
74571
74638
  * The plan is a list of writes computed against the model shown:
74572
- * a load replaces the model, so the plan goes with it and the
74573
- * dialog, if it stays up (a load that failed keeps the model),
74574
- * asks for a Preview again.
74639
+ * a load that LANDS replaces the model, so the plan goes with it
74640
+ * (and end_load() closes the dialog built on that model). A load
74641
+ * that fails or never leaves keeps the model, and the plan.
74575
74642
  ***************************************************************/
74576
74643
  function forget_import_plan(gobj) {
74577
74644
  let priv = gobj.priv;
@@ -75138,6 +75205,8 @@ function ac_mt_command_answer(gobj, event, kw, src) {
75138
75205
  priv.order_undo = {};
75139
75206
  priv.records_before = null;
75140
75207
  priv.model_gen++;
75208
+ priv.reload_on_open = false;
75209
+ forget_import_plan(gobj);
75141
75210
  build_model(gobj);
75142
75211
  start_measuring(gobj);
75143
75212
  return end_load(gobj);
@@ -75268,7 +75337,8 @@ function ac_show(gobj, event, kw, src) {
75268
75337
  priv.pending_seg = seg;
75269
75338
  return 0;
75270
75339
  }
75271
- return apply_seg(gobj, seg);
75340
+ apply_seg(gobj, seg);
75341
+ return reload_after_move(gobj);
75272
75342
  }
75273
75343
  /***************************************************************
75274
75344
  * Hidden by the host: drop the overlays. A dialog outlives the
@@ -75707,6 +75777,15 @@ function ac_export(gobj, event, kw, src) {
75707
75777
  open_export(gobj, treedb);
75708
75778
  return 0;
75709
75779
  }
75780
+ /***************************************************************
75781
+ * The export dialog's C / JSON switch. Its texts were composed
75782
+ * when it opened, so it reads nothing of the model and is
75783
+ * answered in every state: the dialog stays up through a load
75784
+ * that fails and through a move sent by the host.
75785
+ ***************************************************************/
75786
+ function ac_export_view(gobj, event, kw, src) {
75787
+ return show_export_view(gobj, kw && kw.pane);
75788
+ }
75710
75789
  function ac_import(gobj, event, kw, src) {
75711
75790
  let treedb = current_treedb(gobj);
75712
75791
  if (refuse_if_readonly(gobj, event)) return -1;
@@ -75890,6 +75969,11 @@ function create_gclass$1(gclass_name) {
75890
75969
  "EV_DRAFTS",
75891
75970
  ac_drafts,
75892
75971
  null
75972
+ ],
75973
+ [
75974
+ "EV_EXPORT_VIEW",
75975
+ ac_export_view,
75976
+ null
75893
75977
  ]
75894
75978
  ];
75895
75979
  const TREEDB_WIDE = [
@@ -76097,6 +76181,7 @@ function create_gclass$1(gclass_name) {
76097
76181
  ["EV_UNDO_ORDER", 0],
76098
76182
  ["EV_VALIDATE", 0],
76099
76183
  ["EV_EXPORT", 0],
76184
+ ["EV_EXPORT_VIEW", 0],
76100
76185
  ["EV_IMPORT", 0],
76101
76186
  ["EV_PREVIEW_IMPORT", 0],
76102
76187
  ["EV_APPLY_IMPORT", 0],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.25.11",
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",
@@ -249,6 +249,9 @@ let PRIVATE_DATA = {
249
249
  import_plan: null,
250
250
  import_pane: null,
251
251
  import_run: null,
252
+
253
+ /* The export dialog while it is up: its content and its two texts */
254
+ export_view: null,
252
255
  };
253
256
 
254
257
  let __gclass__ = null;
@@ -494,7 +497,12 @@ function built_on_a_replaced_model(gobj, event, kw)
494
497
  * export) -- is refused and said: it would be computed on what
495
498
  * the store may no longer hold, and the load that lands closes a
496
499
  * dialog built on the old model anyway (end_load()). A move is
497
- * not refused (reload_after_move()): it goes, and reads there.
500
+ * not refused (reload_after_move()): it goes, and reads there --
501
+ * the editor's own moves and the host's alike (ac_show(): a url,
502
+ * the browser's Back).
503
+ *
504
+ * Paid by ANY load that lands, whoever asked it
505
+ * (ac_mt_command_answer()): the store has just been read whole.
498
506
  *
499
507
  * Not on a timer, and not at once: a load refused on a deadline
500
508
  * asked again at once is refused the same way, and one asked on a
@@ -558,9 +566,10 @@ function reload_after_move(gobj)
558
566
  * left open is answered in ST_LOADING instead (ac_wait_for_the_load),
559
567
  * and a load that LANDS closes it (end_load()): it was built on the
560
568
  * model the load replaced, and its Save sends every field it shows.
561
- * An import plan is forgotten here for the same reason: it is a list
562
- * of writes computed against the model going away -- once the load
563
- * has LEFT (one that could not be sent replaces nothing).
569
+ * An import plan is forgotten for the same reason, and at the same
570
+ * moment: when the load LANDS and the model it was computed on goes
571
+ * (ac_mt_command_answer()). A load that cannot leave, or that fails,
572
+ * replaces nothing, and the plan stays with the dialog showing it.
564
573
  *
565
574
  * `keep_written`: a load nobody ASKED for -- the reconnect, a write
566
575
  * that turned out done, the reload a refused load owed -- keeps what
@@ -626,9 +635,6 @@ function request_model(gobj, keep_written)
626
635
  end_load(gobj);
627
636
  return -1;
628
637
  }
629
- /* Only a load that LEFT replaces the model the plan was computed
630
- * on; one that could not be sent keeps it, and the plan with it. */
631
- forget_import_plan(gobj);
632
638
  priv.pending = left;
633
639
  return 0;
634
640
  }
@@ -1739,6 +1745,7 @@ function close_dialog(gobj)
1739
1745
  }
1740
1746
  let dialog = priv.dialog;
1741
1747
  priv.dialog = null;
1748
+ priv.export_view = null;
1742
1749
  try {
1743
1750
  dialog.close();
1744
1751
  } catch(e) {
@@ -1760,6 +1767,7 @@ function open_dialog(gobj, $content, title, logical, title_prefix)
1760
1767
  t: t,
1761
1768
  on_close: () => {
1762
1769
  priv.dialog = null;
1770
+ priv.export_view = null;
1763
1771
  }
1764
1772
  });
1765
1773
  refresh_language($content, t);
@@ -2279,11 +2287,7 @@ function open_export(gobj, treedb)
2279
2287
  for(let $tab of $content.querySelectorAll(".SCHEMA_EXPORT_TAB")) {
2280
2288
  $tab.addEventListener("click", (evt) => {
2281
2289
  evt.stopPropagation();
2282
- for(let $other of $content.querySelectorAll(".SCHEMA_EXPORT_TAB")) {
2283
- $other.classList.toggle("selected_state", $other === $tab);
2284
- $other.setAttribute("aria-pressed", ($other === $tab)? "true": "false");
2285
- }
2286
- $text.value = ($tab.dataset.pane === "c") ? c_text : json_text;
2290
+ gobj_send_event(gobj, "EV_EXPORT_VIEW", {pane: $tab.dataset.pane}, gobj);
2287
2291
  });
2288
2292
  }
2289
2293
  $content.querySelector(".SCHEMA_EXPORT_COPY").addEventListener("click", (evt) => {
@@ -2292,6 +2296,35 @@ function open_export(gobj, treedb)
2292
2296
  });
2293
2297
 
2294
2298
  open_dialog(gobj, $content, "export", "SCHEMA_EXPORT_DIALOG", treedb.id);
2299
+ /* After open_dialog(): it closes the dialog before, and that forgets
2300
+ * the view of the one it closes. */
2301
+ priv.export_view = {$content: $content, texts: {c: c_text, json: json_text}};
2302
+ }
2303
+
2304
+ /***************************************************************
2305
+ * Show one of the export's two texts, and say which one is shown
2306
+ * (`selected_state` + aria-pressed). Reached only through
2307
+ * EV_EXPORT_VIEW: a click is an action like any other.
2308
+ ***************************************************************/
2309
+ function show_export_view(gobj, pane)
2310
+ {
2311
+ let view = gobj.priv.export_view;
2312
+
2313
+ if(!view) {
2314
+ log_error(`${gobj_short_name(gobj)}: export view '${pane}' asked with no export open`);
2315
+ return -1;
2316
+ }
2317
+ if(!Object.prototype.hasOwnProperty.call(view.texts, pane)) {
2318
+ log_error(`${gobj_short_name(gobj)}: the export has no view '${pane}'`);
2319
+ return -1;
2320
+ }
2321
+ for(let $tab of view.$content.querySelectorAll(".SCHEMA_EXPORT_TAB")) {
2322
+ let shown = ($tab.dataset.pane === pane);
2323
+ $tab.classList.toggle("selected_state", shown);
2324
+ $tab.setAttribute("aria-pressed", shown ? "true" : "false");
2325
+ }
2326
+ view.$content.querySelector(".SCHEMA_EXPORT_TEXT").value = view.texts[pane];
2327
+ return 0;
2295
2328
  }
2296
2329
 
2297
2330
  /***************************************************************
@@ -2384,9 +2417,9 @@ function open_import(gobj, treedb)
2384
2417
 
2385
2418
  /***************************************************************
2386
2419
  * The plan is a list of writes computed against the model shown:
2387
- * a load replaces the model, so the plan goes with it and the
2388
- * dialog, if it stays up (a load that failed keeps the model),
2389
- * asks for a Preview again.
2420
+ * a load that LANDS replaces the model, so the plan goes with it
2421
+ * (and end_load() closes the dialog built on that model). A load
2422
+ * that fails or never leaves keeps the model, and the plan.
2390
2423
  ***************************************************************/
2391
2424
  function forget_import_plan(gobj)
2392
2425
  {
@@ -3103,6 +3136,17 @@ function ac_mt_command_answer(gobj, event, kw, src)
3103
3136
  priv.order_undo = {};
3104
3137
  priv.records_before = null;
3105
3138
  priv.model_gen++;
3139
+ /* The store was read whole: whatever reload was owed is paid,
3140
+ * whoever asked for this one -- the operator's Refresh, a late
3141
+ * write, the reconnect. Left owed, the next edit was refused
3142
+ * and read the store AGAIN (sixth independent review). */
3143
+ priv.reload_on_open = false;
3144
+ /* The plan was a list of writes computed on the model this load
3145
+ * replaces. Forgotten HERE, where the model goes, and not when
3146
+ * the requests leave: a load that fails replaces nothing, and
3147
+ * forgetting early left its dialog up with no plan and Import
3148
+ * disabled (sixth independent review). */
3149
+ forget_import_plan(gobj);
3106
3150
  build_model(gobj);
3107
3151
  /* Same reason as order_undo above: this is the LOAD, and the
3108
3152
  * baseline is what a later "the version did not move" is measured
@@ -3297,7 +3341,12 @@ function ac_show(gobj, event, kw, src)
3297
3341
  priv.pending_seg = seg;
3298
3342
  return 0; /* applied when the write lands (end_writes) or the load (end_load) */
3299
3343
  }
3300
- return apply_seg(gobj, seg);
3344
+ /* A move like the editor's own (ac_back() and the rest): it goes,
3345
+ * and pays the reload a refused load owes there. Without it a url
3346
+ * or the browser's Back left it owed on a screen that asked for
3347
+ * nothing (sixth independent review). */
3348
+ apply_seg(gobj, seg);
3349
+ return reload_after_move(gobj);
3301
3350
  }
3302
3351
 
3303
3352
  /***************************************************************
@@ -3903,6 +3952,17 @@ function ac_export(gobj, event, kw, src)
3903
3952
  return 0;
3904
3953
  }
3905
3954
 
3955
+ /***************************************************************
3956
+ * The export dialog's C / JSON switch. Its texts were composed
3957
+ * when it opened, so it reads nothing of the model and is
3958
+ * answered in every state: the dialog stays up through a load
3959
+ * that fails and through a move sent by the host.
3960
+ ***************************************************************/
3961
+ function ac_export_view(gobj, event, kw, src)
3962
+ {
3963
+ return show_export_view(gobj, kw && kw.pane);
3964
+ }
3965
+
3906
3966
  function ac_import(gobj, event, kw, src)
3907
3967
  {
3908
3968
  let treedb = current_treedb(gobj);
@@ -3994,8 +4054,9 @@ function run_import(gobj)
3994
4054
  let priv = gobj.priv;
3995
4055
  let plan = priv.import_plan;
3996
4056
 
3997
- /* Not an ERROR: a load that left forgets the plan (request_model()),
3998
- * and the confirmation of it may still be up. The operator's Yes
4057
+ /* Not an ERROR: a load that lands forgets the plan
4058
+ * (ac_mt_command_answer()), and the confirmation of it may still be
4059
+ * up. The operator's Yes
3999
4060
  * is answered, not lost in the log. */
4000
4061
  if(!plan) {
4001
4062
  log_warning(`${gobj_short_name(gobj)}: the import plan is gone`);
@@ -4099,7 +4160,9 @@ function create_gclass(gclass_name)
4099
4160
  ["EV_TRANSPORT_STATE", ac_transport_state, null],
4100
4161
  ["EV_LANGUAGE_CHANGED", ac_language_changed, null],
4101
4162
  ["EV_REFRESH", ac_refresh, null],
4102
- ["EV_DRAFTS", ac_drafts, null]
4163
+ ["EV_DRAFTS", ac_drafts, null],
4164
+ /* The export dialog outlives the screen it was opened from. */
4165
+ ["EV_EXPORT_VIEW", ac_export_view, null]
4103
4166
  ];
4104
4167
 
4105
4168
  /* What a treedb is open for: reading it whole, and writing it
@@ -4215,6 +4278,7 @@ function create_gclass(gclass_name)
4215
4278
  ["EV_UNDO_ORDER", 0],
4216
4279
  ["EV_VALIDATE", 0],
4217
4280
  ["EV_EXPORT", 0],
4281
+ ["EV_EXPORT_VIEW", 0],
4218
4282
  ["EV_IMPORT", 0],
4219
4283
  ["EV_PREVIEW_IMPORT", 0],
4220
4284
  ["EV_APPLY_IMPORT", 0],
@@ -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
  /************************************************************
@@ -25,6 +25,14 @@
25
25
  * silence, and a toolbar gated on the state instead of on the
26
26
  * treedb being there.
27
27
  *
28
+ * The sixth independent review (same day) found where the owed
29
+ * reload still lingered: a load that LANDED by another road (the
30
+ * operator's Refresh, a late write) left it owed, so the next
31
+ * edit was refused and read the store again; a move sent by the
32
+ * host did not run it; a load that failed forgot the import plan
33
+ * of a dialog it left up; and the export's two views switched in
34
+ * a DOM handler, out of the machine.
35
+ *
28
36
  * Driven through the FSM on a document double, with a fake
29
37
  * transport whose state is the one the library reads.
30
38
  *
@@ -68,6 +76,7 @@ const {
68
76
  gobj_start_up, gobj_create_yuno, gobj_create, gobj_create_service,
69
77
  gobj_start, gobj_send_event, gobj_read_attr,
70
78
  gobj_change_state, gobj_current_state,
79
+ gobj_set_gobj_trace,
71
80
  set_log_callback,
72
81
  } = await import("@yuneta/gobj-js");
73
82
  const {register_c_yui_schema_editor} = await import("./c_yui_schema_editor.js");
@@ -575,11 +584,12 @@ describe("a dialog built on the model a reload replaced (fourth review)", () =>
575
584
  expect(editor.priv.import_plan.writes.length).toBeGreaterThan(0);
576
585
 
577
586
  gobj_send_event(editor, "EV_REFRESH", {}, host);
578
- expect(editor.priv.import_plan).toBe(null);
579
587
  answer_the_load_with(editor, remote, records_with((r) => {
580
588
  r.cols.push({id: "db.users.email", value: "email", order: 2, type: "integer",
581
589
  topics: ["topics^db.users^cols"]});
582
590
  }));
591
+ /* Forgotten when the load LANDS, not when it leaves (sixth review). */
592
+ expect(editor.priv.import_plan).toBe(null);
583
593
  expect(dialog.closed).toBe(true);
584
594
 
585
595
  dialog.$content.querySelector(".SCHEMA_IMPORT_RUN").disabled = false;
@@ -1035,3 +1045,179 @@ describe("the answers of the editor's confirmations are i18n keys (fifth review,
1035
1045
  .toEqual(["delete", "cancel"]);
1036
1046
  });
1037
1047
  });
1048
+
1049
+ describe("the owed reload, settled wherever a load lands (sixth review)", () => {
1050
+
1051
+ function refused_load(name, subpath)
1052
+ {
1053
+ const built = build(name, subpath);
1054
+ gobj_send_event(built.editor, "EV_REFRESH", {}, built.host);
1055
+ const asked = take("nodes");
1056
+ answer(built.editor, built.remote, asked[0], -1, null, "deadline");
1057
+ answer(built.editor, built.remote, asked[1], 0, RECORDS[asked[1].kw.topic_name]);
1058
+ answer(built.editor, built.remote, asked[2], 0, RECORDS[asked[2].kw.topic_name]);
1059
+ expect(built.editor.priv.reload_on_open).toBe(true);
1060
+ expect(shown).toEqual([KEPT]);
1061
+ return built;
1062
+ }
1063
+
1064
+ test("the operator's Refresh that lands pays it: the next edit opens its form", () => {
1065
+ const {editor, remote, host} = refused_load("q1", "db/users");
1066
+ gobj_send_event(editor, "EV_REFRESH", {}, host);
1067
+ expect(answer_the_load(editor, remote)).toBe(3);
1068
+ expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
1069
+ expect(editor.priv.reload_on_open).toBe(false);
1070
+
1071
+ const before = modals.length;
1072
+ gobj_send_event(editor, "EV_EDIT_COLUMN", {col: "id"}, host);
1073
+ expect(modals.length).toBe(before + 1);
1074
+ expect(take("nodes")).toEqual([]);
1075
+ expect(shown).toEqual([KEPT]);
1076
+ expect(errors()).toEqual([]);
1077
+ expect(not_defined()).toEqual([]);
1078
+ });
1079
+
1080
+ test("the reload of a late write that lands pays it too", () => {
1081
+ const {editor, remote, host} = build("q2", "db/users");
1082
+ const w = start_a_write(editor, host, "name");
1083
+ drop(editor, remote, host);
1084
+ reconnect(editor, remote, host);
1085
+ expect(answer_the_load(editor, remote)).toBe(3);
1086
+
1087
+ gobj_send_event(editor, "EV_REFRESH", {}, host);
1088
+ const asked = take("nodes");
1089
+ answer(editor, remote, asked[0], -1, null, "deadline");
1090
+ answer(editor, remote, asked[1], 0, RECORDS[asked[1].kw.topic_name]);
1091
+ answer(editor, remote, asked[2], 0, RECORDS[asked[2].kw.topic_name]);
1092
+ expect(editor.priv.reload_on_open).toBe(true);
1093
+
1094
+ /* The write given up answers DONE: the model is read again. */
1095
+ answer(editor, remote, w, 0, {id: "db.users.name", value: "name", type: "string",
1096
+ order: 2, topics: [{id: "db.users", topic_name: "topics", hook_name: "cols"}]});
1097
+ expect(gobj_current_state(editor)).toBe("ST_LOADING");
1098
+ expect(answer_the_load(editor, remote)).toBe(3);
1099
+ expect(editor.priv.reload_on_open).toBe(false);
1100
+
1101
+ const before = modals.length;
1102
+ gobj_send_event(editor, "EV_EDIT_COLUMN", {col: "id"}, host);
1103
+ expect(modals.length).toBe(before + 1);
1104
+ expect(take("nodes")).toEqual([]);
1105
+ expect(errors()).toEqual([]);
1106
+ });
1107
+
1108
+ test("a move sent by the HOST (a url, the browser's Back) runs it, as the editor's own moves do", () => {
1109
+ const {editor, remote, host} = refused_load("q3", "db/users");
1110
+ gobj_send_event(editor, "EV_SHOW", {subpath: "db"}, host);
1111
+ expect(gobj_current_state(editor)).toBe("ST_LOADING");
1112
+ expect(editor.priv.reload_on_open).toBe(false);
1113
+ expect(shown).toEqual([KEPT]); /* a move is not refused */
1114
+ expect(answer_the_load(editor, remote)).toBe(3);
1115
+ expect(gobj_current_state(editor)).toBe("ST_TOPICS");
1116
+ expect(errors()).toEqual([]);
1117
+ expect(not_defined()).toEqual([]);
1118
+ });
1119
+
1120
+ test("a host EV_SHOW with nothing owed asks for nothing", () => {
1121
+ const {editor, remote, host} = build("q4", "db/users");
1122
+ gobj_send_event(editor, "EV_SHOW", {subpath: "db"}, host);
1123
+ expect(gobj_current_state(editor)).toBe("ST_TOPICS");
1124
+ expect(take("nodes")).toEqual([]);
1125
+ });
1126
+ });
1127
+
1128
+ describe("the import plan outlives a load that fails (sixth review)", () => {
1129
+
1130
+ const IMPORT = JSON.stringify({id: "db", topics: [{id: "users", pkey: "id", cols: [
1131
+ {id: "id", type: "string", flag: ["persistent", "required"]},
1132
+ {id: "email", type: "string"}
1133
+ ]}]});
1134
+
1135
+ test("a load that LEFT and failed replaced nothing: the plan and its Import stay", () => {
1136
+ const {editor, remote, host} = build("m1", "db");
1137
+ gobj_send_event(editor, "EV_IMPORT", {}, host);
1138
+ const dialog = modals[modals.length - 1];
1139
+ gobj_send_event(editor, "EV_PREVIEW_IMPORT", {prune: false, text: IMPORT,
1140
+ model_gen: editor.priv.model_gen}, editor);
1141
+ const plan = editor.priv.import_plan;
1142
+ expect(plan.writes.length).toBeGreaterThan(0);
1143
+
1144
+ gobj_send_event(editor, "EV_REFRESH", {}, host);
1145
+ const asked = take("nodes");
1146
+ answer(editor, remote, asked[0], -1, null, "deadline");
1147
+ answer(editor, remote, asked[1], 0, RECORDS[asked[1].kw.topic_name]);
1148
+ answer(editor, remote, asked[2], 0, RECORDS[asked[2].kw.topic_name]);
1149
+
1150
+ expect(dialog.closed).toBe(false);
1151
+ expect(editor.priv.import_plan).toBe(plan);
1152
+ expect(dialog.$content.querySelector(".SCHEMA_IMPORT_RUN").disabled).toBe(false);
1153
+ expect(dialog.$content.querySelector(".SCHEMA_IMPORT_PLAN").childNodes.length)
1154
+ .toBeGreaterThan(0);
1155
+ expect(errors()).toEqual([]);
1156
+ });
1157
+
1158
+ test("a load that LANDS forgets it, and closes the dialog it was shown in", () => {
1159
+ const {editor, remote, host} = build("m2", "db");
1160
+ gobj_send_event(editor, "EV_IMPORT", {}, host);
1161
+ const dialog = modals[modals.length - 1];
1162
+ gobj_send_event(editor, "EV_PREVIEW_IMPORT", {prune: false, text: IMPORT,
1163
+ model_gen: editor.priv.model_gen}, editor);
1164
+ expect(editor.priv.import_plan).toBeTruthy();
1165
+
1166
+ gobj_send_event(editor, "EV_REFRESH", {}, host);
1167
+ expect(editor.priv.import_plan).toBeTruthy(); /* in the air: nothing replaced yet */
1168
+ expect(answer_the_load(editor, remote)).toBe(3);
1169
+ expect(editor.priv.import_plan).toBe(null);
1170
+ expect(dialog.closed).toBe(true);
1171
+ expect(shown).toEqual([STALE]);
1172
+ expect(errors()).toEqual([]);
1173
+ });
1174
+ });
1175
+
1176
+ describe("the export's two views go through the machine (sixth review)", () => {
1177
+
1178
+ test("a click on a view is an event, and the action switches the text", () => {
1179
+ const {editor, remote, host} = build("y1", "db");
1180
+ gobj_send_event(editor, "EV_EXPORT", {}, host);
1181
+ const $content = modals[modals.length - 1].$content;
1182
+ const $tabs = [...$content.querySelectorAll(".SCHEMA_EXPORT_TAB")];
1183
+ const $text = $content.querySelector(".SCHEMA_EXPORT_TEXT");
1184
+
1185
+ gobj_set_gobj_trace(editor, "machine", true);
1186
+ try {
1187
+ $tabs[1].click();
1188
+ } finally {
1189
+ gobj_set_gobj_trace(editor, "machine", false);
1190
+ }
1191
+ expect(logged.some((l) => /EV_EXPORT_VIEW/.test(l.msg))).toBe(true);
1192
+ expect($text.value.trim().startsWith("{")).toBe(true);
1193
+ expect($tabs.map(($b) => $b.getAttribute("aria-pressed"))).toEqual(["false", "true"]);
1194
+ expect($tabs.map(($b) => $b.classList.contains("selected_state"))).toEqual([false, true]);
1195
+
1196
+ gobj_send_event(editor, "EV_EXPORT_VIEW", {pane: "c"}, editor);
1197
+ expect($text.value.trim().startsWith("{")).toBe(false);
1198
+ expect($tabs.map(($b) => $b.getAttribute("aria-pressed"))).toEqual(["true", "false"]);
1199
+ expect(errors()).toEqual([]);
1200
+ expect(not_defined()).toEqual([]);
1201
+ });
1202
+
1203
+ test("answered while a load is in the air, where the dialog is still up", () => {
1204
+ const {editor, remote, host} = build("y2", "db");
1205
+ gobj_send_event(editor, "EV_EXPORT", {}, host);
1206
+ const $content = modals[modals.length - 1].$content;
1207
+ gobj_send_event(editor, "EV_REFRESH", {}, host);
1208
+ expect(gobj_current_state(editor)).toBe("ST_LOADING");
1209
+ $content.querySelectorAll(".SCHEMA_EXPORT_TAB")[1].click();
1210
+ expect($content.querySelector(".SCHEMA_EXPORT_TEXT").value.trim().startsWith("{")).toBe(true);
1211
+ expect(not_defined()).toEqual([]);
1212
+ expect(answer_the_load(editor, remote)).toBe(3);
1213
+ expect(errors()).toEqual([]);
1214
+ });
1215
+
1216
+ test("a view that is not one, or no export open, is an ERROR, not a guess", () => {
1217
+ const {editor, remote, host} = build("y3", "db");
1218
+ gobj_send_event(editor, "EV_EXPORT_VIEW", {pane: "c"}, editor);
1219
+ gobj_send_event(editor, "EV_EXPORT", {}, host);
1220
+ gobj_send_event(editor, "EV_EXPORT_VIEW", {pane: "yaml"}, editor);
1221
+ expect(errors().length).toBe(2);
1222
+ });
1223
+ });
@@ -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
+ });