@loadbare/app 0.20.0 → 0.22.0

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.
Files changed (61) hide show
  1. package/README.md +18 -0
  2. package/dist/build/assemble.d.ts.map +1 -1
  3. package/dist/build/assemble.js +8 -3
  4. package/dist/build/assemble.js.map +1 -1
  5. package/dist/build/build.d.ts +62 -0
  6. package/dist/build/build.d.ts.map +1 -0
  7. package/dist/build/build.js +148 -0
  8. package/dist/build/build.js.map +1 -0
  9. package/dist/build/cli.d.ts +6 -1
  10. package/dist/build/cli.d.ts.map +1 -1
  11. package/dist/build/cli.js +25 -78
  12. package/dist/build/cli.js.map +1 -1
  13. package/dist/build/declared.d.ts.map +1 -1
  14. package/dist/build/declared.js +3 -0
  15. package/dist/build/declared.js.map +1 -1
  16. package/dist/build/skills-cli.d.ts +2 -1
  17. package/dist/build/skills-cli.d.ts.map +1 -1
  18. package/dist/build/skills-cli.js +8 -1
  19. package/dist/build/skills-cli.js.map +1 -1
  20. package/dist/build/skills.d.ts +2 -0
  21. package/dist/build/skills.d.ts.map +1 -1
  22. package/dist/build/skills.js +2 -0
  23. package/dist/build/skills.js.map +1 -1
  24. package/dist/core/lb-constants.d.ts +14 -0
  25. package/dist/core/lb-constants.d.ts.map +1 -1
  26. package/dist/core/lb-constants.js +25 -0
  27. package/dist/core/lb-constants.js.map +1 -1
  28. package/dist/dev/lb-dev.d.ts +128 -0
  29. package/dist/dev/lb-dev.d.ts.map +1 -0
  30. package/dist/dev/lb-dev.js +289 -0
  31. package/dist/dev/lb-dev.js.map +1 -0
  32. package/dist/hub/lb-apply.d.ts +10 -2
  33. package/dist/hub/lb-apply.d.ts.map +1 -1
  34. package/dist/hub/lb-apply.js +8 -2
  35. package/dist/hub/lb-apply.js.map +1 -1
  36. package/dist/hub/lb-hub.browser.d.ts.map +1 -1
  37. package/dist/hub/lb-hub.browser.js +127 -19
  38. package/dist/hub/lb-hub.browser.js.map +1 -1
  39. package/docs/comparison.md +4 -3
  40. package/docs/reference/builder.md +12 -4
  41. package/docs/reference/chrome.md +52 -1
  42. package/docs/reference/data-binding.md +27 -12
  43. package/docs/reference/dev-server.md +171 -0
  44. package/docs/reference/names.md +14 -1
  45. package/docs/reference/overview.md +1 -0
  46. package/docs/reference/server.md +8 -3
  47. package/docs/reference/widgets.md +10 -277
  48. package/docs/testing.md +2 -1
  49. package/docs/tutorials/000-getting-started.md +1 -1
  50. package/docs/tutorials/040-displaying-data.md +1 -1
  51. package/docs/tutorials/090-using-widget-libraries.md +1 -1
  52. package/package.json +4 -2
  53. package/skills/loadbare-app/SKILL.md +30 -12
  54. package/skills/loadbare-app/references/builder.md +12 -4
  55. package/skills/loadbare-app/references/chrome.md +52 -1
  56. package/skills/loadbare-app/references/data-binding.md +27 -12
  57. package/skills/loadbare-app/references/dev-server.md +171 -0
  58. package/skills/loadbare-app/references/names.md +14 -1
  59. package/skills/loadbare-app/references/overview.md +1 -0
  60. package/skills/loadbare-app/references/server.md +8 -3
  61. package/skills/loadbare-app/references/widgets.md +10 -277
@@ -1,5 +1,5 @@
1
1
  /// <reference lib="dom" />
2
- import { ATTR_COLUMN, ATTR_COLUMN_VALUE, ATTR_KEY_VALUE, ATTR_PAGE, ATTR_PAGE_TITLE, ATTR_QUERY, ATTR_REQUEST, ATTR_REQUEST_ERROR, ATTR_REQUEST_PENDING, ATTR_REQUEST_REFUSED, ATTR_ROW_EDITING, ATTR_SELECT, ATTR_URL_LINK, ATTR_URL_PUSH, ATTR_URL_UNKNOWN, CHOICES_QUERY, COMMAND_EDIT, COMMAND_NEW, COMMAND_RELEASE, COMMAND_SORT, HUB_TAG_NAME, KIND_ROW, LB_DONE_EVENT_NAME, LB_ENDPOINT, LB_EVENT_NAME, LB_REFUSAL_STATUS, LB_REQUEST_TIMEOUT_MS, LB_COMMAND_PREFIX, LB_RESERVED_PREFIX, LIVE_ROW, REQUEST_ROW_DELETE, REQUEST_ROW_INSERT, REQUEST_ROW_UPDATE, URL_COLUMN_PAGE_LABEL, URL_COLUMN_PAGE_UNKNOWN, URL_COLUMN_PATH, URL_QUERY, } from "../core/lb-constants.js";
2
+ import { ATTR_COLUMN, ATTR_COLUMN_VALUE, ATTR_KEY_VALUE, ATTR_PAGE, ATTR_PAGE_TITLE, ATTR_QUERY, ATTR_REQUEST, ATTR_REQUEST_ERROR, ATTR_REQUEST_PENDING, ATTR_REQUEST_REFUSED, ATTR_ROW_EDITING, ATTR_SELECT, ATTR_URL_LINK, ATTR_URL_PUSH, ATTR_URL_UNKNOWN, CHOICES_QUERY, COMMAND_EDIT, COMMAND_NEW, COMMAND_RELEASE, COMMAND_SORT, HUB_COLUMN_AS_OF, HUB_COLUMN_ERROR, HUB_COLUMN_LOAD_STATE, HUB_COLUMN_PENDING, HUB_COLUMN_SERVER_STATE, HUB_ERROR_NETWORK, HUB_ERROR_TIMEOUT, HUB_LOAD_FAILED, HUB_LOAD_LOADED, HUB_LOAD_LOADING, HUB_QUERY, HUB_SERVER_FAILING, HUB_SERVER_REACHABLE, HUB_SERVER_UNREACHABLE, HUB_TAG_NAME, KIND_ROW, LB_DONE_EVENT_NAME, LB_ENDPOINT, LB_EVENT_NAME, LB_REFUSAL_STATUS, LB_REQUEST_TIMEOUT_MS, LB_COMMAND_PREFIX, LB_RESERVED_PREFIX, LIVE_ROW, REQUEST_ROW_DELETE, REQUEST_ROW_INSERT, REQUEST_ROW_UPDATE, URL_COLUMN_PAGE_LABEL, URL_COLUMN_PAGE_UNKNOWN, URL_COLUMN_PATH, URL_QUERY, } from "../core/lb-constants.js";
3
3
  import { isRowRequest, nextLocation, pageNameFor, } from "../core/lb-types.js";
4
4
  import { applyResponse, applyRow, blankRow, focusIn, gatherInto, select, forget, isControl, isFormAssociated, keptAnswer, resort, within, } from "./lb-apply.js";
5
5
  import { attachPlaced, detachRemoved, placed, requestDone, upgradeAbsent, } from "./lb-lifecycle.js";
@@ -143,6 +143,18 @@ class RefusedError extends Error {
143
143
  this.name = "RefusedError";
144
144
  }
145
145
  }
146
+ /**
147
+ * A round trip that failed, and why, in the words `lb-error` uses: the
148
+ * network, the deadline, or the HTTP status the server answered with.
149
+ */
150
+ class RoundTripError extends Error {
151
+ reason;
152
+ constructor(reason, message) {
153
+ super(message);
154
+ this.reason = reason;
155
+ this.name = "RoundTripError";
156
+ }
157
+ }
146
158
  const shown = [];
147
159
  function validates(el) {
148
160
  const v = el;
@@ -390,19 +402,28 @@ async function fetchItems(page, request) {
390
402
  const controller = new AbortController();
391
403
  const timeout = setTimeout(() => controller.abort(), LB_REQUEST_TIMEOUT_MS);
392
404
  try {
393
- // An empty body is how the hub asks for the page's queries.
394
- const res = await fetch(url, {
395
- method: "POST",
396
- headers: { "content-type": "application/json" },
397
- body: JSON.stringify(request ?? {}),
398
- signal: controller.signal,
399
- });
405
+ let res;
406
+ try {
407
+ // An empty body is how the hub asks for the page's queries.
408
+ res = await fetch(url, {
409
+ method: "POST",
410
+ headers: { "content-type": "application/json" },
411
+ body: JSON.stringify(request ?? {}),
412
+ signal: controller.signal,
413
+ });
414
+ }
415
+ catch (err) {
416
+ const reason = controller.signal.aborted
417
+ ? HUB_ERROR_TIMEOUT
418
+ : HUB_ERROR_NETWORK;
419
+ throw new RoundTripError(reason, `lb-hub: ${reason === HUB_ERROR_TIMEOUT ? "no answer in time" : String(err)} for ${url}`);
420
+ }
400
421
  if (res.status === LB_REFUSAL_STATUS) {
401
422
  const body = (await res.json().catch(() => ({})));
402
423
  throw new RefusedError(body.refusal ?? {});
403
424
  }
404
425
  if (!res.ok) {
405
- throw new Error(`lb-hub: server responded ${res.status} for ${url}`);
426
+ throw new RoundTripError(String(res.status), `lb-hub: server responded ${res.status} for ${url}`);
406
427
  }
407
428
  return (await res.json());
408
429
  }
@@ -485,6 +506,16 @@ function point(form, row, key, live) {
485
506
  * Let a pointed form go: empty it and drop its key. A form inside a
486
507
  * `<dialog>` closes it, and the dialog's `close` finds nothing left to do.
487
508
  */
509
+ /**
510
+ * Whether a form's controls show a row on record: one whose key is not
511
+ * empty, as a form over a `row` query does once the answer to its insert
512
+ * lands the new row on it.
513
+ */
514
+ function onRecord(form) {
515
+ const control = controlsOf(form)[0];
516
+ const query = control && queryOf(control);
517
+ return !!query && !!keyFrom(control, query);
518
+ }
488
519
  function release(form) {
489
520
  if (!pointed.has(form))
490
521
  return;
@@ -544,6 +575,12 @@ class LbHub extends HTMLElement {
544
575
  * lands, so each custom element in it is filled before it is attached.
545
576
  */
546
577
  unplaced = [];
578
+ /** The hub's own state, as the query `lb-hub` serves it. */
579
+ trips = 0;
580
+ loadState = HUB_LOAD_LOADING;
581
+ serverState = "";
582
+ error = "";
583
+ asOf = "";
547
584
  connectedCallback() {
548
585
  this.main = this.querySelector("main");
549
586
  // Completed in the capture phase, on the way down, so every ancestor
@@ -750,6 +787,11 @@ class LbHub extends HTMLElement {
750
787
  withdraw(refused);
751
788
  }
752
789
  const at = here();
790
+ // A form pointed at a live row finishes an edit begun in that row, so
791
+ // its request is sent from inside the row's list. The row is the one it
792
+ // was pointed at when sent.
793
+ const saving = savedFrom.get(request);
794
+ const within = (saving && pointed.get(saving)) || origin;
753
795
  const items = await this.roundTrip(this.page, request, origin);
754
796
  if (items instanceof RefusedError)
755
797
  this.refused(request, items, origin);
@@ -765,7 +807,7 @@ class LbHub extends HTMLElement {
765
807
  const load = items.filter((item) => item !== url);
766
808
  landed = load;
767
809
  if (url === undefined) {
768
- applyResponse(this, load, { requested: true, origin });
810
+ applyResponse(this, load, { requested: true, origin: within });
769
811
  }
770
812
  else
771
813
  landed = await this.follow(url, load, origin);
@@ -775,12 +817,14 @@ class LbHub extends HTMLElement {
775
817
  for (const box of selectorsFor(this, request.query))
776
818
  select(box, false);
777
819
  }
778
- // A pointed form has done what it was pointed for, and lets go.
779
- const saved = savedFrom.get(request);
780
- if (saved)
781
- release(saved);
782
- else
783
- insertedFrom.get(request)?.reset();
820
+ // A pointed form has done what it was pointed for, and lets go. A form
821
+ // an insert was gathered from starts over, unless the answer landed the
822
+ // new row on it.
823
+ const inserted = insertedFrom.get(request);
824
+ if (saving)
825
+ release(saving);
826
+ else if (inserted && !onRecord(inserted))
827
+ inserted.reset();
784
828
  done(origin, request, landed);
785
829
  }
786
830
  /**
@@ -813,26 +857,84 @@ class LbHub extends HTMLElement {
813
857
  * flight, and `lb-request-error` when it fails. The error on failure.
814
858
  */
815
859
  async roundTrip(page, request, origin) {
860
+ // A page load says how the page showing got its data, unless the URL
861
+ // has moved on by the time it returns, and a later load says instead.
862
+ const load = request === undefined;
863
+ const at = here();
816
864
  pending(origin, true);
865
+ this.trips++;
866
+ if (load)
867
+ this.loadState = HUB_LOAD_LOADING;
868
+ this.landHub();
817
869
  try {
818
- return await fetchItems(page, request);
870
+ const items = await fetchItems(page, request);
871
+ this.serverState = HUB_SERVER_REACHABLE;
872
+ this.error = "";
873
+ this.asOf = new Date().toISOString();
874
+ if (load && here() === at)
875
+ this.loadState = HUB_LOAD_LOADED;
876
+ return items;
819
877
  }
820
878
  catch (err) {
821
879
  // A refusal is the handler's answer, not a failure: the request's
822
880
  // sender shows it, and nothing is reported.
823
- if (err instanceof RefusedError)
881
+ if (err instanceof RefusedError) {
882
+ this.serverState = HUB_SERVER_REACHABLE;
883
+ this.error = "";
824
884
  return err;
885
+ }
825
886
  console.error(`lb-hub: request failed for '${page}'`, err);
826
887
  origin?.setAttribute(ATTR_REQUEST_ERROR, "");
888
+ // A server that answered with an error is there and failing; one that
889
+ // never answered is out of reach.
890
+ this.error = err instanceof RoundTripError ? err.reason : HUB_ERROR_NETWORK;
891
+ this.serverState =
892
+ err instanceof RoundTripError && /^\d+$/.test(err.reason)
893
+ ? HUB_SERVER_FAILING
894
+ : HUB_SERVER_UNREACHABLE;
895
+ if (load && here() === at)
896
+ this.loadState = HUB_LOAD_FAILED;
827
897
  return err instanceof Error ? err : new Error(String(err));
828
898
  }
829
899
  finally {
830
900
  pending(origin, false);
901
+ this.trips--;
902
+ this.landHub();
903
+ }
904
+ }
905
+ /**
906
+ * The hub's own query: one row, keyed by the path, holding how the page's
907
+ * data and the server stand. The same columns are stamped on the hub
908
+ * itself, so a stylesheet can reach the whole document without the
909
+ * markup naming the query. An empty one is not stamped at all.
910
+ */
911
+ landHub() {
912
+ const row = {
913
+ [URL_COLUMN_PATH]: location.pathname,
914
+ [HUB_COLUMN_LOAD_STATE]: this.loadState,
915
+ [HUB_COLUMN_SERVER_STATE]: this.serverState,
916
+ [HUB_COLUMN_ERROR]: this.error,
917
+ [HUB_COLUMN_PENDING]: this.trips,
918
+ [HUB_COLUMN_AS_OF]: this.asOf,
919
+ };
920
+ for (const [name, value] of Object.entries(row)) {
921
+ if (name === URL_COLUMN_PATH)
922
+ continue;
923
+ if (value === "")
924
+ this.removeAttribute(name);
925
+ else
926
+ this.setAttribute(name, String(value));
831
927
  }
928
+ if (!this.querySelector(`[${ATTR_QUERY}="${HUB_QUERY}"]`))
929
+ return;
930
+ applyResponse(this, [
931
+ { query: HUB_QUERY, kind: KIND_ROW, key: URL_COLUMN_PATH, row },
932
+ ]);
832
933
  }
833
934
  /**
834
935
  * Answer a request for a query the hub serves, with no round trip. The
835
- * hub serves `lb-url` and `lb-choices`, and updates them.
936
+ * hub serves `lb-url` and `lb-choices`, and updates them. It serves
937
+ * `lb-hub` too, which nothing but the hub writes.
836
938
  */
837
939
  async answer(request, origin) {
838
940
  if (request.query === CHOICES_QUERY && request.name === REQUEST_ROW_UPDATE) {
@@ -927,6 +1029,9 @@ class LbHub extends HTMLElement {
927
1029
  return again instanceof Error ? [item] : [item, ...again];
928
1030
  }
929
1031
  const template = this.arrive();
1032
+ // The request's answer carried the page's queries for where it moved.
1033
+ this.loadState = HUB_LOAD_LOADED;
1034
+ this.landHub();
930
1035
  this.place();
931
1036
  if (template)
932
1037
  applyResponse(this, load);
@@ -954,6 +1059,9 @@ class LbHub extends HTMLElement {
954
1059
  async navigate(origin) {
955
1060
  const template = this.arrive();
956
1061
  if (!template) {
1062
+ // No page, so nothing to load: the unknown page is what shows.
1063
+ this.loadState = HUB_LOAD_LOADED;
1064
+ this.landHub();
957
1065
  this.place();
958
1066
  attachPlaced();
959
1067
  return [];