@loadbare/app 0.12.0 → 0.14.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 (56) hide show
  1. package/dist/build/assemble.d.ts.map +1 -1
  2. package/dist/build/assemble.js +87 -1
  3. package/dist/build/assemble.js.map +1 -1
  4. package/dist/build/cli.js +2 -1
  5. package/dist/build/cli.js.map +1 -1
  6. package/dist/build/elements.d.ts +9 -1
  7. package/dist/build/elements.d.ts.map +1 -1
  8. package/dist/build/elements.js +50 -0
  9. package/dist/build/elements.js.map +1 -1
  10. package/dist/build/origins.d.ts +2 -0
  11. package/dist/build/origins.d.ts.map +1 -1
  12. package/dist/build/origins.js +1 -1
  13. package/dist/build/origins.js.map +1 -1
  14. package/dist/core/lb-constants.d.ts +34 -1
  15. package/dist/core/lb-constants.d.ts.map +1 -1
  16. package/dist/core/lb-constants.js +105 -0
  17. package/dist/core/lb-constants.js.map +1 -1
  18. package/dist/core/lb-types.d.ts +74 -18
  19. package/dist/core/lb-types.d.ts.map +1 -1
  20. package/dist/core/lb-types.js +74 -11
  21. package/dist/core/lb-types.js.map +1 -1
  22. package/dist/hub/lb-apply.d.ts +57 -18
  23. package/dist/hub/lb-apply.d.ts.map +1 -1
  24. package/dist/hub/lb-apply.js +661 -63
  25. package/dist/hub/lb-apply.js.map +1 -1
  26. package/dist/hub/lb-hub.browser.d.ts.map +1 -1
  27. package/dist/hub/lb-hub.browser.js +400 -33
  28. package/dist/hub/lb-hub.browser.js.map +1 -1
  29. package/dist/server/lb-express.d.ts +19 -7
  30. package/dist/server/lb-express.d.ts.map +1 -1
  31. package/dist/server/lb-express.js +62 -26
  32. package/dist/server/lb-express.js.map +1 -1
  33. package/dist/server/lb-server.d.ts +80 -11
  34. package/dist/server/lb-server.d.ts.map +1 -1
  35. package/dist/server/lb-server.js +185 -33
  36. package/dist/server/lb-server.js.map +1 -1
  37. package/docs/TECHREF-1.0.md +584 -111
  38. package/docs/comparison.md +17 -13
  39. package/docs/possible-ideas.md +308 -0
  40. package/docs/reference/chrome.md +7 -1
  41. package/docs/reference/custom-elements.md +134 -39
  42. package/docs/reference/data-binding.md +405 -17
  43. package/docs/reference/page-files.md +101 -19
  44. package/docs/reference/server.md +24 -0
  45. package/docs/reference/widgets.md +118 -25
  46. package/docs/roadmap.md +17 -6
  47. package/docs/testing.md +21 -10
  48. package/package.json +2 -3
  49. package/skills/loadbare-app/SKILL.md +92 -30
  50. package/skills/loadbare-app/references/TECHREF-1.0.md +584 -111
  51. package/skills/loadbare-app/references/chrome.md +7 -1
  52. package/skills/loadbare-app/references/custom-elements.md +134 -39
  53. package/skills/loadbare-app/references/data-binding.md +405 -17
  54. package/skills/loadbare-app/references/page-files.md +101 -19
  55. package/skills/loadbare-app/references/server.md +24 -0
  56. package/skills/loadbare-app/references/widgets.md +118 -25
@@ -1,4 +1,4 @@
1
- import type { HubData, HubRequest, HubResponse, HubResult, Kind, Patch, RequestFields, Row } from "../core/lb-types.js";
1
+ import type { HubData, HubRequest, HubResponse, HubResult, Kind, GatheredRow, HubRefusal, Patch, RequestFields, Row } from "../core/lb-types.js";
2
2
  /**
3
3
  * Whatever the application hands the engine for the duration of one request.
4
4
  *
@@ -34,15 +34,37 @@ export interface Query {
34
34
  readonly kind: Kind;
35
35
  readonly key: string;
36
36
  readonly run: (ctx: HubContext) => HubResult | Promise<HubResult>;
37
+ /** A `rows` query's order, as `rows()` declared it. */
38
+ readonly order?: string;
39
+ /** Whether a `rows` query reads the order parm and sorts by it itself. */
40
+ readonly serverSortedByUrl?: boolean;
41
+ }
42
+ /**
43
+ * What a `rows` query declares about its order.
44
+ *
45
+ * `order` is the order the hub places its rows by: columns separated by
46
+ * commas, each descending when it begins with `-`, as in
47
+ * `category_order,-opened`. The user's order, carried in the query parm
48
+ * `lb-order-<query>`, replaces it. With neither, rows show in the order the
49
+ * query answered them in.
50
+ *
51
+ * `serverSortedByUrl` says the query reads that parm and sorts by it itself,
52
+ * which a query that pages or limits its rows must: a change of the user's
53
+ * order then runs the query again. Without it, the hub sorts the rows it
54
+ * already has, with no round trip.
55
+ */
56
+ export interface RowsOptions {
57
+ order?: string;
58
+ serverSortedByUrl?: boolean;
37
59
  }
38
60
  /** A query that answers with one row, identified by its `key` column. */
39
61
  export declare function row(key: string, run: (ctx: HubContext) => Row | Promise<Row>): Query;
40
62
  /**
41
- * A query that answers with all its rows, and therefore also their order.
42
- * Rows land by their `key` column: a row whose key is not in the answer is
43
- * gone.
63
+ * A query that answers with all its rows. Rows land by their `key` column: a
64
+ * row whose key is not in the answer is gone. The hub places them by the
65
+ * order in `options`, or in the order they arrived when there is none.
44
66
  */
45
- export declare function rows(key: string, run: (ctx: HubContext) => Row[] | Promise<Row[]>): Query;
67
+ export declare function rows(key: string, run: (ctx: HubContext) => Row[] | Promise<Row[]>, options?: RowsOptions): Query;
46
68
  /**
47
69
  * Only what changed, returned from a handler for a `rows` query. Rows named
48
70
  * here are added or updated, keys in `drop` are removed, and everything
@@ -71,6 +93,29 @@ export declare function patch(change: {
71
93
  * This is Post/Redirect/Get without the redirect.
72
94
  */
73
95
  export declare function url(columns: Record<string, string>): HubData;
96
+ /**
97
+ * A handler's refusal: thrown, and answered to the browser with the words
98
+ * it carries. Every other error a handler throws reaches the browser as a
99
+ * failure with nothing in it.
100
+ */
101
+ export declare class Refusal extends Error {
102
+ readonly refusal: HubRefusal;
103
+ constructor(refusal: HubRefusal);
104
+ }
105
+ /**
106
+ * Refuse a request, in words written for the user. Throw what it returns:
107
+ *
108
+ * throw refusal("That series already has paychecks entered.");
109
+ * throw refusal({ amount: "Must be more than zero" });
110
+ * throw refusal("Two fields need attention.", { amount: "…" });
111
+ *
112
+ * A message alone is for the request as a whole; columns name the controls
113
+ * the request gathered, and each message is shown on its control. Thrown
114
+ * rather than returned, so a refusal can come from anywhere the handler
115
+ * calls, and whatever the application has open rolls back as for any
116
+ * throw.
117
+ */
118
+ export declare function refusal(message: string | Record<string, string>, columns?: Record<string, string>): Refusal;
74
119
  /** The shape of a `<stub>.queries.ts` module. */
75
120
  export type Queries = Record<string, Query>;
76
121
  /**
@@ -85,14 +130,21 @@ export interface Handler<R = RequestFields> {
85
130
  run: (ctx: HubContext, request: R) => void | HubData | Promise<void | HubData>;
86
131
  refresh: string[];
87
132
  }
133
+ /**
134
+ * `rows` holds the rows of the queries nested in the form the request came
135
+ * from, by query name, each with its own values: a master's details,
136
+ * written with it.
137
+ */
88
138
  export type RowInsertHandler = Handler<{
89
139
  key?: string;
90
140
  values: Record<string, string>;
141
+ rows?: Record<string, GatheredRow[]>;
91
142
  }>;
92
143
  /** `values` holds only the columns being set, as an SQL UPDATE sets them. */
93
144
  export type RowUpdateHandler = Handler<{
94
145
  key: string;
95
146
  values: Record<string, string>;
147
+ rows?: Record<string, GatheredRow[]>;
96
148
  }>;
97
149
  export type RowDeleteHandler = Handler<{
98
150
  key: string;
@@ -115,7 +167,11 @@ export interface Crud {
115
167
  *
116
168
  * `onPageEnter` runs when the page loads, before its queries. It declares no
117
169
  * refresh set: the page's queries all run afterward, so whatever it changed
118
- * is already in the response.
170
+ * is already in the response. It may return `url()` instead, to move the
171
+ * page to other query parms or another page before anything shows, as an
172
+ * application restoring what the user last had on screen does. The page
173
+ * then loads there in the same round trip, and `onPageEnter` running for
174
+ * that load is not followed again.
119
175
  *
120
176
  * `handlers` holds the page's declared requests, by name. A name the page
121
177
  * did not declare is refused, so the wire cannot reach anything the page has
@@ -124,7 +180,7 @@ export interface Crud {
124
180
  * `crud` holds what the requests Loadbare provides run, by query name.
125
181
  */
126
182
  export interface Requests {
127
- onPageEnter?: (ctx: HubContext) => void | Promise<void>;
183
+ onPageEnter?: (ctx: HubContext) => void | HubData | Promise<void | HubData>;
128
184
  handlers?: Record<string, Handler>;
129
185
  crud?: Record<string, Crud>;
130
186
  }
@@ -138,15 +194,28 @@ export interface Page {
138
194
  * `.queries.ts` it discovers — see docs/reference/builder.md.
139
195
  */
140
196
  export type Pages = Record<string, Page>;
197
+ /**
198
+ * How a page load treats a `url()` its `onPageEnter` returned.
199
+ *
200
+ * With `follow` false it is ignored and the queries run, as they must for a
201
+ * load that is itself at a URL something else moved to. With `parms`, the
202
+ * query parms the page is being loaded at, one that leads to the page and
203
+ * parms already in hand is ignored the same way, so the page loads once.
204
+ */
205
+ export interface PageLoadOptions {
206
+ follow?: boolean;
207
+ parms?: URLSearchParams;
208
+ }
141
209
  /**
142
210
  * The engine. Both calls answer with response items. A request whose
143
- * handler returned `url()` answers with the `lb-url` item alone: the caller
144
- * builds a context at the new URL, calls `dataForPage` with it, and sends
145
- * the load beside the item. `hubRoutes` does exactly that.
211
+ * handler returned `url()`, and a page load whose `onPageEnter` did, answer
212
+ * with the `lb-url` item alone: the caller builds a context at the new URL,
213
+ * calls `dataForPage` with it and `follow` false, and sends the load beside the
214
+ * item. `hubRoutes` does exactly that.
146
215
  */
147
216
  export interface Hub {
148
217
  /** A page load: its `onPageEnter`, then all of its queries. */
149
- dataForPage(page: string, ctx: HubContext): Promise<HubResponse>;
218
+ dataForPage(page: string, ctx: HubContext, options?: PageLoadOptions): Promise<HubResponse>;
150
219
  /** A request: run the handler its name picks, then the refresh set. */
151
220
  runRequest(page: string, request: HubRequest, ctx: HubContext): Promise<HubResponse>;
152
221
  }
@@ -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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"}
@@ -4,19 +4,25 @@
4
4
  // vocabulary the browser writes, this one is the vocabulary the application
5
5
  // writes. The engine below is the whole of Loadbare on the server. It ships
6
6
  // no HTTP server, no router, and no data layer.
7
- import { isRowRequest } from "../core/lb-types.js";
7
+ import { isRowRequest, nextLocation, parseOrder } from "../core/lb-types.js";
8
8
  import { KIND_ROW, KIND_ROWS, LB_RESERVED_PREFIX, REQUEST_ROW_DELETE, REQUEST_ROW_INSERT, REQUEST_ROW_UPDATE, URL_COLUMN_PATH, URL_QUERY, } from "../core/lb-constants.js";
9
9
  /** A query that answers with one row, identified by its `key` column. */
10
10
  export function row(key, run) {
11
11
  return { kind: KIND_ROW, key, run };
12
12
  }
13
13
  /**
14
- * A query that answers with all its rows, and therefore also their order.
15
- * Rows land by their `key` column: a row whose key is not in the answer is
16
- * gone.
14
+ * A query that answers with all its rows. Rows land by their `key` column: a
15
+ * row whose key is not in the answer is gone. The hub places them by the
16
+ * order in `options`, or in the order they arrived when there is none.
17
17
  */
18
- export function rows(key, run) {
19
- return { kind: KIND_ROWS, key, run };
18
+ export function rows(key, run, options = {}) {
19
+ return {
20
+ kind: KIND_ROWS,
21
+ key,
22
+ run,
23
+ ...(options.order !== undefined && { order: options.order }),
24
+ ...(options.serverSortedByUrl && { serverSortedByUrl: true }),
25
+ };
20
26
  }
21
27
  /**
22
28
  * Only what changed, returned from a handler for a `rows` query. Rows named
@@ -54,6 +60,37 @@ const patches = new WeakSet();
54
60
  export function url(columns) {
55
61
  return { [URL_QUERY]: { ...columns } };
56
62
  }
63
+ /**
64
+ * A handler's refusal: thrown, and answered to the browser with the words
65
+ * it carries. Every other error a handler throws reaches the browser as a
66
+ * failure with nothing in it.
67
+ */
68
+ export class Refusal extends Error {
69
+ refusal;
70
+ constructor(refusal) {
71
+ super(refusal.message ?? "refused");
72
+ this.name = "Refusal";
73
+ this.refusal = refusal;
74
+ }
75
+ }
76
+ /**
77
+ * Refuse a request, in words written for the user. Throw what it returns:
78
+ *
79
+ * throw refusal("That series already has paychecks entered.");
80
+ * throw refusal({ amount: "Must be more than zero" });
81
+ * throw refusal("Two fields need attention.", { amount: "…" });
82
+ *
83
+ * A message alone is for the request as a whole; columns name the controls
84
+ * the request gathered, and each message is shown on its control. Thrown
85
+ * rather than returned, so a refusal can come from anywhere the handler
86
+ * calls, and whatever the application has open rolls back as for any
87
+ * throw.
88
+ */
89
+ export function refusal(message, columns) {
90
+ return typeof message === "string"
91
+ ? new Refusal({ message, ...(columns && { columns }) })
92
+ : new Refusal({ columns: message });
93
+ }
57
94
  /** The camel-cased `Crud` key of a request Loadbare provides. */
58
95
  const CRUD_KEYS = {
59
96
  [REQUEST_ROW_INSERT]: "rowInsert",
@@ -82,12 +119,24 @@ export function createHub(pages) {
82
119
  }
83
120
  }
84
121
  // An update or a delete names its row by key, so its handler always
85
- // knows which rows changed and can answer with a patch; removing a row
86
- // never reorders the rest. Refreshing the whole set instead sends every
87
- // row and places every row again, which moves the element the user is
88
- // in. There is no case where that is the answer to either, so it is
89
- // refused rather than warned about. An insert is not: where its row
90
- // goes depends on the markup, which the server cannot see.
122
+ // knows which rows changed and can answer with a patch, and the hub
123
+ // places what the patch names by the query's order. Refreshing the whole
124
+ // set instead sends every row to say one thing. There is no case where
125
+ // that is the answer to either, so it is refused rather than warned
126
+ // about. An insert is not: its key exists only once the row does, and a
127
+ // handler may leave the query to say so.
128
+ // An order is a fact about the query, so a malformed one is refused at
129
+ // startup, as a reserved name is.
130
+ for (const [name, query] of Object.entries(entry.queries)) {
131
+ if (query.order === undefined)
132
+ continue;
133
+ try {
134
+ parseOrder(query.order);
135
+ }
136
+ catch (err) {
137
+ throw new Error(`loadbare: page '${page}' query '${name}': ${err.message}`);
138
+ }
139
+ }
91
140
  for (const [name, crud] of Object.entries(entry.requests.crud ?? {})) {
92
141
  if (entry.queries[name]?.kind !== KIND_ROWS)
93
142
  continue;
@@ -126,11 +175,15 @@ export function createHub(pages) {
126
175
  }
127
176
  item.row = result;
128
177
  }
129
- else if (Array.isArray(result)) {
130
- item.rows = result;
131
- }
132
178
  else {
133
- item.patch = result;
179
+ if (Array.isArray(result))
180
+ item.rows = result;
181
+ else
182
+ item.patch = result;
183
+ if (query.order !== undefined)
184
+ item.order = query.order;
185
+ if (query.serverSortedByUrl)
186
+ item.serverSortedByUrl = true;
134
187
  }
135
188
  out.push(item);
136
189
  }
@@ -170,21 +223,43 @@ export function createHub(pages) {
170
223
  return [];
171
224
  }
172
225
  const { [URL_QUERY]: moved, ...stated } = (await handler.run(ctx, request)) ?? {};
173
- if (moved !== undefined) {
174
- const refused = refusal(moved);
175
- if (refused === undefined) {
176
- return [
177
- {
178
- query: URL_QUERY,
179
- kind: KIND_ROW,
180
- key: URL_COLUMN_PATH,
181
- row: moved,
182
- },
183
- ];
226
+ const item = urlItem(page, moved);
227
+ if (item)
228
+ return [item];
229
+ return items(page, {
230
+ ...(await run(page, handler.refresh, ctx)),
231
+ ...stated,
232
+ });
233
+ }
234
+ /**
235
+ * An update or a delete for several rows: the page's `crud` entry runs once
236
+ * per key, as it would for each row alone, so an application writes
237
+ * nothing more to allow it. What each run returns is laid together, a
238
+ * patch onto a patch, and the refresh set runs once.
239
+ *
240
+ * A run that returns `url()` is reported and its row ignored: a URL names
241
+ * one view, and several rows cannot each choose it.
242
+ */
243
+ async function eachKey(page, name, fields, ctx) {
244
+ const { keys, ...one } = fields;
245
+ const query = fields.query ?? "";
246
+ const handler = pages[page]?.requests.crud?.[query]?.[CRUD_KEYS[name]];
247
+ if (!handler) {
248
+ console.warn(`loadbare: page '${page}' declares no ${CRUD_KEYS[name]} for '${query}'`);
249
+ return [];
250
+ }
251
+ if (!Array.isArray(keys) || keys.some((key) => typeof key !== "string")) {
252
+ console.warn(`loadbare: '${name}' carries keys that are not strings`);
253
+ return [];
254
+ }
255
+ let stated = {};
256
+ for (const key of keys) {
257
+ const { [URL_QUERY]: moved, ...answer } = (await handler.run(ctx, { ...one, key })) ?? {};
258
+ if (moved !== undefined) {
259
+ console.warn(`loadbare: ${CRUD_KEYS[name]} for '${query}' returned url() for ` +
260
+ `one of several rows, ignoring it`);
184
261
  }
185
- // The handler has already run, so the page still hears about it, by
186
- // its refresh set, as if no row for lb-url had been returned.
187
- console.warn(`loadbare: page '${page}' returned ${URL_QUERY} ${refused}, ignoring it`);
262
+ stated = layered(stated, answer);
188
263
  }
189
264
  return items(page, {
190
265
  ...(await run(page, handler.refresh, ctx)),
@@ -192,17 +267,33 @@ export function createHub(pages) {
192
267
  });
193
268
  }
194
269
  return {
195
- async dataForPage(page, ctx) {
270
+ async dataForPage(page, ctx, { follow = true, parms } = {}) {
196
271
  const entry = pages[page];
197
272
  if (!entry) {
198
273
  console.warn(`loadbare: no page '${page}'`);
199
274
  return [];
200
275
  }
201
- await entry.requests.onPageEnter?.(ctx);
276
+ const { [URL_QUERY]: moved, ...stated } = (await entry.requests.onPageEnter?.(ctx)) ?? {};
277
+ if (Object.keys(stated).length > 0) {
278
+ console.warn(`loadbare: page '${page}' onPageEnter returned answers; it returns ` +
279
+ `url() or nothing, ignoring them`);
280
+ }
281
+ const item = follow ? urlItem(page, moved) : undefined;
282
+ if (item && !(parms && leadsHere(page, parms, item.row)))
283
+ return [item];
202
284
  return items(page, await run(page, Object.keys(entry.queries), ctx));
203
285
  },
204
286
  runRequest(page, request, ctx) {
205
287
  const { name, ...fields } = request;
288
+ if (fields.rows !== undefined && !wellFormedRows(fields.rows)) {
289
+ console.warn(`loadbare: '${name}' carries rows that are not keys and string ` +
290
+ `values by query, ignoring it`);
291
+ return Promise.resolve([]);
292
+ }
293
+ if (fields.keys !== undefined &&
294
+ (name === REQUEST_ROW_UPDATE || name === REQUEST_ROW_DELETE)) {
295
+ return eachKey(page, name, fields, ctx);
296
+ }
206
297
  if (isRowRequest(name)) {
207
298
  const query = fields.query ?? "";
208
299
  return settle(page, pages[page]?.requests.crud?.[query]?.[CRUD_KEYS[name]], fields, ctx, `loadbare: page '${page}' declares no ${CRUD_KEYS[name]} for '${query}'`);
@@ -211,13 +302,74 @@ export function createHub(pages) {
211
302
  },
212
303
  };
213
304
  }
305
+ /** Gathered rows as the hub sends them: string keys and string values. */
306
+ function wellFormedRows(rows) {
307
+ if (typeof rows !== "object" || rows === null || Array.isArray(rows)) {
308
+ return false;
309
+ }
310
+ return Object.values(rows).every((set) => Array.isArray(set) &&
311
+ set.every((row) => typeof row === "object" &&
312
+ row !== null &&
313
+ typeof row.key === "string" &&
314
+ typeof row.values === "object" &&
315
+ row.values !== null &&
316
+ Object.values(row.values).every((v) => typeof v === "string")));
317
+ }
318
+ /**
319
+ * Two answers as one: a patch laid onto a patch for the same query keeps
320
+ * what both name, and any other answer replaces the one before it.
321
+ */
322
+ function layered(before, after) {
323
+ const out = { ...before };
324
+ for (const [name, answer] of Object.entries(after)) {
325
+ const was = out[name];
326
+ if (was && patches.has(was) && patches.has(answer)) {
327
+ const a = was;
328
+ const b = answer;
329
+ out[name] = patch({
330
+ rows: [...(a.rows ?? []), ...(b.rows ?? [])],
331
+ drop: [...(a.drop ?? []), ...(b.drop ?? [])],
332
+ });
333
+ }
334
+ else {
335
+ out[name] = answer;
336
+ }
337
+ }
338
+ return out;
339
+ }
340
+ /**
341
+ * The `lb-url` item for a row a handler or `onPageEnter` returned, or
342
+ * nothing when it returned none or one that cannot be used. Whatever
343
+ * returned it has already run, so a row that cannot be used is reported and
344
+ * the call goes on as if none had been returned.
345
+ */
346
+ function urlItem(page, moved) {
347
+ if (moved === undefined)
348
+ return undefined;
349
+ const refused = unusableUrl(moved);
350
+ if (refused === undefined) {
351
+ return {
352
+ query: URL_QUERY,
353
+ kind: KIND_ROW,
354
+ key: URL_COLUMN_PATH,
355
+ row: moved,
356
+ };
357
+ }
358
+ console.warn(`loadbare: page '${page}' returned ${URL_QUERY} ${refused}, ignoring it`);
359
+ return undefined;
360
+ }
361
+ /** Whether a row for `lb-url` leads to the page and parms already in hand. */
362
+ function leadsHere(page, parms, row) {
363
+ const next = nextLocation(page, parms, row);
364
+ return next.page === page && next.parms.toString() === parms.toString();
365
+ }
214
366
  /**
215
367
  * Why a returned row for `lb-url` cannot be used, or nothing when it can.
216
368
  *
217
369
  * A value is a string, as a control's is and a URL's is. At least one column
218
370
  * is named: a row naming none changes nothing.
219
371
  */
220
- function refusal(columns) {
372
+ function unusableUrl(columns) {
221
373
  if (typeof columns !== "object" ||
222
374
  columns === null ||
223
375
  Array.isArray(columns)) {
@@ -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 Loadbare on the server. It ships\n// no HTTP server, no router, and no data layer.\n\nimport type {\n HubData,\n HubRequest,\n HubResponse,\n HubResult,\n Kind,\n Patch,\n RequestFields,\n ResponseItem,\n Row,\n} from \"../core/lb-types.js\";\nimport { isRowRequest } from \"../core/lb-types.js\";\nimport {\n KIND_ROW,\n KIND_ROWS,\n LB_RESERVED_PREFIX,\n REQUEST_ROW_DELETE,\n REQUEST_ROW_INSERT,\n REQUEST_ROW_UPDATE,\n URL_COLUMN_PATH,\n URL_QUERY,\n} from \"../core/lb-constants.js\";\n\n/**\n * Whatever the application hands the engine for the duration of one request.\n *\n * Loadbare declares it empty and never reads it. An application fills it in\n * by 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 * The page's query parms are the other expected member: a query that reads\n * one off the context takes no argument, and re-runs under `refresh` like\n * any other.\n */\nexport interface HubContext {}\n\n/**\n * A declared query: its kind, its key, and the run that answers it.\n *\n * Kind and key are properties of the name rather than of any one answer, so\n * they are declared here and never inferred from what comes back. The engine\n * sends both with every answer, so the markup repeats neither. Every query\n * has a key; an aggregate row answers with a constant one.\n *\n * Write one with `row()` or `rows()` below; nothing else builds one.\n */\nexport interface Query {\n readonly kind: Kind;\n readonly key: string;\n readonly run: (ctx: HubContext) => HubResult | Promise<HubResult>;\n}\n\n/** A query that answers with one row, identified by its `key` column. */\nexport function row(\n key: string,\n run: (ctx: HubContext) => Row | Promise<Row>,\n): Query {\n return { kind: KIND_ROW, key, run };\n}\n\n/**\n * A query that answers with all its rows, and therefore also their order.\n * Rows land by their `key` column: a row whose key is not in the answer is\n * gone.\n */\nexport function rows(\n key: string,\n run: (ctx: HubContext) => Row[] | Promise<Row[]>,\n): Query {\n return { kind: KIND_ROWS, key, run };\n}\n\n/**\n * Only what changed, returned from a handler for a `rows` query. Rows named\n * here are added or updated, keys in `drop` are removed, and everything\n * unnamed is left alone — its contents, and its place in whatever order the\n * page is keeping.\n *\n * `Array.isArray` is what tells a patch from all rows, 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?: unknown[] }): Patch {\n const made = { ...change };\n patches.add(made);\n return made;\n}\n\n/**\n * Every object `patch()` made. A patch and a row are both plain objects, so\n * this is how the engine refuses a patch answering a `row` query.\n */\nconst patches = new WeakSet<object>();\n\n/**\n * A new row for `lb-url`, returned from a handler when only the handler can\n * know where the page belongs: the key of a row it inserted, or the absence\n * of one it deleted.\n *\n * A column other than `lb-path` is a query parm: set, or taken out when its\n * value is empty. With `lb-path` naming another page, the page is entered\n * with only the parms named here; otherwise every other parm is kept.\n *\n * The page then loads at the new URL in the same round trip, as a cold load\n * of it would, so the refresh set is not run and whatever else the handler\n * returned is dropped: both were answers for the URL the page is leaving.\n * This is Post/Redirect/Get without the redirect.\n */\nexport function url(columns: Record<string, string>): HubData {\n return { [URL_QUERY]: { ...columns } };\n}\n\n/** The shape of a `<stub>.queries.ts` module. */\nexport type Queries = Record<string, Query>;\n\n/**\n * What a handler does, and which queries re-run once it has.\n *\n * `run` may also return answers of its own, which are laid over the\n * refreshed ones. That is how a patch reaches the browser: a query answers\n * for all its rows and cannot know why it re-ran, but the handler knows\n * exactly what it changed and can say only that.\n */\nexport interface Handler<R = RequestFields> {\n run: (\n ctx: HubContext,\n request: R,\n ) => void | HubData | Promise<void | HubData>;\n refresh: string[];\n}\n\nexport type RowInsertHandler = Handler<{\n key?: string;\n values: Record<string, string>;\n}>;\n/** `values` holds only the columns being set, as an SQL UPDATE sets them. */\nexport type RowUpdateHandler = Handler<{\n key: string;\n values: Record<string, string>;\n}>;\nexport type RowDeleteHandler = Handler<{ key: string }>;\n\n/**\n * What the requests Loadbare provides do to one query, keyed by its name in\n * `Requests.crud`. A query with no entry here permits none of them: the wire\n * cannot reach anything the page has not published.\n *\n * Every key is the request name with the prefix stripped and the rest\n * camel-cased: `lb-row-insert` runs `rowInsert`.\n */\nexport interface Crud {\n rowInsert?: RowInsertHandler;\n rowUpdate?: RowUpdateHandler;\n rowDelete?: RowDeleteHandler;\n}\n\n/**\n * The shape of a `<stub>.requests.ts` module.\n *\n * `onPageEnter` runs when the page loads, before its queries. It declares no\n * refresh set: the page's queries all run afterward, so whatever it changed\n * is already in the response.\n *\n * `handlers` holds the page's declared requests, by name. A name the page\n * did not declare is refused, so the wire cannot reach anything the page has\n * not published.\n *\n * `crud` holds what the requests Loadbare provides run, by query name.\n */\nexport interface Requests {\n onPageEnter?: (ctx: HubContext) => void | Promise<void>;\n handlers?: Record<string, Handler>;\n crud?: Record<string, Crud>;\n}\n\n/** A page is three files sharing a stub; two of them are these. */\nexport interface Page {\n queries: Queries;\n requests: Requests;\n}\n\n/**\n * The page registry. The builder writes it from every `.requests.ts` and\n * `.queries.ts` it discovers — see docs/reference/builder.md.\n */\nexport type Pages = Record<string, Page>;\n\n/**\n * The engine. Both calls answer with response items. A request whose\n * handler returned `url()` answers with the `lb-url` item alone: the caller\n * builds a context at the new URL, calls `dataForPage` with it, and sends\n * the load beside the item. `hubRoutes` does exactly that.\n */\nexport interface Hub {\n /** A page load: its `onPageEnter`, then all of its queries. */\n dataForPage(page: string, ctx: HubContext): Promise<HubResponse>;\n\n /** A request: run the handler its name picks, then the refresh set. */\n runRequest(\n page: string,\n request: HubRequest,\n ctx: HubContext,\n ): Promise<HubResponse>;\n}\n\n/** The camel-cased `Crud` key of a request Loadbare provides. */\nconst CRUD_KEYS: Record<string, keyof Crud> = {\n [REQUEST_ROW_INSERT]: \"rowInsert\",\n [REQUEST_ROW_UPDATE]: \"rowUpdate\",\n [REQUEST_ROW_DELETE]: \"rowDelete\",\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 — its own query,\n // the requests it provides — 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.handlers ?? {}),\n ...Object.keys(entry.requests.crud ?? {}),\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 // An update or a delete names its row by key, so its handler always\n // knows which rows changed and can answer with a patch; removing a row\n // never reorders the rest. Refreshing the whole set instead sends every\n // row and places every row again, which moves the element the user is\n // in. There is no case where that is the answer to either, so it is\n // refused rather than warned about. An insert is not: where its row\n // goes depends on the markup, which the server cannot see.\n for (const [name, crud] of Object.entries(entry.requests.crud ?? {})) {\n if (entry.queries[name]?.kind !== KIND_ROWS) continue;\n for (const request of [\"rowUpdate\", \"rowDelete\"] as const) {\n if (crud[request]?.refresh.includes(name)) {\n throw new Error(\n `loadbare: page '${page}' refreshes '${name}' after its own ` +\n `${request}; return the changed rows in a patch instead`,\n );\n }\n }\n }\n }\n\n /**\n * Answers into response items, adding the kind and key each query\n * declared. An answer that disagrees with its declaration is a mistake in\n * the application rather than a case to handle, and is left out: a page is\n * better off missing one query than showing the wrong shape for it.\n */\n function items(page: string, data: HubData): HubResponse {\n const out: ResponseItem[] = [];\n for (const [name, result] of Object.entries(data)) {\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 item: ResponseItem = {\n query: name,\n kind: query.kind,\n key: query.key,\n };\n if (query.kind === KIND_ROW) {\n if (Array.isArray(result) || patches.has(result)) {\n console.warn(\n `loadbare: query '${name}' is declared row but answered with ` +\n `${Array.isArray(result) ? \"rows\" : \"a patch\"}`,\n );\n continue;\n }\n item.row = result as Row;\n } else if (Array.isArray(result)) {\n item.rows = result;\n } else {\n item.patch = result as Patch;\n }\n out.push(item);\n }\n return out;\n }\n\n /** Run queries by name, each answering with all its rows or its row. */\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 if (Array.isArray(result) !== (query.kind === KIND_ROWS)) {\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 * Run a handler, then its refresh set against the same context, laying\n * what the handler itself returned over the refreshed queries — the\n * narrower answer wins because it knows what actually changed.\n *\n * A handler that returned `url()` answers with the `lb-url` item alone.\n * Loading the page there needs a context built at the new URL, and\n * building one is the caller's.\n */\n async function settle(\n page: string,\n handler: Handler<never> | undefined,\n request: RequestFields,\n ctx: HubContext,\n notFound: string,\n ): Promise<HubResponse> {\n if (!handler) {\n console.warn(notFound);\n return [];\n }\n const { [URL_QUERY]: moved, ...stated } =\n (await handler.run(ctx, request as never)) ?? {};\n if (moved !== undefined) {\n const refused = refusal(moved);\n if (refused === undefined) {\n return [\n {\n query: URL_QUERY,\n kind: KIND_ROW,\n key: URL_COLUMN_PATH,\n row: moved as Row,\n },\n ];\n }\n // The handler has already run, so the page still hears about it, by\n // its refresh set, as if no row for lb-url had been returned.\n console.warn(\n `loadbare: page '${page}' returned ${URL_QUERY} ${refused}, ignoring it`,\n );\n }\n return items(page, {\n ...(await run(page, handler.refresh, ctx)),\n ...stated,\n });\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 items(page, await run(page, Object.keys(entry.queries), ctx));\n },\n\n runRequest(page, request, ctx) {\n const { name, ...fields } = request;\n if (isRowRequest(name)) {\n const query = fields.query ?? \"\";\n return settle(\n page,\n pages[page]?.requests.crud?.[query]?.[CRUD_KEYS[name]!] as\n Handler<never> | undefined,\n fields,\n ctx,\n `loadbare: page '${page}' declares no ${CRUD_KEYS[name]} for '${query}'`,\n );\n }\n return settle(\n page,\n pages[page]?.requests.handlers?.[name] as Handler<never> | undefined,\n fields,\n ctx,\n `loadbare: page '${page}' declares no request '${name}'`,\n );\n },\n };\n}\n\n/**\n * Why a returned row for `lb-url` cannot be used, or nothing when it can.\n *\n * A value is a string, as a control's is and a URL's is. At least one column\n * is named: a row naming none changes nothing.\n */\nfunction refusal(columns: unknown): string | undefined {\n if (\n typeof columns !== \"object\" ||\n columns === null ||\n Array.isArray(columns)\n ) {\n return \"that is not a row\";\n }\n const values = Object.values(columns);\n if (values.length === 0) return \"naming no column\";\n if (!values.every((v) => typeof v === \"string\")) {\n return `holding a value that is not a string; write url({ name: \"...\" })`;\n }\n return undefined;\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 Loadbare on the server. It ships\n// no HTTP server, no router, and no data layer.\n\nimport type {\n HubData,\n HubRequest,\n HubResponse,\n HubResult,\n Kind,\n GatheredRow,\n HubRefusal,\n Patch,\n RequestFields,\n ResponseItem,\n Row,\n} from \"../core/lb-types.js\";\nimport { isRowRequest, nextLocation, parseOrder } from \"../core/lb-types.js\";\nimport {\n KIND_ROW,\n KIND_ROWS,\n LB_RESERVED_PREFIX,\n REQUEST_ROW_DELETE,\n REQUEST_ROW_INSERT,\n REQUEST_ROW_UPDATE,\n URL_COLUMN_PATH,\n URL_QUERY,\n} from \"../core/lb-constants.js\";\n\n/**\n * Whatever the application hands the engine for the duration of one request.\n *\n * Loadbare declares it empty and never reads it. An application fills it in\n * by 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 * The page's query parms are the other expected member: a query that reads\n * one off the context takes no argument, and re-runs under `refresh` like\n * any other.\n */\nexport interface HubContext {}\n\n/**\n * A declared query: its kind, its key, and the run that answers it.\n *\n * Kind and key are properties of the name rather than of any one answer, so\n * they are declared here and never inferred from what comes back. The engine\n * sends both with every answer, so the markup repeats neither. Every query\n * has a key; an aggregate row answers with a constant one.\n *\n * Write one with `row()` or `rows()` below; nothing else builds one.\n */\nexport interface Query {\n readonly kind: Kind;\n readonly key: string;\n readonly run: (ctx: HubContext) => HubResult | Promise<HubResult>;\n /** A `rows` query's order, as `rows()` declared it. */\n readonly order?: string;\n /** Whether a `rows` query reads the order parm and sorts by it itself. */\n readonly serverSortedByUrl?: boolean;\n}\n\n/**\n * What a `rows` query declares about its order.\n *\n * `order` is the order the hub places its rows by: columns separated by\n * commas, each descending when it begins with `-`, as in\n * `category_order,-opened`. The user's order, carried in the query parm\n * `lb-order-<query>`, replaces it. With neither, rows show in the order the\n * query answered them in.\n *\n * `serverSortedByUrl` says the query reads that parm and sorts by it itself,\n * which a query that pages or limits its rows must: a change of the user's\n * order then runs the query again. Without it, the hub sorts the rows it\n * already has, with no round trip.\n */\nexport interface RowsOptions {\n order?: string;\n serverSortedByUrl?: boolean;\n}\n\n/** A query that answers with one row, identified by its `key` column. */\nexport function row(\n key: string,\n run: (ctx: HubContext) => Row | Promise<Row>,\n): Query {\n return { kind: KIND_ROW, key, run };\n}\n\n/**\n * A query that answers with all its rows. Rows land by their `key` column: a\n * row whose key is not in the answer is gone. The hub places them by the\n * order in `options`, or in the order they arrived when there is none.\n */\nexport function rows(\n key: string,\n run: (ctx: HubContext) => Row[] | Promise<Row[]>,\n options: RowsOptions = {},\n): Query {\n return {\n kind: KIND_ROWS,\n key,\n run,\n ...(options.order !== undefined && { order: options.order }),\n ...(options.serverSortedByUrl && { serverSortedByUrl: true }),\n };\n}\n\n/**\n * Only what changed, returned from a handler for a `rows` query. Rows named\n * here are added or updated, keys in `drop` are removed, and everything\n * unnamed is left alone — its contents, and its place in whatever order the\n * page is keeping.\n *\n * `Array.isArray` is what tells a patch from all rows, 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?: unknown[] }): Patch {\n const made = { ...change };\n patches.add(made);\n return made;\n}\n\n/**\n * Every object `patch()` made. A patch and a row are both plain objects, so\n * this is how the engine refuses a patch answering a `row` query.\n */\nconst patches = new WeakSet<object>();\n\n/**\n * A new row for `lb-url`, returned from a handler when only the handler can\n * know where the page belongs: the key of a row it inserted, or the absence\n * of one it deleted.\n *\n * A column other than `lb-path` is a query parm: set, or taken out when its\n * value is empty. With `lb-path` naming another page, the page is entered\n * with only the parms named here; otherwise every other parm is kept.\n *\n * The page then loads at the new URL in the same round trip, as a cold load\n * of it would, so the refresh set is not run and whatever else the handler\n * returned is dropped: both were answers for the URL the page is leaving.\n * This is Post/Redirect/Get without the redirect.\n */\nexport function url(columns: Record<string, string>): HubData {\n return { [URL_QUERY]: { ...columns } };\n}\n\n/**\n * A handler's refusal: thrown, and answered to the browser with the words\n * it carries. Every other error a handler throws reaches the browser as a\n * failure with nothing in it.\n */\nexport class Refusal extends Error {\n readonly refusal: HubRefusal;\n\n constructor(refusal: HubRefusal) {\n super(refusal.message ?? \"refused\");\n this.name = \"Refusal\";\n this.refusal = refusal;\n }\n}\n\n/**\n * Refuse a request, in words written for the user. Throw what it returns:\n *\n * throw refusal(\"That series already has paychecks entered.\");\n * throw refusal({ amount: \"Must be more than zero\" });\n * throw refusal(\"Two fields need attention.\", { amount: \"…\" });\n *\n * A message alone is for the request as a whole; columns name the controls\n * the request gathered, and each message is shown on its control. Thrown\n * rather than returned, so a refusal can come from anywhere the handler\n * calls, and whatever the application has open rolls back as for any\n * throw.\n */\nexport function refusal(\n message: string | Record<string, string>,\n columns?: Record<string, string>,\n): Refusal {\n return typeof message === \"string\"\n ? new Refusal({ message, ...(columns && { columns }) })\n : new Refusal({ columns: message });\n}\n\n/** The shape of a `<stub>.queries.ts` module. */\nexport type Queries = Record<string, Query>;\n\n/**\n * What a handler does, and which queries re-run once it has.\n *\n * `run` may also return answers of its own, which are laid over the\n * refreshed ones. That is how a patch reaches the browser: a query answers\n * for all its rows and cannot know why it re-ran, but the handler knows\n * exactly what it changed and can say only that.\n */\nexport interface Handler<R = RequestFields> {\n run: (\n ctx: HubContext,\n request: R,\n ) => void | HubData | Promise<void | HubData>;\n refresh: string[];\n}\n\n/**\n * `rows` holds the rows of the queries nested in the form the request came\n * from, by query name, each with its own values: a master's details,\n * written with it.\n */\nexport type RowInsertHandler = Handler<{\n key?: string;\n values: Record<string, string>;\n rows?: Record<string, GatheredRow[]>;\n}>;\n/** `values` holds only the columns being set, as an SQL UPDATE sets them. */\nexport type RowUpdateHandler = Handler<{\n key: string;\n values: Record<string, string>;\n rows?: Record<string, GatheredRow[]>;\n}>;\nexport type RowDeleteHandler = Handler<{ key: string }>;\n\n/**\n * What the requests Loadbare provides do to one query, keyed by its name in\n * `Requests.crud`. A query with no entry here permits none of them: the wire\n * cannot reach anything the page has not published.\n *\n * Every key is the request name with the prefix stripped and the rest\n * camel-cased: `lb-row-insert` runs `rowInsert`.\n */\nexport interface Crud {\n rowInsert?: RowInsertHandler;\n rowUpdate?: RowUpdateHandler;\n rowDelete?: RowDeleteHandler;\n}\n\n/**\n * The shape of a `<stub>.requests.ts` module.\n *\n * `onPageEnter` runs when the page loads, before its queries. It declares no\n * refresh set: the page's queries all run afterward, so whatever it changed\n * is already in the response. It may return `url()` instead, to move the\n * page to other query parms or another page before anything shows, as an\n * application restoring what the user last had on screen does. The page\n * then loads there in the same round trip, and `onPageEnter` running for\n * that load is not followed again.\n *\n * `handlers` holds the page's declared requests, by name. A name the page\n * did not declare is refused, so the wire cannot reach anything the page has\n * not published.\n *\n * `crud` holds what the requests Loadbare provides run, by query name.\n */\nexport interface Requests {\n onPageEnter?: (ctx: HubContext) => void | HubData | Promise<void | HubData>;\n handlers?: Record<string, Handler>;\n crud?: Record<string, Crud>;\n}\n\n/** A page is three files sharing a stub; two of them are these. */\nexport interface Page {\n queries: Queries;\n requests: Requests;\n}\n\n/**\n * The page registry. The builder writes it from every `.requests.ts` and\n * `.queries.ts` it discovers — see docs/reference/builder.md.\n */\nexport type Pages = Record<string, Page>;\n\n/**\n * How a page load treats a `url()` its `onPageEnter` returned.\n *\n * With `follow` false it is ignored and the queries run, as they must for a\n * load that is itself at a URL something else moved to. With `parms`, the\n * query parms the page is being loaded at, one that leads to the page and\n * parms already in hand is ignored the same way, so the page loads once.\n */\nexport interface PageLoadOptions {\n follow?: boolean;\n parms?: URLSearchParams;\n}\n\n/**\n * The engine. Both calls answer with response items. A request whose\n * handler returned `url()`, and a page load whose `onPageEnter` did, answer\n * with the `lb-url` item alone: the caller builds a context at the new URL,\n * calls `dataForPage` with it and `follow` false, and sends the load beside the\n * item. `hubRoutes` does exactly that.\n */\nexport interface Hub {\n /** A page load: its `onPageEnter`, then all of its queries. */\n dataForPage(\n page: string,\n ctx: HubContext,\n options?: PageLoadOptions,\n ): Promise<HubResponse>;\n\n /** A request: run the handler its name picks, then the refresh set. */\n runRequest(\n page: string,\n request: HubRequest,\n ctx: HubContext,\n ): Promise<HubResponse>;\n}\n\n/** The camel-cased `Crud` key of a request Loadbare provides. */\nconst CRUD_KEYS: Record<string, keyof Crud> = {\n [REQUEST_ROW_INSERT]: \"rowInsert\",\n [REQUEST_ROW_UPDATE]: \"rowUpdate\",\n [REQUEST_ROW_DELETE]: \"rowDelete\",\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 — its own query,\n // the requests it provides — 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.handlers ?? {}),\n ...Object.keys(entry.requests.crud ?? {}),\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 // An update or a delete names its row by key, so its handler always\n // knows which rows changed and can answer with a patch, and the hub\n // places what the patch names by the query's order. Refreshing the whole\n // set instead sends every row to say one thing. There is no case where\n // that is the answer to either, so it is refused rather than warned\n // about. An insert is not: its key exists only once the row does, and a\n // handler may leave the query to say so.\n // An order is a fact about the query, so a malformed one is refused at\n // startup, as a reserved name is.\n for (const [name, query] of Object.entries(entry.queries)) {\n if (query.order === undefined) continue;\n try {\n parseOrder(query.order);\n } catch (err) {\n throw new Error(\n `loadbare: page '${page}' query '${name}': ${(err as Error).message}`,\n );\n }\n }\n\n for (const [name, crud] of Object.entries(entry.requests.crud ?? {})) {\n if (entry.queries[name]?.kind !== KIND_ROWS) continue;\n for (const request of [\"rowUpdate\", \"rowDelete\"] as const) {\n if (crud[request]?.refresh.includes(name)) {\n throw new Error(\n `loadbare: page '${page}' refreshes '${name}' after its own ` +\n `${request}; return the changed rows in a patch instead`,\n );\n }\n }\n }\n }\n\n /**\n * Answers into response items, adding the kind and key each query\n * declared. An answer that disagrees with its declaration is a mistake in\n * the application rather than a case to handle, and is left out: a page is\n * better off missing one query than showing the wrong shape for it.\n */\n function items(page: string, data: HubData): HubResponse {\n const out: ResponseItem[] = [];\n for (const [name, result] of Object.entries(data)) {\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 item: ResponseItem = {\n query: name,\n kind: query.kind,\n key: query.key,\n };\n if (query.kind === KIND_ROW) {\n if (Array.isArray(result) || patches.has(result)) {\n console.warn(\n `loadbare: query '${name}' is declared row but answered with ` +\n `${Array.isArray(result) ? \"rows\" : \"a patch\"}`,\n );\n continue;\n }\n item.row = result as Row;\n } else {\n if (Array.isArray(result)) item.rows = result;\n else item.patch = result as Patch;\n if (query.order !== undefined) item.order = query.order;\n if (query.serverSortedByUrl) item.serverSortedByUrl = true;\n }\n out.push(item);\n }\n return out;\n }\n\n /** Run queries by name, each answering with all its rows or its row. */\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 if (Array.isArray(result) !== (query.kind === KIND_ROWS)) {\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 * Run a handler, then its refresh set against the same context, laying\n * what the handler itself returned over the refreshed queries — the\n * narrower answer wins because it knows what actually changed.\n *\n * A handler that returned `url()` answers with the `lb-url` item alone.\n * Loading the page there needs a context built at the new URL, and\n * building one is the caller's.\n */\n async function settle(\n page: string,\n handler: Handler<never> | undefined,\n request: RequestFields,\n ctx: HubContext,\n notFound: string,\n ): Promise<HubResponse> {\n if (!handler) {\n console.warn(notFound);\n return [];\n }\n const { [URL_QUERY]: moved, ...stated } =\n (await handler.run(ctx, request as never)) ?? {};\n const item = urlItem(page, moved);\n if (item) return [item];\n return items(page, {\n ...(await run(page, handler.refresh, ctx)),\n ...stated,\n });\n }\n\n /**\n * An update or a delete for several rows: the page's `crud` entry runs once\n * per key, as it would for each row alone, so an application writes\n * nothing more to allow it. What each run returns is laid together, a\n * patch onto a patch, and the refresh set runs once.\n *\n * A run that returns `url()` is reported and its row ignored: a URL names\n * one view, and several rows cannot each choose it.\n */\n async function eachKey(\n page: string,\n name: string,\n fields: RequestFields,\n ctx: HubContext,\n ): Promise<HubResponse> {\n const { keys, ...one } = fields;\n const query = fields.query ?? \"\";\n const handler = pages[page]?.requests.crud?.[query]?.[CRUD_KEYS[name]!] as\n | Handler<never>\n | undefined;\n if (!handler) {\n console.warn(\n `loadbare: page '${page}' declares no ${CRUD_KEYS[name]} for '${query}'`,\n );\n return [];\n }\n if (!Array.isArray(keys) || keys.some((key) => typeof key !== \"string\")) {\n console.warn(`loadbare: '${name}' carries keys that are not strings`);\n return [];\n }\n let stated: HubData = {};\n for (const key of keys) {\n const { [URL_QUERY]: moved, ...answer } =\n (await handler.run(ctx, { ...one, key } as never)) ?? {};\n if (moved !== undefined) {\n console.warn(\n `loadbare: ${CRUD_KEYS[name]} for '${query}' returned url() for ` +\n `one of several rows, ignoring it`,\n );\n }\n stated = layered(stated, answer);\n }\n return items(page, {\n ...(await run(page, handler.refresh, ctx)),\n ...stated,\n });\n }\n\n return {\n async dataForPage(page, ctx, { follow = true, parms } = {}) {\n const entry = pages[page];\n if (!entry) {\n console.warn(`loadbare: no page '${page}'`);\n return [];\n }\n const { [URL_QUERY]: moved, ...stated } =\n (await entry.requests.onPageEnter?.(ctx)) ?? {};\n if (Object.keys(stated).length > 0) {\n console.warn(\n `loadbare: page '${page}' onPageEnter returned answers; it returns ` +\n `url() or nothing, ignoring them`,\n );\n }\n const item = follow ? urlItem(page, moved) : undefined;\n if (item && !(parms && leadsHere(page, parms, item.row!))) return [item];\n return items(page, await run(page, Object.keys(entry.queries), ctx));\n },\n\n runRequest(page, request, ctx) {\n const { name, ...fields } = request;\n if (fields.rows !== undefined && !wellFormedRows(fields.rows)) {\n console.warn(\n `loadbare: '${name}' carries rows that are not keys and string ` +\n `values by query, ignoring it`,\n );\n return Promise.resolve([]);\n }\n if (\n fields.keys !== undefined &&\n (name === REQUEST_ROW_UPDATE || name === REQUEST_ROW_DELETE)\n ) {\n return eachKey(page, name, fields, ctx);\n }\n if (isRowRequest(name)) {\n const query = fields.query ?? \"\";\n return settle(\n page,\n pages[page]?.requests.crud?.[query]?.[CRUD_KEYS[name]!] as\n Handler<never> | undefined,\n fields,\n ctx,\n `loadbare: page '${page}' declares no ${CRUD_KEYS[name]} for '${query}'`,\n );\n }\n return settle(\n page,\n pages[page]?.requests.handlers?.[name] as Handler<never> | undefined,\n fields,\n ctx,\n `loadbare: page '${page}' declares no request '${name}'`,\n );\n },\n };\n}\n\n/** Gathered rows as the hub sends them: string keys and string values. */\nfunction wellFormedRows(rows: unknown): boolean {\n if (typeof rows !== \"object\" || rows === null || Array.isArray(rows)) {\n return false;\n }\n return Object.values(rows).every(\n (set) =>\n Array.isArray(set) &&\n set.every(\n (row) =>\n typeof row === \"object\" &&\n row !== null &&\n typeof row.key === \"string\" &&\n typeof row.values === \"object\" &&\n row.values !== null &&\n Object.values(row.values).every((v) => typeof v === \"string\"),\n ),\n );\n}\n\n/**\n * Two answers as one: a patch laid onto a patch for the same query keeps\n * what both name, and any other answer replaces the one before it.\n */\nfunction layered(before: HubData, after: HubData): HubData {\n const out: HubData = { ...before };\n for (const [name, answer] of Object.entries(after)) {\n const was = out[name];\n if (was && patches.has(was) && patches.has(answer)) {\n const a = was as Patch;\n const b = answer as Patch;\n out[name] = patch({\n rows: [...(a.rows ?? []), ...(b.rows ?? [])],\n drop: [...(a.drop ?? []), ...(b.drop ?? [])],\n });\n } else {\n out[name] = answer;\n }\n }\n return out;\n}\n\n/**\n * The `lb-url` item for a row a handler or `onPageEnter` returned, or\n * nothing when it returned none or one that cannot be used. Whatever\n * returned it has already run, so a row that cannot be used is reported and\n * the call goes on as if none had been returned.\n */\nfunction urlItem(page: string, moved: unknown): ResponseItem | undefined {\n if (moved === undefined) return undefined;\n const refused = unusableUrl(moved);\n if (refused === undefined) {\n return {\n query: URL_QUERY,\n kind: KIND_ROW,\n key: URL_COLUMN_PATH,\n row: moved as Row,\n };\n }\n console.warn(\n `loadbare: page '${page}' returned ${URL_QUERY} ${refused}, ignoring it`,\n );\n return undefined;\n}\n\n/** Whether a row for `lb-url` leads to the page and parms already in hand. */\nfunction leadsHere(page: string, parms: URLSearchParams, row: Row): boolean {\n const next = nextLocation(page, parms, row);\n return next.page === page && next.parms.toString() === parms.toString();\n}\n\n/**\n * Why a returned row for `lb-url` cannot be used, or nothing when it can.\n *\n * A value is a string, as a control's is and a URL's is. At least one column\n * is named: a row naming none changes nothing.\n */\nfunction unusableUrl(columns: unknown): string | undefined {\n if (\n typeof columns !== \"object\" ||\n columns === null ||\n Array.isArray(columns)\n ) {\n return \"that is not a row\";\n }\n const values = Object.values(columns);\n if (values.length === 0) return \"naming no column\";\n if (!values.every((v) => typeof v === \"string\")) {\n return `holding a value that is not a string; write url({ name: \"...\" })`;\n }\n return undefined;\n}\n"]}