@loadbare/app 0.7.4 → 0.8.1

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 (48) hide show
  1. package/dist/build/skills-cli.d.ts +12 -0
  2. package/dist/build/skills-cli.d.ts.map +1 -0
  3. package/dist/build/skills-cli.js +81 -0
  4. package/dist/build/skills-cli.js.map +1 -0
  5. package/dist/build/skills.d.ts +47 -0
  6. package/dist/build/skills.d.ts.map +1 -0
  7. package/dist/build/skills.js +124 -0
  8. package/dist/build/skills.js.map +1 -0
  9. package/dist/core/lb-constants.d.ts +1 -2
  10. package/dist/core/lb-constants.d.ts.map +1 -1
  11. package/dist/core/lb-constants.js +13 -12
  12. package/dist/core/lb-constants.js.map +1 -1
  13. package/dist/core/lb-types.d.ts +4 -10
  14. package/dist/core/lb-types.d.ts.map +1 -1
  15. package/dist/core/lb-types.js.map +1 -1
  16. package/dist/hub/lb-hub.browser.d.ts.map +1 -1
  17. package/dist/hub/lb-hub.browser.js +47 -24
  18. package/dist/hub/lb-hub.browser.js.map +1 -1
  19. package/dist/server/lb-express.d.ts.map +1 -1
  20. package/dist/server/lb-express.js +2 -7
  21. package/dist/server/lb-express.js.map +1 -1
  22. package/dist/server/lb-server.d.ts +8 -15
  23. package/dist/server/lb-server.d.ts.map +1 -1
  24. package/dist/server/lb-server.js +0 -3
  25. package/dist/server/lb-server.js.map +1 -1
  26. package/docs/TECHREF-1.0.md +21 -13
  27. package/docs/analysis-closed-set.md +16 -9
  28. package/docs/comparison.md +7 -6
  29. package/docs/reference/custom-elements.md +11 -6
  30. package/docs/reference/data-binding.md +36 -12
  31. package/docs/reference/page-files.md +13 -9
  32. package/docs/reference/widgets.md +4 -4
  33. package/docs/roadmap.md +1 -1
  34. package/docs/testing.md +5 -1
  35. package/docs/theory.md +1 -1
  36. package/docs/tutorials/080-widget-requests.md +9 -28
  37. package/package.json +8 -4
  38. package/skills/loadbare-app/SKILL.md +258 -0
  39. package/skills/loadbare-app/references/TECHREF-1.0.md +1189 -0
  40. package/skills/loadbare-app/references/builder.md +134 -0
  41. package/skills/loadbare-app/references/chrome.md +158 -0
  42. package/skills/loadbare-app/references/css.md +44 -0
  43. package/skills/loadbare-app/references/custom-elements.md +397 -0
  44. package/skills/loadbare-app/references/data-binding.md +457 -0
  45. package/skills/loadbare-app/references/overview.md +38 -0
  46. package/skills/loadbare-app/references/page-files.md +194 -0
  47. package/skills/loadbare-app/references/server.md +142 -0
  48. package/skills/loadbare-app/references/widgets.md +174 -0
@@ -1,13 +1,9 @@
1
1
  /// <reference lib="dom" />
2
- import { ACTION_CELL_CHANGE, ACTION_ROW_DELETE, ACTION_ROW_INSERT, ACTION_ROW_UPDATE, ATTR_ACTION, ATTR_CELL, ATTR_ERROR, ATTR_KEY_VALUE, ATTR_LIST, ATTR_NAV_LINK, ATTR_PAGE, ATTR_PENDING, ATTR_ROW, ATTR_UNKNOWN_PAGE, ATTR_VALUE, LB_ACTIONS, LB_RESERVED_PREFIX, HUB_TAG_NAME, NAV_CELL_LABEL, NAV_CELL_URI, NAV_ROW, LB_EVENT_NAME, LB_ENDPOINT, LB_REQUEST_TIMEOUT_MS, } from "../core/lb-constants.js";
2
+ import { ACTION_ROW_DELETE, ACTION_ROW_INSERT, ACTION_ROW_UPDATE, ATTR_ACTION, ATTR_CELL, ATTR_ERROR, ATTR_KEY_VALUE, ATTR_LIST, ATTR_NAV_LINK, ATTR_PAGE, ATTR_PENDING, ATTR_ROW, ATTR_UNKNOWN_PAGE, ATTR_VALUE, LB_ACTIONS, LB_RESERVED_PREFIX, HUB_TAG_NAME, NAV_CELL_LABEL, NAV_CELL_URI, NAV_ROW, LB_EVENT_NAME, LB_ENDPOINT, LB_REQUEST_TIMEOUT_MS, } from "../core/lb-constants.js";
3
3
  import { applyData, isValueControl, within } from "./lb-apply.js";
4
4
  export { applyData, applyRow } from "./lb-apply.js";
5
5
  /** What each operation takes from scope, and what it cannot be sent without. */
6
6
  const OPERATIONS = {
7
- [ACTION_CELL_CHANGE]: {
8
- fills: ["list", "key", "cell"],
9
- requires: ["list", "key", "cell", "value"],
10
- },
11
7
  [ACTION_ROW_INSERT]: { fills: ["list"], requires: ["list", "values"] },
12
8
  [ACTION_ROW_DELETE]: { fills: ["list", "key"], requires: ["list", "key"] },
13
9
  [ACTION_ROW_UPDATE]: {
@@ -35,7 +31,7 @@ function scope(request, el) {
35
31
  const { action } = request;
36
32
  const reserved = action.startsWith(LB_RESERVED_PREFIX);
37
33
  // The reserved prefix is the whole of the discriminant, so a reserved
38
- // name that is not one of the four operations is a typo rather than
34
+ // name that is not one of the three operations is a typo rather than
39
35
  // something to forward. Refused here, where the widget can see why: a
40
36
  // server would only report that it declares no such action.
41
37
  if (reserved && !LB_ACTIONS.includes(action)) {
@@ -78,7 +74,12 @@ function scope(request, el) {
78
74
  }
79
75
  for (const name of operation.fills)
80
76
  fill(name);
81
- // Gathered from the record the element belongs to, the way a button
77
+ // An element carrying lb-cell is a record of one cell, the way a control
78
+ // has a value and a form has values: the map holds that cell and nothing
79
+ // beside it. Its value is the one the dispatcher sent, or else what its
80
+ // control holds.
81
+ //
82
+ // Anything else gathers from the record it belongs to, the way a button
82
83
  // submits its form owner: the nearest form, table row or live row around
83
84
  // it, the element itself included. A <tr> counts because it is the one
84
85
  // place a form cannot go around a row's controls. What else sits beside
@@ -87,23 +88,44 @@ function scope(request, el) {
87
88
  // cells are meant. Outside every scope the list is what is missing, and
88
89
  // that is what gets reported.
89
90
  if (fields.values === undefined && operation.requires.includes("values")) {
90
- const owner = el.closest(`form, tr, [${ATTR_KEY_VALUE}]`);
91
- const ours = !enclosing || (owner !== enclosing && enclosing.contains(owner));
92
- if (owner && ours) {
93
- const { values, read } = gatherValues(owner);
94
- if (read.length > 0) {
95
- fields.values = values;
96
- if (action === ACTION_ROW_INSERT)
97
- gathered.set(request, read);
91
+ const cell = el.getAttribute(ATTR_CELL);
92
+ if (cell !== null) {
93
+ const control = controlOf(el);
94
+ const value = typeof fields.value === "string" ? fields.value : control?.value;
95
+ if (value !== undefined) {
96
+ fields.values = { [cell]: value };
97
+ if (action === ACTION_ROW_INSERT && control?.value === value) {
98
+ gathered.set(request, [{ cell: el, control, value }]);
99
+ }
98
100
  }
99
101
  }
100
- else if (enclosing) {
101
- console.error(`lb-hub: '${action}' from <${el.localName}> is in no form, table ` +
102
- `row or live row to gather values from, not sent; put its cells ` +
103
- `in a <form>`, el);
102
+ else if (fields.value !== undefined) {
103
+ console.error(`lb-hub: '${action}' from <${el.localName}> carries a value but no ` +
104
+ `${ATTR_CELL} to say which column it is, not sent`, el);
104
105
  return false;
105
106
  }
107
+ else {
108
+ const owner = el.closest(`form, tr, [${ATTR_KEY_VALUE}]`);
109
+ const ours = !enclosing || (owner !== enclosing && enclosing.contains(owner));
110
+ if (owner && ours) {
111
+ const { values, read } = gatherValues(owner);
112
+ if (read.length > 0) {
113
+ fields.values = values;
114
+ if (action === ACTION_ROW_INSERT)
115
+ gathered.set(request, read);
116
+ }
117
+ }
118
+ else if (enclosing) {
119
+ console.error(`lb-hub: '${action}' from <${el.localName}> is in no form, table ` +
120
+ `row or live row to gather values from, not sent; put its cells ` +
121
+ `in a <form>`, el);
122
+ return false;
123
+ }
124
+ }
106
125
  }
126
+ // An operation carries its values and nothing else: the value they were
127
+ // built from is not a second copy on the wire.
128
+ delete fields.value;
107
129
  const missing = operation.requires.filter((f) => fields[f] === undefined);
108
130
  if (missing.length > 0) {
109
131
  console.error(`lb-hub: '${action}' from <${el.localName}> has no ` +
@@ -325,13 +347,14 @@ class LbHub extends HTMLElement {
325
347
  if (el instanceof HTMLFormElement)
326
348
  return;
327
349
  // A button beside the cells is pressed once they are done. An element
328
- // holding them has no such moment: a click into one of its own inputs
329
- // would send the row.
350
+ // that is a cell, or holds them, has no such moment: a click into its
351
+ // own input would send the row. Deciding when a cell has changed is a
352
+ // widget's.
330
353
  const action = el.getAttribute(ATTR_ACTION);
331
354
  if ((action === ACTION_ROW_INSERT || action === ACTION_ROW_UPDATE) &&
332
- el.querySelector(`[${ATTR_CELL}]`)) {
333
- console.error(`lb-hub: '${action}' on <${el.localName}> holds its own cells, ` +
334
- `so a click inside it is not sent; put it on a <form> or a button`, el);
355
+ (el.hasAttribute(ATTR_CELL) || el.querySelector(`[${ATTR_CELL}]`))) {
356
+ console.error(`lb-hub: '${action}' on <${el.localName}> is or holds a cell, so ` +
357
+ `a click on it is not sent; put it on a <form>, a button or a widget`, el);
335
358
  return;
336
359
  }
337
360
  e.preventDefault();
@@ -1 +1 @@
1
- {"version":3,"file":"lb-hub.browser.js","sourceRoot":"","sources":["../../hub/lb-hub.browser.ts"],"names":[],"mappings":"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","sourcesContent":["/// <reference lib=\"dom\" />\n\nimport {\n ACTION_CELL_CHANGE,\n ACTION_ROW_DELETE,\n ACTION_ROW_INSERT,\n ACTION_ROW_UPDATE,\n ATTR_ACTION,\n ATTR_CELL,\n ATTR_ERROR,\n ATTR_KEY_VALUE,\n ATTR_LIST,\n ATTR_NAV_LINK,\n ATTR_PAGE,\n ATTR_PENDING,\n ATTR_ROW,\n ATTR_UNKNOWN_PAGE,\n ATTR_VALUE,\n LB_ACTIONS,\n LB_RESERVED_PREFIX,\n HUB_TAG_NAME,\n NAV_CELL_LABEL,\n NAV_CELL_URI,\n NAV_ROW,\n LB_EVENT_NAME,\n LB_ENDPOINT,\n LB_REQUEST_TIMEOUT_MS,\n} from \"../core/lb-constants.js\";\nimport type { HubData, HubRequest } from \"../core/lb-types.js\";\nimport { applyData, isValueControl, within } from \"./lb-apply.js\";\n\nexport { applyData, applyRow } from \"./lb-apply.js\";\n\n/** What each operation takes from scope, and what it cannot be sent without. */\nconst OPERATIONS: Record<\n string,\n { fills: readonly string[]; requires: readonly string[] }\n> = {\n [ACTION_CELL_CHANGE]: {\n fills: [\"list\", \"key\", \"cell\"],\n requires: [\"list\", \"key\", \"cell\", \"value\"],\n },\n [ACTION_ROW_INSERT]: { fills: [\"list\"], requires: [\"list\", \"values\"] },\n [ACTION_ROW_DELETE]: { fills: [\"list\", \"key\"], requires: [\"list\", \"key\"] },\n [ACTION_ROW_UPDATE]: {\n fills: [\"list\", \"key\"],\n requires: [\"list\", \"key\", \"values\"],\n },\n};\n\n/**\n * Complete a request from where its element sits — see docs/TECHREF-1.0.md,\n * \"Requests\". Every request passes through here, whoever built it, so a\n * widget states what it did and never where it is.\n *\n * The scope field is `list` or `row`, whichever attribute scoped the nearest\n * ancestor, so a declared action fired inside a single-row scope says so and\n * the wire never needs a noun for the addressable thing. A field the request\n * already carries is the dispatcher's to decide and is left as it is.\n *\n * False when the request cannot be sent, having said why.\n */\nfunction scope(request: HubRequest | undefined, el: Element): boolean {\n if (!request?.action) {\n console.warn(`lb-hub: event without an action, ignoring`, request);\n return false;\n }\n const { action } = request;\n const reserved = action.startsWith(LB_RESERVED_PREFIX);\n // The reserved prefix is the whole of the discriminant, so a reserved\n // name that is not one of the four operations is a typo rather than\n // something to forward. Refused here, where the widget can see why: a\n // server would only report that it declares no such action.\n if (reserved && !(LB_ACTIONS as readonly string[]).includes(action)) {\n console.error(\n `lb-hub: '${action}' begins with '${LB_RESERVED_PREFIX}' ` +\n `but names no operation, ignoring`,\n );\n return false;\n }\n\n const fields = request as unknown as Record<string, unknown>;\n // The element's own lb-list or lb-row is what it displays. Where it sits\n // is its ancestors', the way a control belongs to the form around it.\n const parent = el.parentElement;\n const enclosing = parent?.closest(`[${ATTR_LIST}], [${ATTR_ROW}]`);\n const row = parent?.closest(`[${ATTR_KEY_VALUE}]`);\n const inScope: Record<string, string | null | undefined> = {\n list: enclosing?.getAttribute(ATTR_LIST),\n row: enclosing?.hasAttribute(ATTR_LIST)\n ? null\n : enclosing?.getAttribute(ATTR_ROW),\n // Only a row of that scope: a key from outside it names a row of some\n // other list.\n key:\n row && enclosing?.contains(row) ? row.getAttribute(ATTR_KEY_VALUE) : null,\n cell: el.getAttribute(ATTR_CELL),\n };\n const fill = (name: string) => {\n const value = inScope[name];\n if (fields[name] === undefined && value !== null && value !== undefined) {\n fields[name] = value;\n }\n };\n\n const operation = OPERATIONS[action];\n if (!operation) {\n // A declared action names one scope or the other, never both.\n if (fields.list === undefined && fields.row === undefined) {\n fill(\"list\");\n fill(\"row\");\n }\n fill(\"key\");\n fill(\"cell\");\n return true;\n }\n\n for (const name of operation.fills) fill(name);\n // Gathered from the record the element belongs to, the way a button\n // submits its form owner: the nearest form, table row or live row around\n // it, the element itself included. A <tr> counts because it is the one\n // place a form cannot go around a row's controls. What else sits beside\n // the element never decides it. The record is the scope's, so it is never\n // the scope itself nor outside it; with none, there is nothing to say which\n // cells are meant. Outside every scope the list is what is missing, and\n // that is what gets reported.\n if (fields.values === undefined && operation.requires.includes(\"values\")) {\n const owner = el.closest(`form, tr, [${ATTR_KEY_VALUE}]`);\n const ours =\n !enclosing || (owner !== enclosing && enclosing.contains(owner));\n if (owner && ours) {\n const { values, read } = gatherValues(owner);\n if (read.length > 0) {\n fields.values = values;\n if (action === ACTION_ROW_INSERT) gathered.set(request, read);\n }\n } else if (enclosing) {\n console.error(\n `lb-hub: '${action}' from <${el.localName}> is in no form, table ` +\n `row or live row to gather values from, not sent; put its cells ` +\n `in a <form>`,\n el,\n );\n return false;\n }\n }\n const missing = operation.requires.filter((f) => fields[f] === undefined);\n if (missing.length > 0) {\n console.error(\n `lb-hub: '${action}' from <${el.localName}> has no ` +\n `${missing.join(\", \")} in scope, not sent`,\n el,\n );\n return false;\n }\n return true;\n}\n\n/** Dispatch the bare action an element carries, for the hub to scope. */\nfunction dispatchAction(el: Element): void {\n // Pressed again while its round trip is in flight. Ignoring it makes a\n // pending native action disabled in fact, so a stylesheet only has to\n // show it. A widget is not held back this way: one that sends on change\n // must have its latest value sent, not dropped.\n if (el.hasAttribute(ATTR_PENDING)) return;\n const detail = { action: el.getAttribute(ATTR_ACTION)! } as HubRequest;\n el.dispatchEvent(new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }));\n}\n\n/** A form control whose state is its `value`. */\ntype ValueControl = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;\n\n/** One cell a request read, the control it read, and the value it read. */\ninterface Read {\n cell: Element;\n control: ValueControl;\n value: string;\n}\n\n/**\n * What each insert gathered, until its round trip succeeds. Keyed by the\n * request itself, so a request an ancestor stops leaves nothing behind.\n */\nconst gathered = new WeakMap<HubRequest, Read[]>();\n\n/**\n * The control a cell reads from, for gathering rather than landing.\n *\n * A cell is always either a native form control itself (a bare\n * `<input lb-cell>`) or a widget that wraps one (`<lb-input>`, `<lb-select>`)\n * — that wrapping is structural, not incidental, which is what lets this be\n * one rule instead of one per widget type. The controls are the ones a value\n * lands on, so what is read back is what landed.\n */\nfunction controlOf(el: Element): ValueControl | undefined {\n return isValueControl(el)\n ? el\n : [...el.querySelectorAll(\"input, select, textarea\")].find(isValueControl);\n}\n\n/**\n * Every lb-cell of an element's scope, gathered into one values map. Shared\n * by lb-row-insert and lb-row-update — both submit a batch of cells, and\n * differ only in whether a key comes with them. What was read comes back\n * beside the map, so an insert can reset it once it succeeds.\n *\n * Found the way a row lands, so a cell inside a nested scope is that scope's\n * and is not read: the options of a picker in the row are the picker's rows,\n * while the picker's own cell is the row's.\n */\nfunction gatherValues(el: Element): {\n values: Record<string, string>;\n read: Read[];\n} {\n const values: Record<string, string> = {};\n const read: Read[] = [];\n for (const cell of within(el, `[${ATTR_CELL}]`)) {\n const name = cell.getAttribute(ATTR_CELL)!;\n const control = controlOf(cell);\n if (control === undefined) {\n console.warn(\n `lb-hub: lb-cell '${name}' has no input, select or textarea to ` +\n `read, ignoring it`,\n cell,\n );\n continue;\n }\n values[name] = control.value;\n read.push({ cell, control, value: control.value });\n }\n return { values, read };\n}\n\n/**\n * After an insert succeeds, what it gathered goes back to its default, the\n * way `form.reset()` resets a form: the row those cells held now lives in the\n * list. A control showing something other than what was read holds an edit\n * made during the round trip, which was not sent, and is left alone.\n *\n * Where a value landed, `lb-value` comes off the cell first, so a widget that\n * renders from it sees the attribute go and returns to its default. The\n * control is reset after, so it ends at its default whatever the widget did.\n * Nothing is dispatched, as `form.reset()` fires no `change`.\n */\nfunction resetGathered(read: Read[]): void {\n for (const { cell, control, value } of read) {\n if (control.value !== value) continue;\n cell.removeAttribute(ATTR_VALUE);\n control.removeAttribute(ATTR_VALUE);\n if (control.localName === \"select\") {\n for (const option of (control as HTMLSelectElement).options) {\n option.selected = option.defaultSelected;\n }\n } else {\n const text = control as HTMLInputElement | HTMLTextAreaElement;\n text.value = text.defaultValue;\n }\n }\n}\n\n/**\n * A path names a page host, and nothing finer. The bare path resolves to\n * `index`, the same convention a static file server follows — an\n * application names its landing page that way rather than the hub singling\n * one out.\n */\nfunction pageNameFor(path: string): string {\n const name = path.replace(/^\\/+|\\/+$/g, \"\");\n return name === \"\" ? \"index\" : name;\n}\n\n/**\n * The `<template>` a page's markup ships in. The builder guarantees one per\n * page name (build/locations.ts refuses two page files sharing a stub), so\n * a second match here means the document was assembled by something else,\n * and is worth saying out loud rather than resolving by document order.\n */\nfunction pageHostFor(page: string): HTMLTemplateElement | null {\n const hosts: HTMLTemplateElement[] = [];\n for (const el of document.querySelectorAll(`template[${ATTR_PAGE}]`)) {\n if (el.getAttribute(ATTR_PAGE) === page) {\n hosts.push(el as HTMLTemplateElement);\n }\n }\n if (hosts.length === 0) {\n console.error(`lb-hub: no page host for '${page}'`);\n return null;\n }\n if (hosts.length > 1) {\n console.error(\n `lb-hub: ${hosts.length} page hosts carry ${ATTR_PAGE}=\"${page}\", ` +\n `using the first`,\n );\n }\n return hosts[0]!;\n}\n\n/**\n * Fetch one round trip against the client-side deadline. Throws — rather\n * than swallowing — on a non-ok response, a network failure, or a timeout,\n * so a caller that needs to know the request failed (as opposed to one that\n * only ever wants the freshest data, like navigate()) can tell the two\n * apart from a genuinely empty result.\n */\nasync function fetchData(page: string, request?: HubRequest): Promise<HubData> {\n const url = `${LB_ENDPOINT}?page=${encodeURIComponent(page)}`;\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), LB_REQUEST_TIMEOUT_MS);\n try {\n // An empty body is how the hub asks for the page's whole query set.\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify(request ?? {}),\n signal: controller.signal,\n });\n if (!res.ok) {\n throw new Error(`lb-hub: server responded ${res.status} for ${url}`);\n }\n return (await res.json()) as HubData;\n } finally {\n clearTimeout(timeout);\n }\n}\n\n/**\n * The hub is a singleton outside <main>. It survives every page change,\n * so it needs no id to disambiguate it.\n */\nclass LbHub extends HTMLElement {\n private main!: HTMLElement;\n\n /**\n * The page currently in <main>. A page declares its own requests, so a request\n * has to say which page it came from. The page rides on the URL rather\n * than in the request, leaving the operation set closed.\n */\n private page = \"\";\n\n connectedCallback() {\n this.main = this.querySelector(\"main\")!;\n\n // Scoped in the capture phase, on the way down, so every ancestor that\n // sees the request on its way back up sees it whole. A request that\n // cannot be sent is stopped here, before anything sees it at all.\n this.addEventListener(\n LB_EVENT_NAME,\n (e: Event) => {\n const origin = e.target as Element;\n const request = (e as CustomEvent<HubRequest>).detail;\n if (!scope(request, origin)) e.stopPropagation();\n },\n { capture: true },\n );\n\n this.addEventListener(LB_EVENT_NAME, (e: Event) => {\n const request = (e as CustomEvent<HubRequest>).detail;\n // The element that dispatched the request, not e.currentTarget (the\n // hub itself, since the event bubbles) — pending/error state belongs\n // at the origin so a widget can watch its own attributes. aria-busy\n // states the same fact to assistive technology, which no stylesheet\n // on lb-pending reaches.\n const origin = e.target as Element | null;\n origin?.removeAttribute(ATTR_ERROR);\n origin?.setAttribute(ATTR_PENDING, \"\");\n origin?.setAttribute(\"aria-busy\", \"true\");\n void fetchData(this.page, request)\n .then((data) => {\n applyData(this, data);\n resetGathered(gathered.get(request) ?? []);\n })\n .catch((err) => {\n console.error(`lb-hub: request failed`, err);\n origin?.setAttribute(ATTR_ERROR, \"\");\n })\n .finally(() => {\n origin?.removeAttribute(ATTR_PENDING);\n origin?.removeAttribute(\"aria-busy\");\n });\n });\n\n /**\n * A native element carrying an action. The hub dispatches the bare action\n * from that element as the ordinary bubbling event, rather than sending\n * it directly, so it is scoped exactly as a widget's is and an ancestor\n * widget can still stop it and confirm. A native action button and a\n * hand-written widget therefore produce identical events.\n *\n * A widget carrying an action is left alone — it owns its own interaction\n * and decides what counts as performing it, which for a <select> is a\n * change rather than a click. The test is the same lexical one applyData\n * uses: a hyphen in the tag name.\n */\n this.addEventListener(\"click\", (e: MouseEvent) => {\n const el = (e.target as Element | null)?.closest(`[${ATTR_ACTION}]`);\n if (!el || el.localName.includes(\"-\")) return;\n // A form performs its action on submit, whatever was clicked in it.\n if (el instanceof HTMLFormElement) return;\n // A button beside the cells is pressed once they are done. An element\n // holding them has no such moment: a click into one of its own inputs\n // would send the row.\n const action = el.getAttribute(ATTR_ACTION);\n if (\n (action === ACTION_ROW_INSERT || action === ACTION_ROW_UPDATE) &&\n el.querySelector(`[${ATTR_CELL}]`)\n ) {\n console.error(\n `lb-hub: '${action}' on <${el.localName}> holds its own cells, ` +\n `so a click inside it is not sent; put it on a <form> or a button`,\n el,\n );\n return;\n }\n e.preventDefault();\n dispatchAction(el);\n });\n\n // The same delegation for the event a form owns.\n this.addEventListener(\"submit\", (e: SubmitEvent) => {\n const form = e.target as Element | null;\n if (!form || !form.hasAttribute(ATTR_ACTION)) return;\n e.preventDefault();\n dispatchAction(form);\n });\n\n // Only anchors that opted in. Everything else is a real link.\n this.addEventListener(\"click\", (e: MouseEvent) => {\n const link = (e.target as Element | null)?.closest(`a[${ATTR_NAV_LINK}]`);\n if (!link) return;\n e.preventDefault();\n const path = new URL((link as HTMLAnchorElement).href).pathname;\n history.pushState(null, \"\", path);\n void this.navigate(path);\n });\n\n addEventListener(\"popstate\", () => void this.navigate(location.pathname));\n\n void this.navigate(location.pathname);\n }\n\n /**\n * Navigation is the one operation that replaces host DOM. The host is\n * already in the document, so selecting a page is getElementById() and\n * there is no fetching mechanism on the host channel.\n */\n private async navigate(path: string): Promise<void> {\n const page = pageNameFor(path);\n this.landNavigation(path, page);\n const template = pageHostFor(page);\n if (!template) {\n this.reportUnknownPage();\n this.reveal();\n return;\n }\n this.page = page;\n // Insert the page shell before fetching its data, not after: the\n // template is already in the document, so holding it back behind a\n // network round trip only leaves <main> empty for no reason.\n this.main.replaceChildren(template.content.cloneNode(true));\n this.reveal();\n let data: HubData = {};\n try {\n data = await fetchData(page);\n } catch (err) {\n console.error(`lb-hub: failed to load data for '${page}'`, err);\n }\n applyData(this, data);\n }\n\n /**\n * An app that ships `<body hidden>` (see docs/reference/chrome.md) is\n * asking to stay invisible until there is something coherent to show,\n * rather than flash the chrome before `<main>` has real content. This is\n * a no-op for an app that doesn't use that convention.\n */\n private reveal(): void {\n document.body.hidden = false;\n }\n\n /**\n * The hub's own row: where the page is, in the vocabulary a server\n * answer arrives in, so a chrome displays it the way it displays anything\n * — `lb-row=\"lb-navigation\"` on a subtree, `lb-cell` on what shows a\n * value. Landed before the page host is looked up, so a miss has it too;\n * the unknown-page dialog is one consumer among whatever others the\n * chrome writes.\n *\n * The label is the nav's: the text of the first lb-nav-link anchor whose\n * href names this page. Taken from the nav rather than from a click, so a\n * click, a reload and the back button all land the same label — and a\n * path no anchor names lands an empty one, explicitly, so a consumer is\n * never left showing the previous page's.\n *\n * Landed only where a subtree names it. applyData() warns about a query\n * with no scope, and a chrome that displays no navigation has nothing to\n * be warned about.\n */\n private landNavigation(path: string, page: string): void {\n if (!this.querySelector(`[${ATTR_ROW}=\"${NAV_ROW}\"]`)) return;\n let label = \"\";\n for (const a of this.querySelectorAll<HTMLAnchorElement>(\n `a[${ATTR_NAV_LINK}]`,\n )) {\n if (pageNameFor(new URL(a.href).pathname) !== page) continue;\n label = a.textContent?.trim() ?? \"\";\n break;\n }\n applyData(this, {\n [NAV_ROW]: { [NAV_CELL_LABEL]: label, [NAV_CELL_URI]: path },\n });\n }\n\n /**\n * A path that resolves to no page host is discovered client-side, after a\n * successful 200 — every route gets the same document, so there is no\n * HTTP 404 to report (see docs/tutorials/010-pages-and-navigation.md).\n * lb-unknown-page is optional: an application that declares none gets\n * nothing beyond navigate()'s console.error. The builder guarantees the\n * element it's on is a <dialog> (build/assemble.ts), so this can call\n * showModal() without checking the tag here.\n *\n * Only opens it. What the dialog says it gets from landNavigation(),\n * already run, by naming the hub's row in lb-row like any subtree.\n *\n * Scoped to the hub, like every other lookup here, so that one rule holds\n * without exception: what the hub acts on is inside the hub.\n */\n private reportUnknownPage(): void {\n const dialog = this.querySelector(`[${ATTR_UNKNOWN_PAGE}]`);\n if (!dialog) return;\n const modal = dialog as HTMLDialogElement;\n if (!modal.open) modal.showModal();\n }\n}\n\ncustomElements.define(HUB_TAG_NAME, LbHub);\n"]}
1
+ {"version":3,"file":"lb-hub.browser.js","sourceRoot":"","sources":["../../hub/lb-hub.browser.ts"],"names":[],"mappings":"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","sourcesContent":["/// <reference lib=\"dom\" />\n\nimport {\n ACTION_ROW_DELETE,\n ACTION_ROW_INSERT,\n ACTION_ROW_UPDATE,\n ATTR_ACTION,\n ATTR_CELL,\n ATTR_ERROR,\n ATTR_KEY_VALUE,\n ATTR_LIST,\n ATTR_NAV_LINK,\n ATTR_PAGE,\n ATTR_PENDING,\n ATTR_ROW,\n ATTR_UNKNOWN_PAGE,\n ATTR_VALUE,\n LB_ACTIONS,\n LB_RESERVED_PREFIX,\n HUB_TAG_NAME,\n NAV_CELL_LABEL,\n NAV_CELL_URI,\n NAV_ROW,\n LB_EVENT_NAME,\n LB_ENDPOINT,\n LB_REQUEST_TIMEOUT_MS,\n} from \"../core/lb-constants.js\";\nimport type { HubData, HubRequest } from \"../core/lb-types.js\";\nimport { applyData, isValueControl, within } from \"./lb-apply.js\";\n\nexport { applyData, applyRow } from \"./lb-apply.js\";\n\n/** What each operation takes from scope, and what it cannot be sent without. */\nconst OPERATIONS: Record<\n string,\n { fills: readonly string[]; requires: readonly string[] }\n> = {\n [ACTION_ROW_INSERT]: { fills: [\"list\"], requires: [\"list\", \"values\"] },\n [ACTION_ROW_DELETE]: { fills: [\"list\", \"key\"], requires: [\"list\", \"key\"] },\n [ACTION_ROW_UPDATE]: {\n fills: [\"list\", \"key\"],\n requires: [\"list\", \"key\", \"values\"],\n },\n};\n\n/**\n * Complete a request from where its element sits — see docs/TECHREF-1.0.md,\n * \"Requests\". Every request passes through here, whoever built it, so a\n * widget states what it did and never where it is.\n *\n * The scope field is `list` or `row`, whichever attribute scoped the nearest\n * ancestor, so a declared action fired inside a single-row scope says so and\n * the wire never needs a noun for the addressable thing. A field the request\n * already carries is the dispatcher's to decide and is left as it is.\n *\n * False when the request cannot be sent, having said why.\n */\nfunction scope(request: HubRequest | undefined, el: Element): boolean {\n if (!request?.action) {\n console.warn(`lb-hub: event without an action, ignoring`, request);\n return false;\n }\n const { action } = request;\n const reserved = action.startsWith(LB_RESERVED_PREFIX);\n // The reserved prefix is the whole of the discriminant, so a reserved\n // name that is not one of the three operations is a typo rather than\n // something to forward. Refused here, where the widget can see why: a\n // server would only report that it declares no such action.\n if (reserved && !(LB_ACTIONS as readonly string[]).includes(action)) {\n console.error(\n `lb-hub: '${action}' begins with '${LB_RESERVED_PREFIX}' ` +\n `but names no operation, ignoring`,\n );\n return false;\n }\n\n const fields = request as unknown as Record<string, unknown>;\n // The element's own lb-list or lb-row is what it displays. Where it sits\n // is its ancestors', the way a control belongs to the form around it.\n const parent = el.parentElement;\n const enclosing = parent?.closest(`[${ATTR_LIST}], [${ATTR_ROW}]`);\n const row = parent?.closest(`[${ATTR_KEY_VALUE}]`);\n const inScope: Record<string, string | null | undefined> = {\n list: enclosing?.getAttribute(ATTR_LIST),\n row: enclosing?.hasAttribute(ATTR_LIST)\n ? null\n : enclosing?.getAttribute(ATTR_ROW),\n // Only a row of that scope: a key from outside it names a row of some\n // other list.\n key:\n row && enclosing?.contains(row) ? row.getAttribute(ATTR_KEY_VALUE) : null,\n cell: el.getAttribute(ATTR_CELL),\n };\n const fill = (name: string) => {\n const value = inScope[name];\n if (fields[name] === undefined && value !== null && value !== undefined) {\n fields[name] = value;\n }\n };\n\n const operation = OPERATIONS[action];\n if (!operation) {\n // A declared action names one scope or the other, never both.\n if (fields.list === undefined && fields.row === undefined) {\n fill(\"list\");\n fill(\"row\");\n }\n fill(\"key\");\n fill(\"cell\");\n return true;\n }\n\n for (const name of operation.fills) fill(name);\n // An element carrying lb-cell is a record of one cell, the way a control\n // has a value and a form has values: the map holds that cell and nothing\n // beside it. Its value is the one the dispatcher sent, or else what its\n // control holds.\n //\n // Anything else gathers from the record it belongs to, the way a button\n // submits its form owner: the nearest form, table row or live row around\n // it, the element itself included. A <tr> counts because it is the one\n // place a form cannot go around a row's controls. What else sits beside\n // the element never decides it. The record is the scope's, so it is never\n // the scope itself nor outside it; with none, there is nothing to say which\n // cells are meant. Outside every scope the list is what is missing, and\n // that is what gets reported.\n if (fields.values === undefined && operation.requires.includes(\"values\")) {\n const cell = el.getAttribute(ATTR_CELL);\n if (cell !== null) {\n const control = controlOf(el);\n const value =\n typeof fields.value === \"string\" ? fields.value : control?.value;\n if (value !== undefined) {\n fields.values = { [cell]: value };\n if (action === ACTION_ROW_INSERT && control?.value === value) {\n gathered.set(request, [{ cell: el, control, value }]);\n }\n }\n } else if (fields.value !== undefined) {\n console.error(\n `lb-hub: '${action}' from <${el.localName}> carries a value but no ` +\n `${ATTR_CELL} to say which column it is, not sent`,\n el,\n );\n return false;\n } else {\n const owner = el.closest(`form, tr, [${ATTR_KEY_VALUE}]`);\n const ours =\n !enclosing || (owner !== enclosing && enclosing.contains(owner));\n if (owner && ours) {\n const { values, read } = gatherValues(owner);\n if (read.length > 0) {\n fields.values = values;\n if (action === ACTION_ROW_INSERT) gathered.set(request, read);\n }\n } else if (enclosing) {\n console.error(\n `lb-hub: '${action}' from <${el.localName}> is in no form, table ` +\n `row or live row to gather values from, not sent; put its cells ` +\n `in a <form>`,\n el,\n );\n return false;\n }\n }\n }\n // An operation carries its values and nothing else: the value they were\n // built from is not a second copy on the wire.\n delete fields.value;\n const missing = operation.requires.filter((f) => fields[f] === undefined);\n if (missing.length > 0) {\n console.error(\n `lb-hub: '${action}' from <${el.localName}> has no ` +\n `${missing.join(\", \")} in scope, not sent`,\n el,\n );\n return false;\n }\n return true;\n}\n\n/** Dispatch the bare action an element carries, for the hub to scope. */\nfunction dispatchAction(el: Element): void {\n // Pressed again while its round trip is in flight. Ignoring it makes a\n // pending native action disabled in fact, so a stylesheet only has to\n // show it. A widget is not held back this way: one that sends on change\n // must have its latest value sent, not dropped.\n if (el.hasAttribute(ATTR_PENDING)) return;\n const detail = { action: el.getAttribute(ATTR_ACTION)! } as HubRequest;\n el.dispatchEvent(new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }));\n}\n\n/** A form control whose state is its `value`. */\ntype ValueControl = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;\n\n/** One cell a request read, the control it read, and the value it read. */\ninterface Read {\n cell: Element;\n control: ValueControl;\n value: string;\n}\n\n/**\n * What each insert gathered, until its round trip succeeds. Keyed by the\n * request itself, so a request an ancestor stops leaves nothing behind.\n */\nconst gathered = new WeakMap<HubRequest, Read[]>();\n\n/**\n * The control a cell reads from, for gathering rather than landing.\n *\n * A cell is always either a native form control itself (a bare\n * `<input lb-cell>`) or a widget that wraps one (`<lb-input>`, `<lb-select>`)\n * — that wrapping is structural, not incidental, which is what lets this be\n * one rule instead of one per widget type. The controls are the ones a value\n * lands on, so what is read back is what landed.\n */\nfunction controlOf(el: Element): ValueControl | undefined {\n return isValueControl(el)\n ? el\n : [...el.querySelectorAll(\"input, select, textarea\")].find(isValueControl);\n}\n\n/**\n * Every lb-cell of an element's scope, gathered into one values map. Shared\n * by lb-row-insert and lb-row-update — both submit a batch of cells, and\n * differ only in whether a key comes with them. What was read comes back\n * beside the map, so an insert can reset it once it succeeds.\n *\n * Found the way a row lands, so a cell inside a nested scope is that scope's\n * and is not read: the options of a picker in the row are the picker's rows,\n * while the picker's own cell is the row's.\n */\nfunction gatherValues(el: Element): {\n values: Record<string, string>;\n read: Read[];\n} {\n const values: Record<string, string> = {};\n const read: Read[] = [];\n for (const cell of within(el, `[${ATTR_CELL}]`)) {\n const name = cell.getAttribute(ATTR_CELL)!;\n const control = controlOf(cell);\n if (control === undefined) {\n console.warn(\n `lb-hub: lb-cell '${name}' has no input, select or textarea to ` +\n `read, ignoring it`,\n cell,\n );\n continue;\n }\n values[name] = control.value;\n read.push({ cell, control, value: control.value });\n }\n return { values, read };\n}\n\n/**\n * After an insert succeeds, what it gathered goes back to its default, the\n * way `form.reset()` resets a form: the row those cells held now lives in the\n * list. A control showing something other than what was read holds an edit\n * made during the round trip, which was not sent, and is left alone.\n *\n * Where a value landed, `lb-value` comes off the cell first, so a widget that\n * renders from it sees the attribute go and returns to its default. The\n * control is reset after, so it ends at its default whatever the widget did.\n * Nothing is dispatched, as `form.reset()` fires no `change`.\n */\nfunction resetGathered(read: Read[]): void {\n for (const { cell, control, value } of read) {\n if (control.value !== value) continue;\n cell.removeAttribute(ATTR_VALUE);\n control.removeAttribute(ATTR_VALUE);\n if (control.localName === \"select\") {\n for (const option of (control as HTMLSelectElement).options) {\n option.selected = option.defaultSelected;\n }\n } else {\n const text = control as HTMLInputElement | HTMLTextAreaElement;\n text.value = text.defaultValue;\n }\n }\n}\n\n/**\n * A path names a page host, and nothing finer. The bare path resolves to\n * `index`, the same convention a static file server follows — an\n * application names its landing page that way rather than the hub singling\n * one out.\n */\nfunction pageNameFor(path: string): string {\n const name = path.replace(/^\\/+|\\/+$/g, \"\");\n return name === \"\" ? \"index\" : name;\n}\n\n/**\n * The `<template>` a page's markup ships in. The builder guarantees one per\n * page name (build/locations.ts refuses two page files sharing a stub), so\n * a second match here means the document was assembled by something else,\n * and is worth saying out loud rather than resolving by document order.\n */\nfunction pageHostFor(page: string): HTMLTemplateElement | null {\n const hosts: HTMLTemplateElement[] = [];\n for (const el of document.querySelectorAll(`template[${ATTR_PAGE}]`)) {\n if (el.getAttribute(ATTR_PAGE) === page) {\n hosts.push(el as HTMLTemplateElement);\n }\n }\n if (hosts.length === 0) {\n console.error(`lb-hub: no page host for '${page}'`);\n return null;\n }\n if (hosts.length > 1) {\n console.error(\n `lb-hub: ${hosts.length} page hosts carry ${ATTR_PAGE}=\"${page}\", ` +\n `using the first`,\n );\n }\n return hosts[0]!;\n}\n\n/**\n * Fetch one round trip against the client-side deadline. Throws — rather\n * than swallowing — on a non-ok response, a network failure, or a timeout,\n * so a caller that needs to know the request failed (as opposed to one that\n * only ever wants the freshest data, like navigate()) can tell the two\n * apart from a genuinely empty result.\n */\nasync function fetchData(page: string, request?: HubRequest): Promise<HubData> {\n const url = `${LB_ENDPOINT}?page=${encodeURIComponent(page)}`;\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), LB_REQUEST_TIMEOUT_MS);\n try {\n // An empty body is how the hub asks for the page's whole query set.\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify(request ?? {}),\n signal: controller.signal,\n });\n if (!res.ok) {\n throw new Error(`lb-hub: server responded ${res.status} for ${url}`);\n }\n return (await res.json()) as HubData;\n } finally {\n clearTimeout(timeout);\n }\n}\n\n/**\n * The hub is a singleton outside <main>. It survives every page change,\n * so it needs no id to disambiguate it.\n */\nclass LbHub extends HTMLElement {\n private main!: HTMLElement;\n\n /**\n * The page currently in <main>. A page declares its own requests, so a request\n * has to say which page it came from. The page rides on the URL rather\n * than in the request, leaving the operation set closed.\n */\n private page = \"\";\n\n connectedCallback() {\n this.main = this.querySelector(\"main\")!;\n\n // Scoped in the capture phase, on the way down, so every ancestor that\n // sees the request on its way back up sees it whole. A request that\n // cannot be sent is stopped here, before anything sees it at all.\n this.addEventListener(\n LB_EVENT_NAME,\n (e: Event) => {\n const origin = e.target as Element;\n const request = (e as CustomEvent<HubRequest>).detail;\n if (!scope(request, origin)) e.stopPropagation();\n },\n { capture: true },\n );\n\n this.addEventListener(LB_EVENT_NAME, (e: Event) => {\n const request = (e as CustomEvent<HubRequest>).detail;\n // The element that dispatched the request, not e.currentTarget (the\n // hub itself, since the event bubbles) — pending/error state belongs\n // at the origin so a widget can watch its own attributes. aria-busy\n // states the same fact to assistive technology, which no stylesheet\n // on lb-pending reaches.\n const origin = e.target as Element | null;\n origin?.removeAttribute(ATTR_ERROR);\n origin?.setAttribute(ATTR_PENDING, \"\");\n origin?.setAttribute(\"aria-busy\", \"true\");\n void fetchData(this.page, request)\n .then((data) => {\n applyData(this, data);\n resetGathered(gathered.get(request) ?? []);\n })\n .catch((err) => {\n console.error(`lb-hub: request failed`, err);\n origin?.setAttribute(ATTR_ERROR, \"\");\n })\n .finally(() => {\n origin?.removeAttribute(ATTR_PENDING);\n origin?.removeAttribute(\"aria-busy\");\n });\n });\n\n /**\n * A native element carrying an action. The hub dispatches the bare action\n * from that element as the ordinary bubbling event, rather than sending\n * it directly, so it is scoped exactly as a widget's is and an ancestor\n * widget can still stop it and confirm. A native action button and a\n * hand-written widget therefore produce identical events.\n *\n * A widget carrying an action is left alone — it owns its own interaction\n * and decides what counts as performing it, which for a <select> is a\n * change rather than a click. The test is the same lexical one applyData\n * uses: a hyphen in the tag name.\n */\n this.addEventListener(\"click\", (e: MouseEvent) => {\n const el = (e.target as Element | null)?.closest(`[${ATTR_ACTION}]`);\n if (!el || el.localName.includes(\"-\")) return;\n // A form performs its action on submit, whatever was clicked in it.\n if (el instanceof HTMLFormElement) return;\n // A button beside the cells is pressed once they are done. An element\n // that is a cell, or holds them, has no such moment: a click into its\n // own input would send the row. Deciding when a cell has changed is a\n // widget's.\n const action = el.getAttribute(ATTR_ACTION);\n if (\n (action === ACTION_ROW_INSERT || action === ACTION_ROW_UPDATE) &&\n (el.hasAttribute(ATTR_CELL) || el.querySelector(`[${ATTR_CELL}]`))\n ) {\n console.error(\n `lb-hub: '${action}' on <${el.localName}> is or holds a cell, so ` +\n `a click on it is not sent; put it on a <form>, a button or a widget`,\n el,\n );\n return;\n }\n e.preventDefault();\n dispatchAction(el);\n });\n\n // The same delegation for the event a form owns.\n this.addEventListener(\"submit\", (e: SubmitEvent) => {\n const form = e.target as Element | null;\n if (!form || !form.hasAttribute(ATTR_ACTION)) return;\n e.preventDefault();\n dispatchAction(form);\n });\n\n // Only anchors that opted in. Everything else is a real link.\n this.addEventListener(\"click\", (e: MouseEvent) => {\n const link = (e.target as Element | null)?.closest(`a[${ATTR_NAV_LINK}]`);\n if (!link) return;\n e.preventDefault();\n const path = new URL((link as HTMLAnchorElement).href).pathname;\n history.pushState(null, \"\", path);\n void this.navigate(path);\n });\n\n addEventListener(\"popstate\", () => void this.navigate(location.pathname));\n\n void this.navigate(location.pathname);\n }\n\n /**\n * Navigation is the one operation that replaces host DOM. The host is\n * already in the document, so selecting a page is getElementById() and\n * there is no fetching mechanism on the host channel.\n */\n private async navigate(path: string): Promise<void> {\n const page = pageNameFor(path);\n this.landNavigation(path, page);\n const template = pageHostFor(page);\n if (!template) {\n this.reportUnknownPage();\n this.reveal();\n return;\n }\n this.page = page;\n // Insert the page shell before fetching its data, not after: the\n // template is already in the document, so holding it back behind a\n // network round trip only leaves <main> empty for no reason.\n this.main.replaceChildren(template.content.cloneNode(true));\n this.reveal();\n let data: HubData = {};\n try {\n data = await fetchData(page);\n } catch (err) {\n console.error(`lb-hub: failed to load data for '${page}'`, err);\n }\n applyData(this, data);\n }\n\n /**\n * An app that ships `<body hidden>` (see docs/reference/chrome.md) is\n * asking to stay invisible until there is something coherent to show,\n * rather than flash the chrome before `<main>` has real content. This is\n * a no-op for an app that doesn't use that convention.\n */\n private reveal(): void {\n document.body.hidden = false;\n }\n\n /**\n * The hub's own row: where the page is, in the vocabulary a server\n * answer arrives in, so a chrome displays it the way it displays anything\n * — `lb-row=\"lb-navigation\"` on a subtree, `lb-cell` on what shows a\n * value. Landed before the page host is looked up, so a miss has it too;\n * the unknown-page dialog is one consumer among whatever others the\n * chrome writes.\n *\n * The label is the nav's: the text of the first lb-nav-link anchor whose\n * href names this page. Taken from the nav rather than from a click, so a\n * click, a reload and the back button all land the same label — and a\n * path no anchor names lands an empty one, explicitly, so a consumer is\n * never left showing the previous page's.\n *\n * Landed only where a subtree names it. applyData() warns about a query\n * with no scope, and a chrome that displays no navigation has nothing to\n * be warned about.\n */\n private landNavigation(path: string, page: string): void {\n if (!this.querySelector(`[${ATTR_ROW}=\"${NAV_ROW}\"]`)) return;\n let label = \"\";\n for (const a of this.querySelectorAll<HTMLAnchorElement>(\n `a[${ATTR_NAV_LINK}]`,\n )) {\n if (pageNameFor(new URL(a.href).pathname) !== page) continue;\n label = a.textContent?.trim() ?? \"\";\n break;\n }\n applyData(this, {\n [NAV_ROW]: { [NAV_CELL_LABEL]: label, [NAV_CELL_URI]: path },\n });\n }\n\n /**\n * A path that resolves to no page host is discovered client-side, after a\n * successful 200 — every route gets the same document, so there is no\n * HTTP 404 to report (see docs/tutorials/010-pages-and-navigation.md).\n * lb-unknown-page is optional: an application that declares none gets\n * nothing beyond navigate()'s console.error. The builder guarantees the\n * element it's on is a <dialog> (build/assemble.ts), so this can call\n * showModal() without checking the tag here.\n *\n * Only opens it. What the dialog says it gets from landNavigation(),\n * already run, by naming the hub's row in lb-row like any subtree.\n *\n * Scoped to the hub, like every other lookup here, so that one rule holds\n * without exception: what the hub acts on is inside the hub.\n */\n private reportUnknownPage(): void {\n const dialog = this.querySelector(`[${ATTR_UNKNOWN_PAGE}]`);\n if (!dialog) return;\n const modal = dialog as HTMLDialogElement;\n if (!modal.open) modal.showModal();\n }\n}\n\ncustomElements.define(HUB_TAG_NAME, LbHub);\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"lb-express.d.ts","sourceRoot":"","sources":["../../server/lb-express.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAgB,EAAE,KAAK,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,SAAS,CAAC;AAS7D,OAAO,KAAK,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEtD;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,UAAU,CAAC;AAEtD,wBAAgB,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,UAAU,EAAE,UAAU,GAAG,MAAM,CAkFlE"}
1
+ {"version":3,"file":"lb-express.d.ts","sourceRoot":"","sources":["../../server/lb-express.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAgB,EAAE,KAAK,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,SAAS,CAAC;AAQ7D,OAAO,KAAK,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEtD;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,UAAU,CAAC;AAEtD,wBAAgB,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,UAAU,EAAE,UAAU,GAAG,MAAM,CA2ElE"}
@@ -18,7 +18,7 @@
18
18
  * is reserved.
19
19
  */
20
20
  import express from "express";
21
- import { ACTION_CELL_CHANGE, ACTION_ROW_DELETE, ACTION_ROW_INSERT, ACTION_ROW_UPDATE, LB_ENDPOINT, } from "../core/lb-constants.js";
21
+ import { ACTION_ROW_DELETE, ACTION_ROW_INSERT, ACTION_ROW_UPDATE, LB_ENDPOINT, } from "../core/lb-constants.js";
22
22
  import { isOperation } from "../core/lb-types.js";
23
23
  export function hubRoutes(hub, contextFor) {
24
24
  const router = express.Router();
@@ -47,7 +47,7 @@ export function hubRoutes(hub, contextFor) {
47
47
  // the browser side has something to catch.
48
48
  try {
49
49
  // The reserved prefix is the whole of the discriminant: a name so
50
- // prefixed is one of the four operations, and anything else is a name
50
+ // prefixed is one of the three operations, and anything else is a name
51
51
  // the page declared. Nothing else tells them apart, here or anywhere.
52
52
  if (!isOperation(request)) {
53
53
  const { action, list, row, key, cell, value } = request;
@@ -55,11 +55,6 @@ export function hubRoutes(hub, contextFor) {
55
55
  return;
56
56
  }
57
57
  switch (request.action) {
58
- case ACTION_CELL_CHANGE: {
59
- const { list, key, cell, value } = request;
60
- res.json(await hub.runCellChange(page, { list, key, cell, value }, ctx));
61
- return;
62
- }
63
58
  case ACTION_ROW_DELETE: {
64
59
  const { list, key } = request;
65
60
  res.json(await hub.runRowDelete(page, { list, key }, ctx));
@@ -1 +1 @@
1
- {"version":3,"file":"lb-express.js","sourceRoot":"","sources":["../../server/lb-express.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The Express adapter — the standard hand-off from an Express router to a\n * Hub built by createHub(). Every Hub application wires this same router,\n * which is why it ships from here rather than being copied into each one.\n *\n * Express itself is a peer dependency: nothing outside this file imports it,\n * so an application that hosts Hub some other way never pays for it.\n *\n * One endpoint, and no second:\n *\n * POST /lb?page=<name> an empty body asks for the page's whole query\n * set; a body carrying an action runs that action\n * and answers with its refresh set\n *\n * The page rides on the query string rather than in the body, which is what\n * lets the operation set stay closed. Every hub call is a POST, so an\n * application's own GET routes never collide with this one and no page name\n * is reserved.\n */\n\nimport express, { type Request, type Router } from \"express\";\nimport {\n ACTION_CELL_CHANGE,\n ACTION_ROW_DELETE,\n ACTION_ROW_INSERT,\n ACTION_ROW_UPDATE,\n LB_ENDPOINT,\n} from \"../core/lb-constants.js\";\nimport { isOperation, type HubRequest } from \"../core/lb-types.js\";\nimport type { Hub, HubContext } from \"./lb-server.js\";\n\n/**\n * Building the context is the application's job — it is where an\n * authenticated database handle comes from — so it is injected rather than\n * assumed. Whatever established identity has already run by the time this\n * router is reached; where that happens is the application's ordering\n * decision, not Hub's.\n */\nexport type ContextFor = (req: Request) => HubContext;\n\nexport function hubRoutes(hub: Hub, contextFor: ContextFor): Router {\n const router = express.Router();\n\n // Mounted here rather than on the app so that Hub's need for a parsed\n // body is stated where the routes that need it are.\n router.use(express.json());\n\n router.post(LB_ENDPOINT, async (req, res) => {\n const page = String(req.query.page ?? \"\");\n const request = req.body as HubRequest | undefined;\n const ctx = contextFor(req);\n\n // No body at all is the page load: the browser is asking for this\n // page's whole query set, which is every request it makes that names no\n // action. A body that carries something but not an action is malformed.\n if (request === undefined || Object.keys(request).length === 0) {\n res.json(await hub.dataForPage(page, ctx));\n return;\n }\n\n if (!request.action) {\n console.warn(`loadbare: request without an action`);\n res.status(400).json({});\n return;\n }\n\n // A thrown error is how application code (a query, an action, a CRUD\n // op) says \"this request is invalid\" or hits an unexpected failure —\n // it resolves to a real response rather than an unhandled rejection, so\n // the browser side has something to catch.\n try {\n // The reserved prefix is the whole of the discriminant: a name so\n // prefixed is one of the four operations, and anything else is a name\n // the page declared. Nothing else tells them apart, here or anywhere.\n if (!isOperation(request)) {\n const { action, list, row, key, cell, value } = request;\n res.json(\n await hub.runAction(\n page,\n action,\n { list, row, key, cell, value },\n ctx,\n ),\n );\n return;\n }\n switch (request.action) {\n case ACTION_CELL_CHANGE: {\n const { list, key, cell, value } = request;\n res.json(\n await hub.runCellChange(page, { list, key, cell, value }, ctx),\n );\n return;\n }\n case ACTION_ROW_DELETE: {\n const { list, key } = request;\n res.json(await hub.runRowDelete(page, { list, key }, ctx));\n return;\n }\n case ACTION_ROW_INSERT: {\n const { list, values } = request;\n res.json(await hub.runRowInsert(page, { list, values }, ctx));\n return;\n }\n case ACTION_ROW_UPDATE: {\n const { list, key, values } = request;\n res.json(await hub.runRowUpdate(page, { list, key, values }, ctx));\n return;\n }\n default:\n console.warn(\n `loadbare: operation '${(request as HubRequest).action}' not implemented`,\n );\n res.status(400).json({});\n }\n } catch (err) {\n console.error(`loadbare: '${request.action}' threw`, err);\n res.status(500).json({});\n }\n });\n\n return router;\n}\n"]}
1
+ {"version":3,"file":"lb-express.js","sourceRoot":"","sources":["../../server/lb-express.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The Express adapter — the standard hand-off from an Express router to a\n * Hub built by createHub(). Every Hub application wires this same router,\n * which is why it ships from here rather than being copied into each one.\n *\n * Express itself is a peer dependency: nothing outside this file imports it,\n * so an application that hosts Hub some other way never pays for it.\n *\n * One endpoint, and no second:\n *\n * POST /lb?page=<name> an empty body asks for the page's whole query\n * set; a body carrying an action runs that action\n * and answers with its refresh set\n *\n * The page rides on the query string rather than in the body, which is what\n * lets the operation set stay closed. Every hub call is a POST, so an\n * application's own GET routes never collide with this one and no page name\n * is reserved.\n */\n\nimport express, { type Request, type Router } from \"express\";\nimport {\n ACTION_ROW_DELETE,\n ACTION_ROW_INSERT,\n ACTION_ROW_UPDATE,\n LB_ENDPOINT,\n} from \"../core/lb-constants.js\";\nimport { isOperation, type HubRequest } from \"../core/lb-types.js\";\nimport type { Hub, HubContext } from \"./lb-server.js\";\n\n/**\n * Building the context is the application's job — it is where an\n * authenticated database handle comes from — so it is injected rather than\n * assumed. Whatever established identity has already run by the time this\n * router is reached; where that happens is the application's ordering\n * decision, not Hub's.\n */\nexport type ContextFor = (req: Request) => HubContext;\n\nexport function hubRoutes(hub: Hub, contextFor: ContextFor): Router {\n const router = express.Router();\n\n // Mounted here rather than on the app so that Hub's need for a parsed\n // body is stated where the routes that need it are.\n router.use(express.json());\n\n router.post(LB_ENDPOINT, async (req, res) => {\n const page = String(req.query.page ?? \"\");\n const request = req.body as HubRequest | undefined;\n const ctx = contextFor(req);\n\n // No body at all is the page load: the browser is asking for this\n // page's whole query set, which is every request it makes that names no\n // action. A body that carries something but not an action is malformed.\n if (request === undefined || Object.keys(request).length === 0) {\n res.json(await hub.dataForPage(page, ctx));\n return;\n }\n\n if (!request.action) {\n console.warn(`loadbare: request without an action`);\n res.status(400).json({});\n return;\n }\n\n // A thrown error is how application code (a query, an action, a CRUD\n // op) says \"this request is invalid\" or hits an unexpected failure —\n // it resolves to a real response rather than an unhandled rejection, so\n // the browser side has something to catch.\n try {\n // The reserved prefix is the whole of the discriminant: a name so\n // prefixed is one of the three operations, and anything else is a name\n // the page declared. Nothing else tells them apart, here or anywhere.\n if (!isOperation(request)) {\n const { action, list, row, key, cell, value } = request;\n res.json(\n await hub.runAction(\n page,\n action,\n { list, row, key, cell, value },\n ctx,\n ),\n );\n return;\n }\n switch (request.action) {\n case ACTION_ROW_DELETE: {\n const { list, key } = request;\n res.json(await hub.runRowDelete(page, { list, key }, ctx));\n return;\n }\n case ACTION_ROW_INSERT: {\n const { list, values } = request;\n res.json(await hub.runRowInsert(page, { list, values }, ctx));\n return;\n }\n case ACTION_ROW_UPDATE: {\n const { list, key, values } = request;\n res.json(await hub.runRowUpdate(page, { list, key, values }, ctx));\n return;\n }\n default:\n console.warn(\n `loadbare: operation '${(request as HubRequest).action}' not implemented`,\n );\n res.status(400).json({});\n }\n } catch (err) {\n console.error(`loadbare: '${request.action}' threw`, err);\n res.status(500).json({});\n }\n });\n\n return router;\n}\n"]}
@@ -94,17 +94,13 @@ export interface CrudOp<W> {
94
94
  run: (ctx: HubContext, where: W) => void | HubData | Promise<void | HubData>;
95
95
  refresh: string[];
96
96
  }
97
- export type CellChangeOp = CrudOp<{
98
- key: string;
99
- cell: string;
100
- value: string;
101
- }>;
102
97
  export type RowDeleteOp = CrudOp<{
103
98
  key: string;
104
99
  }>;
105
100
  export type RowInsertOp = CrudOp<{
106
101
  values: Record<string, string>;
107
102
  }>;
103
+ /** `values` holds only the columns being set; see `Hub.runRowUpdate`. */
108
104
  export type RowUpdateOp = CrudOp<{
109
105
  key: string;
110
106
  values: Record<string, string>;
@@ -117,11 +113,10 @@ export type RowUpdateOp = CrudOp<{
117
113
  *
118
114
  * Every key here is the reserved `lb-action` value with the prefix stripped
119
115
  * and the rest camel-cased, so the attribute, the wire field and this key
120
- * are one vocabulary. All four are list operations: each needs a key, and a
116
+ * are one vocabulary. All three are list operations: each needs a key, and a
121
117
  * key exists only on a live row inside a list.
122
118
  */
123
119
  export interface Crud {
124
- cellChange?: CellChangeOp;
125
120
  rowDelete?: RowDeleteOp;
126
121
  rowInsert?: RowInsertOp;
127
122
  rowUpdate?: RowUpdateOp;
@@ -165,13 +160,6 @@ export interface Hub {
165
160
  * set declared with it.
166
161
  */
167
162
  runAction(page: string, name: string, where: Where, ctx: HubContext): Promise<HubData>;
168
- /** Edit one cell: run the list's declared `cellChange`, then its refresh set. */
169
- runCellChange(page: string, where: {
170
- list: string;
171
- key: string;
172
- cell: string;
173
- value: string;
174
- }, ctx: HubContext): Promise<HubData>;
175
163
  /** Drop one row: run the list's declared `rowDelete`, then its refresh set. */
176
164
  runRowDelete(page: string, where: {
177
165
  list: string;
@@ -182,7 +170,12 @@ export interface Hub {
182
170
  list: string;
183
171
  values: Record<string, string>;
184
172
  }, ctx: HubContext): Promise<HubData>;
185
- /** Edit several cells at once: run the list's declared `rowUpdate`, then its refresh set. */
173
+ /**
174
+ * Edit a row: run the list's declared `rowUpdate`, then its refresh set.
175
+ * `values` holds only the columns being set — one from a widget that
176
+ * commits a cell, several from a form — and a column absent from it is left
177
+ * as it is, as an SQL UPDATE leaves it.
178
+ */
186
179
  runRowUpdate(page: string, where: {
187
180
  list: string;
188
181
  key: string;
@@ -1 +1 @@
1
- {"version":3,"file":"lb-server.d.ts","sourceRoot":"","sources":["../../server/lb-server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"lb-server.d.ts","sourceRoot":"","sources":["../../server/lb-server.ts"],"names":[],"mappings":"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"}
@@ -100,9 +100,6 @@ export function createHub(pages) {
100
100
  runAction(page, name, where, ctx) {
101
101
  return settle(page, pages[page]?.requests.actions?.[name], where, ctx, `loadbare: page '${page}' declares no action '${name}'`);
102
102
  },
103
- runCellChange(page, where, ctx) {
104
- return settle(page, pages[page]?.requests.crud?.[where.list]?.cellChange, { key: where.key, cell: where.cell, value: where.value }, ctx, `loadbare: page '${page}' declares no cellChange for '${where.list}'`);
105
- },
106
103
  runRowDelete(page, where, ctx) {
107
104
  return settle(page, pages[page]?.requests.crud?.[where.list]?.rowDelete, { key: where.key }, ctx, `loadbare: page '${page}' declares no rowDelete for '${where.list}'`);
108
105
  },
@@ -1 +1 @@
1
- {"version":3,"file":"lb-server.js","sourceRoot":"","sources":["../../server/lb-server.ts"],"names":[],"mappings":"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","sourcesContent":["// The server-side contract and the engine that runs it.\n//\n// This is the counterpart to core/lb-constants.ts: that file is the\n// vocabulary the browser writes, this one is the vocabulary the application\n// writes. The engine below is the whole of Hub on the server. It ships no\n// HTTP server, no router, and no data layer.\n\nimport type { Patch, Row, HubData, HubResult } from \"../core/lb-types.js\";\nimport { LB_RESERVED_PREFIX } from \"../core/lb-constants.js\";\n\n/**\n * Whatever the application hands the engine for the duration of one request.\n *\n * Hub declares it empty and never reads it. An application fills it in by\n * declaration merging, once, anywhere in its own source:\n *\n * declare module \"@loadbare/app/server\" {\n * interface HubContext {\n * db: Db;\n * }\n * }\n *\n * That is why no type on this page takes a type parameter. The context is a\n * request-scoped handle — an authenticated database connection is the\n * expected case — so it is passed per call rather than held by the engine.\n */\nexport interface HubContext {}\n\n/**\n * A declared query: the request context in, one result out.\n *\n * Cardinality is a property of the name rather than of any one answer, so it\n * is declared here and never inferred from what comes back. One name answers\n * with one shape, always. A page that wants the roster once as a single row\n * and once as a set declares two queries.\n *\n * Write one with `row()` or `list()` below; nothing else builds one.\n */\nexport interface Query {\n readonly kind: \"row\" | \"list\";\n readonly run: (ctx: HubContext) => HubResult | Promise<HubResult>;\n}\n\n/** A query that answers with one row. */\nexport function row(run: (ctx: HubContext) => Row | Promise<Row>): Query {\n return { kind: \"row\", run };\n}\n\n/**\n * A query that answers with the entire set, and therefore also the order. A\n * list reconciles to exactly this: a row whose key is not here is gone.\n *\n * There is no wrapper around the array, because the declaration already said\n * this name answers with rows.\n */\nexport function list(run: (ctx: HubContext) => Row[] | Promise<Row[]>): Query {\n return { kind: \"list\", run };\n}\n\n/**\n * Only what changed, returned from a `crud` run rather than from a query.\n * Rows named here arrive or are updated, keys in `drop` are gone, and\n * everything unnamed is left alone — its contents, and its place in whatever\n * order the widget is keeping.\n *\n * `Array.isArray` is what tells a patch from a whole set, so a patch needs no\n * marker of its own and no column name is reserved to carry one.\n */\nexport function patch(change: { rows?: Row[]; drop?: string[] }): Patch {\n return { ...change };\n}\n\n/** The shape of a `<name>.queries.ts` module. */\nexport type Queries = Record<string, Query>;\n\n/**\n * Where the interaction happened, in the binding vocabulary, plus the one\n * value a control may carry.\n *\n * The browser fills these from attributes it already has. It never names a\n * function — only a name the page declared — which is what keeps this from\n * being an RPC endpoint. A value may ride along because a `<select>` has one\n * and there is nowhere else to put it; it is a string from a control, not an\n * argument list.\n */\nexport interface Where {\n list?: string;\n row?: string;\n key?: string;\n cell?: string;\n value?: string;\n}\n\n/**\n * What an action does, and which queries must re-run once it has.\n *\n * `run` may also return results of its own, which are laid over the refreshed\n * ones. That is how a patch reaches the browser: a query answers for its\n * whole set and cannot know why it was re-run, but the action knows exactly\n * what it changed and can say only that.\n */\nexport interface Action {\n run: (\n ctx: HubContext,\n where: Where,\n ) => void | HubData | Promise<void | HubData>;\n refresh: string[];\n}\n\n/**\n * One CRUD operation on a declared query. Same shape as `Action` — run, then\n * refresh — but `where` carries only what that operation is typed to carry\n * on the wire, rather than the general `Where`.\n */\nexport interface CrudOp<W> {\n run: (ctx: HubContext, where: W) => void | HubData | Promise<void | HubData>;\n refresh: string[];\n}\n\nexport type CellChangeOp = CrudOp<{ key: string; cell: string; value: string }>;\nexport type RowDeleteOp = CrudOp<{ key: string }>;\nexport type RowInsertOp = CrudOp<{ values: Record<string, string> }>;\nexport type RowUpdateOp = CrudOp<{\n key: string;\n values: Record<string, string>;\n}>;\n\n/**\n * The CRUD operations declared for one list, keyed by its name in\n * `Requests.crud`. A name with no entry here permits none of them — the wire\n * cannot reach anything the page has not published, exactly as for a named\n * action.\n *\n * Every key here is the reserved `lb-action` value with the prefix stripped\n * and the rest camel-cased, so the attribute, the wire field and this key\n * are one vocabulary. All four are list operations: each needs a key, and a\n * key exists only on a live row inside a list.\n */\nexport interface Crud {\n cellChange?: CellChangeOp;\n rowDelete?: RowDeleteOp;\n rowInsert?: RowInsertOp;\n rowUpdate?: RowUpdateOp;\n}\n\n/**\n * The shape of a `<name>.requests.ts` module.\n *\n * `onPageEnter` runs once when the page is entered, before any query. It\n * declares no refresh set: entering the page runs the whole query set\n * afterward, so whatever the hook changed is already in the response.\n *\n * `actions` names what this page may be asked to do. A name the page did not\n * declare is refused, so the wire cannot reach anything the page has not\n * published.\n *\n * `crud` is the same rule for the typed CRUD operations, keyed by the list\n * they operate on rather than by a declared name — there is nothing to name,\n * since the row's own binding says what it is.\n */\nexport interface Requests {\n onPageEnter?: (ctx: HubContext) => void | Promise<void>;\n actions?: Record<string, Action>;\n crud?: Record<string, Crud>;\n}\n\n/** A page is three files sharing a basename; two of them are these. */\nexport interface Page {\n queries: Queries;\n requests: Requests;\n}\n\n/**\n * The page registry. The builder populates this automatically from every\n * `.requests.ts`/`.queries.ts` pair it discovers — see docs/reference/builder.md,\n * \"Generating the server-side page registry\".\n */\nexport type Pages = Record<string, Page>;\n\nexport interface Hub {\n /** Entering a page: run its onPageEnter hook, then all of its queries. */\n dataForPage(page: string, ctx: HubContext): Promise<HubData>;\n\n /**\n * An action: run what the page declared under that name, then the refresh\n * set declared with it.\n */\n runAction(\n page: string,\n name: string,\n where: Where,\n ctx: HubContext,\n ): Promise<HubData>;\n\n /** Edit one cell: run the list's declared `cellChange`, then its refresh set. */\n runCellChange(\n page: string,\n where: { list: string; key: string; cell: string; value: string },\n ctx: HubContext,\n ): Promise<HubData>;\n\n /** Drop one row: run the list's declared `rowDelete`, then its refresh set. */\n runRowDelete(\n page: string,\n where: { list: string; key: string },\n ctx: HubContext,\n ): Promise<HubData>;\n\n /** Add one row: run the list's declared `rowInsert`, then its refresh set. */\n runRowInsert(\n page: string,\n where: { list: string; values: Record<string, string> },\n ctx: HubContext,\n ): Promise<HubData>;\n\n /** Edit several cells at once: run the list's declared `rowUpdate`, then its refresh set. */\n runRowUpdate(\n page: string,\n where: { list: string; key: string; values: Record<string, string> },\n ctx: HubContext,\n ): Promise<HubData>;\n}\n\n/**\n * Build the engine over a set of pages. The pages are fixed at startup; the\n * context is not, and arrives with each call.\n */\nexport function createHub(pages: Pages): Hub {\n // Names beginning with the reserved prefix are Loadbare's — the hub's\n // own query, the CRUD operations — so an application cannot declare one.\n // Refused at startup, because a name is a fact about the page and not\n // about any request.\n for (const [page, entry] of Object.entries(pages)) {\n const declared = [\n ...Object.keys(entry.queries),\n ...Object.keys(entry.requests.actions ?? {}),\n ];\n for (const name of declared) {\n if (name.startsWith(LB_RESERVED_PREFIX)) {\n throw new Error(\n `loadbare: page '${page}' declares '${name}', but names ` +\n `beginning with '${LB_RESERVED_PREFIX}' are reserved`,\n );\n }\n }\n }\n\n async function run(\n page: string,\n names: string[],\n ctx: HubContext,\n ): Promise<HubData> {\n const data: HubData = {};\n for (const name of names) {\n const query = pages[page]?.queries[name];\n if (!query) {\n console.warn(`loadbare: page '${page}' declares no query '${name}'`);\n continue;\n }\n const result = await query.run(ctx);\n // Cardinality is declared, so an answer that disagrees is a mistake in\n // the query rather than a case to handle. Refused here, because a page\n // is better off missing one name than showing the wrong shape for it.\n if (Array.isArray(result) !== (query.kind === \"list\")) {\n console.warn(\n `loadbare: query '${name}' is declared ${query.kind} but answered ` +\n `with ${Array.isArray(result) ? \"rows\" : \"one row\"}`,\n );\n continue;\n }\n data[name] = result;\n }\n return data;\n }\n\n /**\n * Shared by every operation kind: run what was declared, then its refresh\n * set against the same context, laying what the operation itself stated\n * over the refreshed queries — the narrower answer wins because it is the\n * one that knows what actually changed.\n */\n async function settle<W>(\n page: string,\n declared: CrudOp<W> | Action | undefined,\n where: W,\n ctx: HubContext,\n notFound: string,\n ): Promise<HubData> {\n if (!declared) {\n console.warn(notFound);\n return {};\n }\n const stated = await declared.run(ctx, where as never);\n return { ...(await run(page, declared.refresh, ctx)), ...(stated ?? {}) };\n }\n\n return {\n async dataForPage(page, ctx) {\n const entry = pages[page];\n if (!entry) {\n console.warn(`loadbare: no page '${page}'`);\n return {};\n }\n await entry.requests.onPageEnter?.(ctx);\n return run(page, Object.keys(entry.queries), ctx);\n },\n\n runAction(page, name, where, ctx) {\n return settle(\n page,\n pages[page]?.requests.actions?.[name],\n where,\n ctx,\n `loadbare: page '${page}' declares no action '${name}'`,\n );\n },\n\n runCellChange(page, where, ctx) {\n return settle(\n page,\n pages[page]?.requests.crud?.[where.list]?.cellChange,\n { key: where.key, cell: where.cell, value: where.value },\n ctx,\n `loadbare: page '${page}' declares no cellChange for '${where.list}'`,\n );\n },\n\n runRowDelete(page, where, ctx) {\n return settle(\n page,\n pages[page]?.requests.crud?.[where.list]?.rowDelete,\n { key: where.key },\n ctx,\n `loadbare: page '${page}' declares no rowDelete for '${where.list}'`,\n );\n },\n\n runRowInsert(page, where, ctx) {\n return settle(\n page,\n pages[page]?.requests.crud?.[where.list]?.rowInsert,\n { values: where.values },\n ctx,\n `loadbare: page '${page}' declares no rowInsert for '${where.list}'`,\n );\n },\n\n runRowUpdate(page, where, ctx) {\n return settle(\n page,\n pages[page]?.requests.crud?.[where.list]?.rowUpdate,\n { key: where.key, values: where.values },\n ctx,\n `loadbare: page '${page}' declares no rowUpdate for '${where.list}'`,\n );\n },\n };\n}\n"]}
1
+ {"version":3,"file":"lb-server.js","sourceRoot":"","sources":["../../server/lb-server.ts"],"names":[],"mappings":"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","sourcesContent":["// The server-side contract and the engine that runs it.\n//\n// This is the counterpart to core/lb-constants.ts: that file is the\n// vocabulary the browser writes, this one is the vocabulary the application\n// writes. The engine below is the whole of Hub on the server. It ships no\n// HTTP server, no router, and no data layer.\n\nimport type { Patch, Row, HubData, HubResult } from \"../core/lb-types.js\";\nimport { LB_RESERVED_PREFIX } from \"../core/lb-constants.js\";\n\n/**\n * Whatever the application hands the engine for the duration of one request.\n *\n * Hub declares it empty and never reads it. An application fills it in by\n * declaration merging, once, anywhere in its own source:\n *\n * declare module \"@loadbare/app/server\" {\n * interface HubContext {\n * db: Db;\n * }\n * }\n *\n * That is why no type on this page takes a type parameter. The context is a\n * request-scoped handle — an authenticated database connection is the\n * expected case — so it is passed per call rather than held by the engine.\n */\nexport interface HubContext {}\n\n/**\n * A declared query: the request context in, one result out.\n *\n * Cardinality is a property of the name rather than of any one answer, so it\n * is declared here and never inferred from what comes back. One name answers\n * with one shape, always. A page that wants the roster once as a single row\n * and once as a set declares two queries.\n *\n * Write one with `row()` or `list()` below; nothing else builds one.\n */\nexport interface Query {\n readonly kind: \"row\" | \"list\";\n readonly run: (ctx: HubContext) => HubResult | Promise<HubResult>;\n}\n\n/** A query that answers with one row. */\nexport function row(run: (ctx: HubContext) => Row | Promise<Row>): Query {\n return { kind: \"row\", run };\n}\n\n/**\n * A query that answers with the entire set, and therefore also the order. A\n * list reconciles to exactly this: a row whose key is not here is gone.\n *\n * There is no wrapper around the array, because the declaration already said\n * this name answers with rows.\n */\nexport function list(run: (ctx: HubContext) => Row[] | Promise<Row[]>): Query {\n return { kind: \"list\", run };\n}\n\n/**\n * Only what changed, returned from a `crud` run rather than from a query.\n * Rows named here arrive or are updated, keys in `drop` are gone, and\n * everything unnamed is left alone — its contents, and its place in whatever\n * order the widget is keeping.\n *\n * `Array.isArray` is what tells a patch from a whole set, so a patch needs no\n * marker of its own and no column name is reserved to carry one.\n */\nexport function patch(change: { rows?: Row[]; drop?: string[] }): Patch {\n return { ...change };\n}\n\n/** The shape of a `<name>.queries.ts` module. */\nexport type Queries = Record<string, Query>;\n\n/**\n * Where the interaction happened, in the binding vocabulary, plus the one\n * value a control may carry.\n *\n * The browser fills these from attributes it already has. It never names a\n * function — only a name the page declared — which is what keeps this from\n * being an RPC endpoint. A value may ride along because a `<select>` has one\n * and there is nowhere else to put it; it is a string from a control, not an\n * argument list.\n */\nexport interface Where {\n list?: string;\n row?: string;\n key?: string;\n cell?: string;\n value?: string;\n}\n\n/**\n * What an action does, and which queries must re-run once it has.\n *\n * `run` may also return results of its own, which are laid over the refreshed\n * ones. That is how a patch reaches the browser: a query answers for its\n * whole set and cannot know why it was re-run, but the action knows exactly\n * what it changed and can say only that.\n */\nexport interface Action {\n run: (\n ctx: HubContext,\n where: Where,\n ) => void | HubData | Promise<void | HubData>;\n refresh: string[];\n}\n\n/**\n * One CRUD operation on a declared query. Same shape as `Action` — run, then\n * refresh — but `where` carries only what that operation is typed to carry\n * on the wire, rather than the general `Where`.\n */\nexport interface CrudOp<W> {\n run: (ctx: HubContext, where: W) => void | HubData | Promise<void | HubData>;\n refresh: string[];\n}\n\nexport type RowDeleteOp = CrudOp<{ key: string }>;\nexport type RowInsertOp = CrudOp<{ values: Record<string, string> }>;\n/** `values` holds only the columns being set; see `Hub.runRowUpdate`. */\nexport type RowUpdateOp = CrudOp<{\n key: string;\n values: Record<string, string>;\n}>;\n\n/**\n * The CRUD operations declared for one list, keyed by its name in\n * `Requests.crud`. A name with no entry here permits none of them — the wire\n * cannot reach anything the page has not published, exactly as for a named\n * action.\n *\n * Every key here is the reserved `lb-action` value with the prefix stripped\n * and the rest camel-cased, so the attribute, the wire field and this key\n * are one vocabulary. All three are list operations: each needs a key, and a\n * key exists only on a live row inside a list.\n */\nexport interface Crud {\n rowDelete?: RowDeleteOp;\n rowInsert?: RowInsertOp;\n rowUpdate?: RowUpdateOp;\n}\n\n/**\n * The shape of a `<name>.requests.ts` module.\n *\n * `onPageEnter` runs once when the page is entered, before any query. It\n * declares no refresh set: entering the page runs the whole query set\n * afterward, so whatever the hook changed is already in the response.\n *\n * `actions` names what this page may be asked to do. A name the page did not\n * declare is refused, so the wire cannot reach anything the page has not\n * published.\n *\n * `crud` is the same rule for the typed CRUD operations, keyed by the list\n * they operate on rather than by a declared name — there is nothing to name,\n * since the row's own binding says what it is.\n */\nexport interface Requests {\n onPageEnter?: (ctx: HubContext) => void | Promise<void>;\n actions?: Record<string, Action>;\n crud?: Record<string, Crud>;\n}\n\n/** A page is three files sharing a basename; two of them are these. */\nexport interface Page {\n queries: Queries;\n requests: Requests;\n}\n\n/**\n * The page registry. The builder populates this automatically from every\n * `.requests.ts`/`.queries.ts` pair it discovers — see docs/reference/builder.md,\n * \"Generating the server-side page registry\".\n */\nexport type Pages = Record<string, Page>;\n\nexport interface Hub {\n /** Entering a page: run its onPageEnter hook, then all of its queries. */\n dataForPage(page: string, ctx: HubContext): Promise<HubData>;\n\n /**\n * An action: run what the page declared under that name, then the refresh\n * set declared with it.\n */\n runAction(\n page: string,\n name: string,\n where: Where,\n ctx: HubContext,\n ): Promise<HubData>;\n\n /** Drop one row: run the list's declared `rowDelete`, then its refresh set. */\n runRowDelete(\n page: string,\n where: { list: string; key: string },\n ctx: HubContext,\n ): Promise<HubData>;\n\n /** Add one row: run the list's declared `rowInsert`, then its refresh set. */\n runRowInsert(\n page: string,\n where: { list: string; values: Record<string, string> },\n ctx: HubContext,\n ): Promise<HubData>;\n\n /**\n * Edit a row: run the list's declared `rowUpdate`, then its refresh set.\n * `values` holds only the columns being set — one from a widget that\n * commits a cell, several from a form — and a column absent from it is left\n * as it is, as an SQL UPDATE leaves it.\n */\n runRowUpdate(\n page: string,\n where: { list: string; key: string; values: Record<string, string> },\n ctx: HubContext,\n ): Promise<HubData>;\n}\n\n/**\n * Build the engine over a set of pages. The pages are fixed at startup; the\n * context is not, and arrives with each call.\n */\nexport function createHub(pages: Pages): Hub {\n // Names beginning with the reserved prefix are Loadbare's — the hub's\n // own query, the CRUD operations — so an application cannot declare one.\n // Refused at startup, because a name is a fact about the page and not\n // about any request.\n for (const [page, entry] of Object.entries(pages)) {\n const declared = [\n ...Object.keys(entry.queries),\n ...Object.keys(entry.requests.actions ?? {}),\n ];\n for (const name of declared) {\n if (name.startsWith(LB_RESERVED_PREFIX)) {\n throw new Error(\n `loadbare: page '${page}' declares '${name}', but names ` +\n `beginning with '${LB_RESERVED_PREFIX}' are reserved`,\n );\n }\n }\n }\n\n async function run(\n page: string,\n names: string[],\n ctx: HubContext,\n ): Promise<HubData> {\n const data: HubData = {};\n for (const name of names) {\n const query = pages[page]?.queries[name];\n if (!query) {\n console.warn(`loadbare: page '${page}' declares no query '${name}'`);\n continue;\n }\n const result = await query.run(ctx);\n // Cardinality is declared, so an answer that disagrees is a mistake in\n // the query rather than a case to handle. Refused here, because a page\n // is better off missing one name than showing the wrong shape for it.\n if (Array.isArray(result) !== (query.kind === \"list\")) {\n console.warn(\n `loadbare: query '${name}' is declared ${query.kind} but answered ` +\n `with ${Array.isArray(result) ? \"rows\" : \"one row\"}`,\n );\n continue;\n }\n data[name] = result;\n }\n return data;\n }\n\n /**\n * Shared by every operation kind: run what was declared, then its refresh\n * set against the same context, laying what the operation itself stated\n * over the refreshed queries — the narrower answer wins because it is the\n * one that knows what actually changed.\n */\n async function settle<W>(\n page: string,\n declared: CrudOp<W> | Action | undefined,\n where: W,\n ctx: HubContext,\n notFound: string,\n ): Promise<HubData> {\n if (!declared) {\n console.warn(notFound);\n return {};\n }\n const stated = await declared.run(ctx, where as never);\n return { ...(await run(page, declared.refresh, ctx)), ...(stated ?? {}) };\n }\n\n return {\n async dataForPage(page, ctx) {\n const entry = pages[page];\n if (!entry) {\n console.warn(`loadbare: no page '${page}'`);\n return {};\n }\n await entry.requests.onPageEnter?.(ctx);\n return run(page, Object.keys(entry.queries), ctx);\n },\n\n runAction(page, name, where, ctx) {\n return settle(\n page,\n pages[page]?.requests.actions?.[name],\n where,\n ctx,\n `loadbare: page '${page}' declares no action '${name}'`,\n );\n },\n\n runRowDelete(page, where, ctx) {\n return settle(\n page,\n pages[page]?.requests.crud?.[where.list]?.rowDelete,\n { key: where.key },\n ctx,\n `loadbare: page '${page}' declares no rowDelete for '${where.list}'`,\n );\n },\n\n runRowInsert(page, where, ctx) {\n return settle(\n page,\n pages[page]?.requests.crud?.[where.list]?.rowInsert,\n { values: where.values },\n ctx,\n `loadbare: page '${page}' declares no rowInsert for '${where.list}'`,\n );\n },\n\n runRowUpdate(page, where, ctx) {\n return settle(\n page,\n pages[page]?.requests.crud?.[where.list]?.rowUpdate,\n { key: where.key, values: where.values },\n ctx,\n `loadbare: page '${page}' declares no rowUpdate for '${where.list}'`,\n );\n },\n };\n}\n"]}