@loadbare/app 0.16.0 → 0.18.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 (46) hide show
  1. package/README.md +10 -14
  2. package/dist/build/assemble.d.ts.map +1 -1
  3. package/dist/build/assemble.js +15 -1
  4. package/dist/build/assemble.js.map +1 -1
  5. package/dist/core/lb-constants.d.ts +6 -2
  6. package/dist/core/lb-constants.d.ts.map +1 -1
  7. package/dist/core/lb-constants.js +38 -8
  8. package/dist/core/lb-constants.js.map +1 -1
  9. package/dist/core/lb-types.d.ts +57 -20
  10. package/dist/core/lb-types.d.ts.map +1 -1
  11. package/dist/core/lb-types.js +122 -11
  12. package/dist/core/lb-types.js.map +1 -1
  13. package/dist/hub/lb-apply.d.ts +38 -26
  14. package/dist/hub/lb-apply.d.ts.map +1 -1
  15. package/dist/hub/lb-apply.js +655 -471
  16. package/dist/hub/lb-apply.js.map +1 -1
  17. package/dist/hub/lb-hub.browser.d.ts.map +1 -1
  18. package/dist/hub/lb-hub.browser.js +77 -59
  19. package/dist/hub/lb-hub.browser.js.map +1 -1
  20. package/dist/hub/lb-lifecycle.d.ts +1 -2
  21. package/dist/hub/lb-lifecycle.d.ts.map +1 -1
  22. package/dist/hub/lb-lifecycle.js +9 -3
  23. package/dist/hub/lb-lifecycle.js.map +1 -1
  24. package/dist/server/lb-server.d.ts +29 -12
  25. package/dist/server/lb-server.d.ts.map +1 -1
  26. package/dist/server/lb-server.js +79 -19
  27. package/dist/server/lb-server.js.map +1 -1
  28. package/docs/TECHREF-1.0.md +167 -53
  29. package/docs/comparison.md +9 -8
  30. package/docs/reference/chrome.md +8 -5
  31. package/docs/reference/custom-elements.md +25 -7
  32. package/docs/reference/data-binding.md +125 -22
  33. package/docs/reference/page-files.md +56 -11
  34. package/docs/reference/widgets.md +16 -1
  35. package/docs/roadmap.md +8 -0
  36. package/docs/testing.md +6 -2
  37. package/docs/theory-old.md +839 -0
  38. package/docs/theory.md +113 -839
  39. package/package.json +1 -1
  40. package/skills/loadbare-app/SKILL.md +32 -20
  41. package/skills/loadbare-app/references/TECHREF-1.0.md +167 -53
  42. package/skills/loadbare-app/references/chrome.md +8 -5
  43. package/skills/loadbare-app/references/custom-elements.md +25 -7
  44. package/skills/loadbare-app/references/data-binding.md +125 -22
  45. package/skills/loadbare-app/references/page-files.md +56 -11
  46. package/skills/loadbare-app/references/widgets.md +16 -1
package/docs/theory.md CHANGED
@@ -1,839 +1,113 @@
1
- # Theory of Loadbare App
2
-
3
- by Ken Downs, August 29, 2026. Revised September 14, 2026.
4
-
5
- Loadbare is one man's answer to the expensive development and slow performance
6
- of modern web apps. When using modern frameworks, it often feels like
7
- I am working
8
- for the tools instead of the other way around. After all of that work,
9
- I have an application that does not respect the user's time, it is
10
- slow.
11
-
12
- My first goal in creating `@loadbare/app` was performance. I want my
13
- applications to be very snappy, I want them to respect the user's
14
- time.
15
-
16
- Efficient development is my second goal, so long as it leads to a
17
- snappy and performant application. My intuitive sense here is that
18
- the enemy of efficient development is what Fred Brooks called
19
- "accidental difficulties", the time you spend servicing the tools that
20
- allow you to build the application. Driving accidental complexity
21
- as close as possible to zero therefore became the next goal, as that
22
- would naturally result in developer efficiency.
23
-
24
- To begin, I had to redefine some of the popular concepts that govern
25
- discussion and decisions around front-end development.
26
-
27
- ## The Principle of Developer Experience
28
-
29
- Developer experience is often defined in terms of developer ergonomics,
30
- leaving out the developer's motivation for wanting to be comfortable.
31
- So I began by establishing a personal definition of the
32
- ideal developer experience:
33
-
34
- > The Principle of Developer Experience: The best developer experience is
35
- > creating an application that people use and appreciate.
36
-
37
- This principle pre-empts arguments for elegant abstractions and tooling
38
- that are not obviously and directly contributing to a satisfied
39
- end user.
40
-
41
- This principle combines and aligns business concerns with engineering
42
- concerns, grounding any solution in the desire to make a useful application.
43
-
44
- While no technical solution can magically provide me with the product
45
- skills to produce an application that users appreciate, I do not
46
- want the solution to make demands on my time that take me away from
47
- that goal.
48
-
49
- ## The Principle of User Experience
50
-
51
- The Principle of Developer Experience grounds us in the end user,
52
- and from here I considered my own largest single complaint about
53
- web apps: they are so damn slow.
54
-
55
- The path to a performant solution puts performance first and all
56
- design decisions follow from that primacy. Performance is not something
57
- to be optimized after a solution has been found by following other
58
- priorities.
59
-
60
- After some research into what people perceive as performant, I came
61
- upon the number of 300ms. Faster than that seems abrupt, slower
62
- seems sluggish. This led to the next foundation principle:
63
-
64
- > The Principle of User Experience: a full server round trip should
65
- > complete, from user action through durable persistence to final
66
- > paint, in a median time of 300ms.
67
-
68
- This scopes the goal to performance and performance only. All other
69
- aspects of the UI are mature, we have a mature inventory of widgets
70
- for user display and interaction. We do not need to invent new
71
- solutions for accessibility, tabs, buttons and so forth. What we need
72
- is a way to use the mature solution and respect the user's time.
73
-
74
- There is a direct practical implication: if we have a 300ms budget,
75
- and roughly assume 200ms median wire time, we must split the remaining
76
- 100ms between database, non-db server, and browser. Since the database
77
- in the limiting case must provide durable persistence, our ideal
78
- processing time for browser and app server activity must
79
- approach zero. In other words, our solution must be a model citizen,
80
- chewing up as few cycles as possible while providing the full modern
81
- experience of a web app, leaving
82
- as much as possible of the 100ms to the database.
83
-
84
- ## The Principle of Essential Complexity
85
-
86
- Now we move on to developer efficiency. Fred Brooks gave us a powerful
87
- way to consider this, when he made the distinction between "essential difficulties"
88
- and "accidental difficulties", later glossed into
89
- "essential complexity" and "accidental complexity".
90
-
91
- Essential complexity is the inherent difficulty of solving the user's
92
- problem. This is where we want to spend our time and effort.
93
- Accidental complexity is the work demanded by the tools we use to solve
94
- the problem. We want to minimize this, always pushing to zero.
95
-
96
- So, with apologies to Brooks and Pike, here is my mash-up and restatement
97
- of their ideas, tuned to the current task:
98
-
99
- > Essential complexity should dominate. Show me your abstractions and tooling and
100
- > I shall remain mystified. Show me your tables and your rendering vocabulary
101
- > and I shall be enlightened - the organization of the code will be self-evident.
102
-
103
- ## The Search For a Solution
104
-
105
- The three principles above give some weight to my intuitive rejection
106
- of the fat browser frameworks, though it would be truer to say the
107
- rejection came first. My extensive experience
108
- with React, Angular and VueJS left me able to state what I
109
- had found lacking. Once stated, the principles made it plain that the
110
- frameworks' motivations and mine are irrelevant to each other.
111
-
112
- This led me to look at the Hypermedia libraries. After several weeks
113
- of iterations, I found myself frustrated. These were very clearly
114
- closer to what I wanted, and the various authors' motivations seemed
115
- very close to my own, but I was still seeing more tooling concerns
116
- than I wanted. I seemed to be fighting the libraries, which could
117
- mean there was some unstated assumption we did not share, or perhaps
118
- they were solving a problem I did not have.
119
-
120
- ## Different Views of The Problem
121
-
122
- The lightbulb moment came when I found the unstated assumption,
123
- and found it served a problem I don't have. The solutions I had
124
- tried all support a DOM that is fully mutable
125
- at any time for any reason. They
126
- all provide or expect an HTML templating system that combines
127
- interpolation of data values with conditional and list rendering,
128
- controlled by run-time attributes.
129
-
130
- But I always think of the front-end as an interface to data that
131
- is either in a relational store or can be sent to the browser
132
- in relational shapes: single rows or sets of rows. I expect the
133
- DOM to mutate in response to data hydration and refreshes, which
134
- dramatically reduces the set of required mutations from "could be
135
- anything at any time" to exactly "refresh these representations
136
- of the data returned by the server".
137
-
138
- There are, of course, other mutations besides
139
- data. A button may be disabled after click when a request is in
140
- flight, and must be enabled when the request completes. Some DOM
141
- nodes become visible or invisible as a result of
142
- user interaction or data refreshes. A response may contain error
143
- information that must be presented to the user. But adding these cases
144
- only reinforced the emerging hypothesis: there is a knowable
145
- and closed set of required mutations for the DOM in a
146
- data-oriented application. A solution that optimizes for that closed
147
- set does not need the
148
- complexity required to support arbitrary DOM updates. This could
149
- be the path to a solution that is performant and reduces accidental
150
- complexity.
151
-
152
- ## The Solution Outline
153
-
154
- Now it was time to iterate on a framework for data-oriented applications
155
- with propositions narrowed to that space:
156
-
157
- 1. If HTML carries some type of data binding attributes,
158
- then the framework can perform all data-driven DOM mutations.
159
- 2. Given a data binding attribute vocabulary, other
160
- mutations could be handled with additional attributes, either as natural
161
- extensions to data binding, or in other channels that are fully
162
- independent and do not interfere with data binding.
163
- 3. Most server code ought to be reducible to responding to requests with
164
- data.
165
-
166
- Propositions 1 and 3 give hope of meeting our performance requirement.
167
- The framework task list is small, and tuning it for performance at
168
- every iteration seems doable.
169
-
170
- Proposition 2 gives hope of meeting our requirement that the application
171
- satisfy the high expectations of a modern UI, with many flexible affordances.
172
-
173
- But as promising as these propositions were, they do not yet fully
174
- cover the requirements listed
175
- at the top of this essay. At this point I only had a foundation and
176
- general direction.
177
- Going further required more iterations with demos and side projects,
178
- tying the general ideas to many specific implementation
179
- decisions.
180
-
181
- ## Solution Particulars
182
-
183
- From here it is no longer useful to trace through the specifics
184
- of various iterations. A full recitation of dead ends, aha moments, revisions
185
- and experiments would be tedious and uninformative.
186
-
187
- It would be equally tedious to recap the [Technical Reference](./TECHREF-1.0.md),
188
- or to flatly list the final mechanisms.
189
-
190
- What follows instead is a basic outline of the mechanisms and
191
- decisions as they finally stand, noting how each contributes to
192
- the major goals and to other nice-to-have goals that are mentioned
193
- in the course of the text.
194
-
195
- ### Scope and Boundaries
196
-
197
- Before I enumerate the mechanisms, I have to state how the framework
198
- boundaries fell out of the Principle of Essential Complexity.
199
-
200
- The framework does what we do not want to repeat in every application:
201
- binding data to the DOM, carrying requests and responses, assembling the static
202
- assets, wiring up the API. Everything else is the essential
203
- complexity of creating a particular application: what the data is, how it
204
- is stored and fetched, who may see it or change it, and how it looks.
205
-
206
- Where the framework leaves something to the application, it provides an
207
- empty slot and has no opinion about what goes in it. It does not:
208
-
209
- - prescribe or prohibit any CSS architecture
210
- - require or prevent any authentication solution
211
- - expect or hinder the use of an ORM, or of `@loadbare/db`
212
-
213
- Loadbare is for applications, not sites. Every route is answered with
214
- the same document, and a path that names no page is found out in the
215
- browser rather than answered with a 404. The path names a page, and the
216
- query string describes what that page has on screen, so the address bar
217
- is always true and a URL can be reloaded or mailed to someone.
218
-
219
- The rule for one-time chores, such as standing up an Express Server,
220
- is that they stay as close as possible to "set and forget", so that
221
- their cost is paid once and they are not a tax on
222
- application development tasks.
223
-
224
- ### HTML and Javascript
225
-
226
- HTML and Javascript are listed here first. They achieved the final
227
- form long before the rest of the framework, drove the builder
228
- design, and were the seed crystal that everything else formed
229
- around.
230
-
231
- The framework organizes HTML and Javascript together, expecting
232
- the HTML to be composed of native elements and custom elements
233
- with light DOM. Framework and application code are both built this
234
- way.
235
-
236
- The HTML is static, and is written in `.html` files, and once
237
- delivered to the browser it remains static.
238
-
239
- The framework manipulates the DOM by reading attributes.
240
- When data arrives, it can do direct scalar assignments
241
- or use `<template>` nodes
242
- that are cloned for lists, with scalar assignments made to
243
- each cloned instance.
244
-
245
- This structure allows for a very simple builder. The builder
246
- scans HTML, and when it finds a custom element, it makes sure
247
- there is either a matching JavaScript file, a matching HTML file,
248
- or both. Only the absence of both of them triggers an error, as
249
- we assume a custom element without a JavaScript class or HTML must
250
- be a mistake.
251
-
252
- Nearly every benefit of the approach falls out of that one
253
- scan. Because the builder finds everything by name, it can do
254
- the following without any configuration and without any new syntax:
255
-
256
- - HTML includes for free, no hacky syntax. When the builder
257
- finds a custom element with a matching `.html` file, it expands
258
- the element in place, recursively.
259
- - Separation of build-time parameters from data binding.
260
- An attribute on a custom element can
261
- be substituted into its HTML at build time, where it becomes
262
- permanent. This might be the label on an input or the text of a button.
263
- Once the HTML is all chased down, build-time parameters are
264
- constants in the shipped HTML code.
265
- - Slots. A custom element can provide the equivalent of named
266
- slots, filled by `<template>` elements that the builder resolves.
267
- - No Javascript imports. A custom element that is used pulls in
268
- its matching script by name. There is no import graph to maintain,
269
- reason about, or break.
270
- - Tree shaking for free. A script whose element is never used in
271
- the HTML is never shipped. There is no dead code analyzer,
272
- because dead code was never pulled in.
273
- - Validation for free. A custom element with neither a script
274
- nor an HTML file is a build error, and it catches the
275
- typo'd tag that the browser would otherwise silently ignore.
276
- - Three static assets. The build produces one HTML document
277
- with every page inside it as a `<template>`, one `client.js`
278
- holding the hub and every widget actually used, and one `app.css`
279
- holding every stylesheet exactly as it was authored. This
280
- is an easy builder to code, and leads to easy server code and
281
- deployment configurations.
282
- - CSS neutral. Because stylesheets are concatenated and nothing
283
- is added, removed or scoped, the builder neither requires nor
284
- prohibits any particular approach to CSS.
285
- - JavaScript is part of the DOM tree. By coupling JavaScript
286
- to custom elements, it can always reliably manipulate its
287
- children, and find shared code in ancestors.
288
-
289
- At runtime the same structure covers the rest of what a modern UI
290
- needs. Lists are handled by the `<template>` mechanism and data
291
- binding, as already mentioned. Conditional rendering is handled
292
- by one more attribute, `lb-show`, naming the column that decides
293
- whether an element is present. When the value is null or false,
294
- the hub moves the element into a `<template>` standing where it
295
- stood, and moves it back out when the value returns. (Note: this
296
- last claim is not yet fully proven by a demo or the author's projects.)
297
-
298
- Finally, and this is only my opinion, but I find static HTML with data
299
- binding attributes to be far easier to author, inspect,
300
- reason about, and maintain than systems like JSX that mix
301
- together permanent items like a button label with interpolated
302
- data, lists, and conditional rendering.
303
-
304
- ### Why the HTML Solution is Performant
305
-
306
- The HTML solution is one of two central pillars to my claim
307
- that `@loadbare/app` can deliver an app that fits into the
308
- median 300ms allotted to round trips from click to paint. Everything
309
- listed in the previous HTML and Javascript shows that the
310
- HTML and Javascript solution makes the framework the "model citizen"
311
- that does as little work as possible, leaving
312
- as much budget as we can to the database.
313
-
314
- Most of the work happens once, at build time. Widget expansion,
315
- build time parameters, slots, composition, validation and tree
316
- shaking are all done by the builder. None of it is repeated in the
317
- browser, and none of it is repeated per request on the server.
318
-
319
- The solution means that the only runtime work left is work that
320
- cannot be done until the data arrives.
321
-
322
- The application loads once. A session begins with three static
323
- files: one HTML document, one script and one stylesheet. They are
324
- plain static assets, so they can be compressed, cached, and served
325
- from anywhere. There are no per-route bundles, no lazy chunks, and
326
- no waterfall of module requests discovering their imports one at a
327
- time. The `--minify` flag shrinks the script and stylesheet further.
328
-
329
- The framework is small. The entire browser framework is the
330
- "hub" (explained below), which is under a thousand lines
331
- of Typescript, comments
332
- included (as of this writing). There is no virtual DOM, no component tree, no
333
- reactivity system and no scheduler to download, parse, or run.
334
-
335
- Navigation fetches no HTML and no code. Every page is already
336
- in the document as a `<template>`, parsed once by the browser when
337
- the document loaded. Moving to a page is a native `cloneNode` of
338
- that template into `<main>`.
339
-
340
- Only data crosses the wire. The server returns rows as JSON.
341
- Rows are far smaller than the markup that displays
342
- them, and the markup is already in the browser. One interaction is
343
- one request and one response, carrying back any refreshed or changed
344
- data.
345
-
346
- Only changed data crosses the wire. A request handler names the queries
347
- it refreshes, so the server re-runs only those, not the whole page.
348
- Where even that is too much, a request can return a patch: the rows
349
- that arrived or changed and the keys that went, rather than the
350
- entire list shipped again.
351
-
352
- The server renders nothing. There is no server-side rendering,
353
- no server templating, and no hydration step. A page's server code
354
- runs its queries and hands back what they returned. Nearly all of
355
- the server's time for a request is therefore the database's time,
356
- which is exactly where the Principle of User Experience wants it.
357
-
358
- The framework mutates DOM in response to data refreshes using a
359
- closed set of operations. The
360
- hub finds its targets with the browser's own selector engine, by
361
- the attributes written in the HTML, and sets a value, some text, or
362
- an attribute, or moves an element into or out of a template. No tree is diffed, and no
363
- element is rebuilt because its data arrived again. The framework
364
- never has to
365
- consider what might have changed, because the response says exactly
366
- what did.
367
-
368
- Nothing runs between interactions. Once data has landed, the
369
- hub waits on events. It keeps no timers, no observers and no
370
- polling loop, so an idle page costs the browser nothing.
371
-
372
- Taken together, the framework's share of a round trip is a small,
373
- fixed amount of work in the browser, a JSON serialization on the
374
- server, and little else.
375
-
376
- ### Chrome and Naming Conventions
377
-
378
- Something else fell out of the HTML and Javascript solution fairly
379
- early in framework development. It turned out that a handful of
380
- hard-coded naming conventions could simplify the builder while
381
- giving flexibility to file organization.
382
-
383
- The builder would require exactly one file named 'chrome.html',
384
- anywhere in the source tree, which contains the application skeleton,
385
- and is processed the same as all other HTML files.
386
-
387
- The builder would recognize any page ending in '.page.html' as a
388
- navigation target.
389
-
390
- A custom element's JavaScript, if present, should be named
391
- `my-custom-element.browser.ts`. The '.browser.' segment requires
392
- deliberate action to get the code into the browser, eliminating
393
- configuration files or switches that specify which code in the
394
- source tree is prohibited or forced into the browser.
395
-
396
- A custom element's HTML, if present, should be named `my-custom-element.html`.
397
- All matching is by name, no configuration required.
398
-
399
- The builder scan begins with `.page.html` files and `chrome.html`. All
400
- logic is simple and already stated: pick up code and HTML for custom
401
- elements, and do so recursively. That is the entire algorithm.
402
-
403
- This means that no special new mechanism was required for
404
- application chrome. There is no requirement for how to
405
- organize application code, and the destiny of every file is evident
406
- from a directory view.
407
-
408
- ### Data Binding
409
-
410
- Once it looked like HTML authoring and building was solved, it was time
411
- to ensure that Data Binding would not crash the party. If data binding
412
- exploded into overlapping features and nasty edge cases, all progress would
413
- halt.
414
-
415
- If the framework browser code is to remain small, performant, and robust,
416
- and fully expressive, then all of it must be achieved by reducing the
417
- data binding to a closed set of non-overlapping features.
418
-
419
- This meant that the earlier vague idea of "relationally shaped" had to
420
- become a scoping decision. The framework browser code will only understand
421
- rows and sets of rows. It will not understand a JSON document where child
422
- rows are embedded in a row, and it will not understand a tree where there are
423
- children in children in children. This does not force a relational
424
- database, but it does force server code to return data in the shapes of
425
- rows and sets of rows.
426
-
427
- That scoping decision allowed us to reduce the data binding vocabulary
428
- to one idea: everything is a query. A query is a name for rows, of kind
429
- `row` or `rows`, with a key. The server declares all three facts about a
430
- query, so the markup names the query and the columns it shows, and
431
- repeats nothing the server already said. Three attributes carry the whole
432
- of it:
433
-
434
- | Attribute | Relational idea | HTML precedent |
435
- | ----------- | -------------------------- | ---------------------------------------------- |
436
- | `lb-query` | A query, one row or many | `<select>` or `<form>`, a container of records |
437
- | `lb-column` | A column | `name` on a form control, which field this is |
438
- | `lb-show` | A column deciding presence | `hidden`, whether an element is shown |
439
-
440
- The key never appears in the markup. The server declares it with the
441
- query, `rows("id", run)`, and the hub stamps each live row with its
442
- value as `lb-key-value`, the way an `<option>` carries a `value` apart
443
- from its text.
444
-
445
- A highly simplified page showing
446
- an invoice and its lines looks like this:
447
-
448
- ```html
449
- <section lb-query="invoice">
450
- <h2 lb-column="number"></h2>
451
- <span lb-column="customer"></span>
452
- </section>
453
-
454
- <table>
455
- <tbody lb-query="invoiceLines">
456
- <template>
457
- <tr><td lb-column="item"></td><td lb-column="amount"></td></tr>
458
- </template>
459
- </tbody>
460
- </table>
461
- ```
462
-
463
- `invoice` is of kind `row`, and with no row template inside it the row
464
- lands on the `<section>` itself. `invoiceLines` is of kind `rows`, and
465
- the hub clones the row template once per row.
466
-
467
- This decision proved decisive in keeping the browser code robust,
468
- expressive, and performant.
469
-
470
-
471
- ### Requests and Request State
472
-
473
- Proposition 2 said that once there is a data binding vocabulary, the
474
- other mutations could be handled with additional attributes, either as
475
- extensions to data binding or in channels that are independent of it.
476
- The idea is that user interaction triggers framework browser code
477
- that "knows what to do", assembling a request driven purely from
478
- attributes.
479
-
480
- This requirement is satisfied with a single new attribute, `lb-request`.
481
- Three request names are Loadbare's: `lb-row-insert`, `lb-row-update` and
482
- `lb-row-delete`. Any other value names a request the page declares on
483
- the server.
484
-
485
- A request is a name and a position. The author writes the name, and
486
- the hub supplies the position from the document, using the same
487
- ancestor rule that decides where a value lands. An element commits the
488
- way HTML already says it does: a form on `submit`, a control on
489
- `change`, anything else on `click`. To add a delete button to the
490
- invoice lines from the previous section, the row template gains one
491
- element:
492
-
493
- ```html
494
- <template>
495
- <tr>
496
- <td lb-column="item"></td>
497
- <td lb-column="amount"></td>
498
- <td><button lb-request="lb-row-delete">Remove</button></td>
499
- </tr>
500
- </template>
501
- ```
502
-
503
- The button names neither the query nor the key. When it is clicked,
504
- the hub reads `invoiceLines` from the nearest ancestor `lb-query` and
505
- the key from the live row around the button, and sends:
506
-
507
- ```
508
- { name: "lb-row-delete", query: "invoiceLines", key: "42" }
509
- ```
510
-
511
- Every request has that one shape: a name, and `query`, `key` and
512
- `values` as present. An insert or update gathers `values` from the
513
- controls around the element that committed: a form's controls, a live
514
- row's, or the controls whose form owner is the element's.
515
-
516
- But we need to be more modern than that. Nowadays we expect the
517
- button to be disabled after it is clicked, and to display a wait
518
- state in case the network is congested. In other words, the
519
- framework browser code needs to notify the event target of its
520
- request state.
521
-
522
- To do this, framework browser code stamps two attributes about a
523
- request, `lb-request-pending` and `lb-request-error`, and a third about
524
- a query, `lb-query-row-count`. The framework browser code ignores a
525
- commit on an element stamped with `lb-request-pending`, meaning the
526
- disabled state can be represented entirely by CSS, no JavaScript
527
- needed.
528
-
529
- The developer writes `lb-*` attributes in markup, and script never
530
- assigns them. The hub and the builder write only their stamps. That
531
- rule is what lets the builder check the markup once, at build time.
532
-
533
- Over many iterations I gradually reduced the core attributes to
534
- these, from a larger set that had more overlapping behaviors and edge
535
- cases. In the end this small set builds on and protects the
536
- HTML/JavaScript solution.
537
-
538
- ### The Hub
539
-
540
- So far I have been using the term "framework browser code". In other
541
- documents we call it the hub.
542
- The hub is one custom element, `<lb-hub>`, and it is the entire
543
- browser framework. It has three jobs: navigation, synthetic context, and the data channel.
544
-
545
- #### Navigation
546
-
547
- As previously stated, the 1.0 builder packages all page HTML into the
548
- chrome and ships it as `app.html`. With all pages already present
549
- in the browser, providing an SPA is fairly simple.
550
-
551
- The hub catches anchor clicks, and checks if the anchor contains the
552
- attribute `lb-url-link`. If so, the hub interprets a plain primary
553
- click as in-app navigation, shows the page the `href` names in
554
- `<main>`, and sends a request to the server for the page data. The
555
- query string rides along, so the server always knows what URL the
556
- browser is showing.
557
-
558
- Anchors without the attribute behave as normal links.
559
-
560
- #### Synthetic Context
561
-
562
- Synthetic context emerged as a useful feature near the very end of
563
- development. Realizing that the hub knows about the current URL, we
564
- had it serve a query of its own, `lb-url`, of kind `row` and keyed by
565
- `lb-path`. Its columns are the path, the page's title as
566
- `lb-page-label`, whether the path names no page as `lb-page-unknown`,
567
- and one column per query parm.
568
-
569
- These can be bound to any HTML, same as any other query the
570
- application declares, but the hub supplies the row. The hub also
571
- answers requests for it: a control writing a query parm is
572
- `<input lb-column="q" lb-request="lb-row-update">` inside
573
- `lb-query="lb-url"`, an ordinary row update that the hub answers by
574
- writing the URL and reloading the page's queries. A handler on the
575
- server moves the URL the same way, by returning `url({ q: "5" })`.
576
-
577
- There may be more synthetic context in the future.
578
-
579
- #### The Data Channel
580
-
581
- We have already worked out that the hub is crafting requests,
582
- stamping request state, and mutating the DOM based on data received
583
- in responses.
584
-
585
- We have implicitly already assigned the hub responsibility for
586
- the data channel, and here I simply want to say it out loud.
587
-
588
- ### Server Page Code
589
-
590
- Proposition 3 said that most server code ought to be reducible to
591
- responding to requests with data. In Loadbare a page is up to three
592
- files that share a name: `invoice.page.html`, `invoice.queries.ts`
593
- and `invoice.requests.ts`. The HTML is the page as the user sees it,
594
- the queries provide the data, and the requests are what it allows.
595
-
596
- Each query is declared with `row(key, run)` or `rows(key, run)`,
597
- naming its kind and its key. Every response carries response items,
598
- each naming its query, kind and key beside the row, the rows, or a
599
- patch. Each request declares the work it does and the
600
- queries to run again afterward. Here is the delete button from
601
- Requests and Request State, as the server sees it:
602
-
603
- ```ts
604
- // invoice.requests.ts
605
- import { patch, type Requests } from "@loadbare/app/server";
606
-
607
- export const requests: Requests = {
608
- crud: {
609
- invoiceLines: {
610
- rowDelete: {
611
- run: async (ctx, { key }) => {
612
- await ctx.db.deleteLine(key);
613
- return { invoiceLines: patch({ drop: [key] }) };
614
- },
615
- refresh: ["invoice"],
616
- },
617
- },
618
- },
619
- };
620
- ```
621
-
622
- Deleting a line changes the invoice total, so the entry refreshes
623
- `invoice`. `invoiceLines` itself does not need to be queried again, since
624
- the only change is one row that went away, and `run` says so with a
625
- patch. The response carries two response items: the refreshed invoice
626
- row, and the patch that removes one line.
627
-
628
- Loadbare, as it turns out, does not have "endpoints" as such. If the
629
- HTML places an `lb-row-delete` inside `lb-query="invoiceLines"`, then the
630
- server carries a CRUD entry `rowDelete` under object invoiceLines. A
631
- declared request is the same, keyed by its name under `handlers`.
632
- Since the browser and server exist to talk to each other in the same
633
- language, no additional abstraction is needed.
634
-
635
-
636
- ### The Express Server
637
-
638
- Loadbare ships no server, and the application writes an ordinary
639
- Express app. I was rather surprised myself to find that the simplest
640
- solution here was to show example code, instead of shipping a shrink-wrapped
641
- server. Most frameworks today provide a
642
- server so completely hidden that a newcomer to the industry can be
643
- forgiven for not knowing Express is underneath.
644
-
645
- The build leaves the server four things to handle: the script, the
646
- stylesheet, the hub's data channel, and the one HTML document.
647
- The builder also writes a file,
648
- `pages.ts`, that gathers every page's queries and requests into one
649
- hub, so the server imports that hub and never lists the pages itself.
650
- The part of the server that belongs to Loadbare is four lines:
651
-
652
- ```ts
653
- app.use(hubRoutes(hub, contextFor));
654
- app.get("/client.js", (_req, res) => res.sendFile(path.join(DIST, "client.js")));
655
- app.get("/app.css", (_req, res) => res.sendFile(path.join(DIST, "app.css")));
656
- app.get(/.*/, (_req, res) => res.sendFile(path.join(DIST, "app.html")));
657
- ```
658
-
659
- These lines stay the same as pages are added. Everything else in the
660
- file belongs to the application, and that is where authentication,
661
- sessions, logging and the opening of the database go.
662
-
663
- If we tried to ship a server, we'd have to load it with hooks for every
664
- concern we don't handle, and we'd end up re-inventing Express. So
665
- we don't do that.
666
-
667
- ### What the Application Provides
668
-
669
- What Loadbare leaves to the application is the essential complexity
670
- named in Scope and Boundaries: what the data is, how it is stored and
671
- fetched, who may see it, and how it looks. On the server, all of it
672
- reaches the page code through one object.
673
-
674
- For each request, the application builds a context, `ctx`, in a
675
- function it hands to the hub in the Express server:
676
-
677
- ```ts
678
- function contextFor(req: Request): HubContext {
679
- return { db: openDb(req) };
680
- }
681
- ```
682
-
683
- Loadbare passes `ctx` to every query and every request.
684
- A query finds exactly what the application put there,
685
- which is usually a database handle opened for the user who made the
686
- request. Authentication decides what goes into `ctx`, and the page
687
- code sees only the result.
688
-
689
- How the application reaches its database is also its own choice. I
690
- always use `@loadbare/db`, which declares calculated values as part
691
- of the schema. Most reads come from one table or a simple view, and
692
- a write to one table cascades its calculations to related tables, so
693
- my page code calls the database directly and there is no business
694
- logic layer in the application at all. A team not using
695
- `@loadbare/db` will probably use an ORM or write a business logic
696
- layer, and it reaches the page code through `ctx` in the same way.
697
-
698
-
699
- ### Conclusions Against the Three Principles
700
-
701
- As we near the end of the essay, I want to summarize why I believe
702
- that `@loadbare/app` meets the goals set out at the top of the
703
- essay. I believe that overall we are somewhere well past
704
- plausible for all claims, but only further iterations and usage
705
- will tell the tale. This is a theory document, not a hard proof.
706
- Performance stats and tables will be published separately.
707
-
708
- The first principle stated in this essay was the Developer Experience
709
- Principle, that the best developer experience was building something
710
- that people use and appreciate.
711
-
712
- No framework can directly give me that wonderful app, so I have to
713
- elaborate that product talent must be leveraged and never blocked
714
- by the framework. If the framework is to help instead of hinder,
715
- it must smooth the path towards a great user experience, with
716
- developer efficiency being a co-equal primary goal.
717
-
718
- #### The User Experience
719
-
720
- User experience, in the broad sense, is a solved problem. We have long had a rich
721
- visual and interaction vocabulary, sufficiently expressive to
722
- present any form of data and allow interaction with it. Loadbare need
723
- invent nothing here, it must only take care not block the correct solutions.
724
-
725
- But the orphan out in the barn is performance, which users arguably
726
- care about more than anything.
727
-
728
- In terms of performance, I believe the claim that Loadbare has a good
729
- chunk of the answer and is deep into "undeniably plausible". The framework's
730
- tasks, footprint and overhead are so small that it cannot help but
731
- to be faster than the more elaborate alternatives. This is true in
732
- both the browser and the server. Loadbare has precisely zero
733
- "abstractions" that promise ergonomics while eroding performance. The
734
- path from request to server to response to paint contains exactly the
735
- steps that are essential to the job being done, and nothing more.
736
-
737
- If we consider performance as settled as it can get in a theory
738
- document, we move on to the rest of the User Experience, where
739
- loadbare follows the principle of not blocking the developer's desired
740
- solution. Anything around accessibility, theming or skinning, branding,
741
- interaction, and responsiveness fit into a widget library and a
742
- CSS solution.
743
-
744
- Consider that HTML is highly accessible when used mindfully,
745
- with a handful of aria-* attributes filling in the blanks. Concerns
746
- such as contrast and target size are all solved in CSS. This is why
747
- loadbare uses plain HTML, light DOM, and allows any structure of CSS: so that
748
- a designer can specify an accessible design that becomes an
749
- accessible widget library that is used to build an accessible app.
750
-
751
- #### Essential Complexity
752
-
753
- The essential complexity claim I believe is more vulnerable to
754
- honest critique or different perspectives, so first we'll look at
755
- it in a condensed form, and then address the best steel man argument
756
- I can muster.
757
-
758
- First, Loadbare has few requirements for project setup. They are
759
- one-time, and do not grow at the scale of the app. Every attempt
760
- has been made to make them "set and forget".
761
-
762
- Second, Loadbare is missing entire categories of developer effort,
763
- while providing expected benefits. HTML includes without imports,
764
- declarative browser data binding and requests, free tree shaking,
765
- extendable with custom elements, a small set of mechanisms, and
766
- nothing to learn about the "the Loadbare way for CSS", among many
767
- others.
768
-
769
- Third, the essential activity of page and widget authoring, where
770
- almost all of the work is, use nothing more than HTML and JavaScript
771
- that is naturally coupled to the DOM. On the server, the queries
772
- and requests have no boilerplate, setup or teardown requirements.
773
-
774
- Fourth, the entire Loadbare vocabulary can fit in a single technical
775
- reference document, which means it can fit into a person's head or
776
- an LLM's context window.
777
-
778
- > Full Disclosure: While I have had good success with LLM's coding
779
- > pages and widgets with the current technical reference, as of
780
- > this writing all Anthropic models consistently make one serious
781
- > mistake in .requests.ts files. They refresh list queries after
782
- > a patch, which is redundant, inefficient, and usually
783
- > destroys scroll position.
784
-
785
- With those four points made, a reasonable critic could still make an
786
- argument that I would not care to dispute. They may say, "Sure Ken,
787
- it looks like low accidental complexity to you because you wrote it,
788
- and you understand it, but all of those data binding attributes and
789
- that forced relational thinking are really just a swap of one set
790
- of accidental complexity for another. The system is really
791
- 'differently accidental' from other frameworks."
792
-
793
- I would not dispute this argument because it is more about perspective
794
- than measurable claims. It is true that Loadbare forces a declarative syntax
795
- and "relational shaped" data. This appears "essential" to me because
796
- I write database apps. By accepting it as essential, I get performant
797
- apps and most of my effort is spent on the apps and not the tooling or
798
- boilerplate. But if an existing stack does not easily allow the
799
- data to be processed into to rows and lists of rows, if the applications affordances
800
- do not seem to readily fit the vocabulary, then it is not a fit.
801
-
802
- In conclusion, we can see simply that Loadbare's claims around
803
- accidental and essential complexity are most likely to
804
- be true when the existing stack can support an application that wants
805
- data to be rows and tables, or easily mapped into them and out of them,
806
- and the desired affordances can be easily mapped to the standard
807
- set of operations against that data.
808
-
809
- ## Appendix: Code Shelf Life
810
-
811
- Loadbare has a benefit not stated elsewhere in the docs, because it
812
- really has no place to land: Loadbare app code has a long shelf life.
813
-
814
- Pages are HTML and CSS, widgets are custom elements, the server is Express,
815
- and the page code is queries against the application's own database. Each of
816
- those is a standard or a mature tool of its own, so the code written
817
- today is exposed to very little framework churn tomorrow.
818
-
819
- Every effort has also been made to scope release 1.0 so that future
820
- features we can imagine now can be made additive. Consider for
821
- example the current practice of a monolithic `app.html` containing
822
- all pages for the app. If this proves unworkable, or threatens
823
- the performance budget, we can add options for alternate solutions
824
- that do not break existing code.
825
-
826
- ## Appendix: Provenance and History
827
-
828
- I wrote Loadbare originally in 2003, as "Andromeda", before Node existed
829
- and even 3 years before we had jQuery. The foundation of Andromeda
830
- is now in `@loadbare/db`, the package that encodes business logic
831
- declaratively and frees me from the ORM and the "logic layer" in the
832
- application.
833
-
834
- Andromeda carried a web layer too, meant to take advantage of the
835
- enriched database. The conviction that led to Andromeda's UI in
836
- 2003 was that we needed a framework optimized for database
837
- applications. I am just as convinced today as I was then, and the effort
838
- to provide that framework continues in `@loadbare/app`.
839
-
1
+ # The Theory of Loadbare/app
2
+
3
+ Loadbare has multiople opinions, but there are two co-primary drivers
4
+ of all design:
5
+
6
+ - The user should expect a median round trip of 300ms for server
7
+ interactions.
8
+ - Elegant abstractions and reimplementations of
9
+ browser behavior must be avoided because they threaten
10
+ the performance goal, and they introduce accidental
11
+ complexity.
12
+
13
+ Two early simplifying decisions were that Loadbare would use SQL
14
+ terms, and would split server results into two shapes that are
15
+ best suited to a UI: a single `row` and a set of `rows`, using
16
+ `column` for the value of a single value in a row. Loadbare
17
+ does not understand non-atomic column values.
18
+
19
+ The framework's job then becomes to support displaying or "landing" values
20
+ into the DOM for a given collection of the `row` and `rows` shapes. It
21
+ also must provide the three writes, insert, update and delete, plus
22
+ an escape hatch for bespoke operations on the server.
23
+
24
+ Loadbare implements the following opinions on declarative systems.
25
+ - Declarative support for the data shapes and their operations
26
+ is easier to reason about for the developer, and greatly reduces
27
+ bugs in application code.
28
+ - Framework support for declarations about SQL shapes is finite
29
+ and free of edge cases, allowing for creation of a robust
30
+ framework processor in the browser.
31
+
32
+ Other opinions include:
33
+ - It would be nice to code straight static HTML, since HTML is
34
+ up to the task, being mature and stable. When
35
+ custom elements with light DOM are added, any bespoke behavior
36
+ can be supported.
37
+ - Frameworks should not interfere with CSS
38
+ - Code written for loadbare should run reliably on a timeframe
39
+ measured in decades. As long as we have browsers, a Loadbare
40
+ app should run.
41
+
42
+ ## The 300ms Budget
43
+
44
+ The first non-negotiable primary goal is a framework where server
45
+ round trips can run in a median 300ms, from click to paint.
46
+
47
+ Of the 300ms, we allocate 200ms as typical wire time when application
48
+ and user are in the same region, leaving 100ms for everything that
49
+ happens in the browser, the application server, and the database.
50
+
51
+ Since the database's response time is not under the control of the
52
+ Loadbare/app framework, we realize Loadbare/app must do its job in as
53
+ little time as possible. This is how we arrived at the second
54
+ primary opinion of Loadbare/app: to avoid elegant abstractions or
55
+ reimplementations of browser behavior that would threaten the
56
+ performance goal. Loadbare/app must be thin and get its job done
57
+ in low double-digit milliseconds, leaving as much time as possible
58
+ to the database and such tasks as authentication or picking up
59
+ a connection from the pool.
60
+
61
+
62
+ ## Ken Random Thought, For later
63
+
64
+ I often wonder if Loadbare/app does not in fact have "low accidental
65
+ complexity" but instead has "different accidental complexity".
66
+
67
+ Given the inherent complexity of the browser, any comprehensive
68
+ solution is going to be non-trivial, so simple measures like vocabulary
69
+ size are not good proxies.
70
+
71
+ The test, when we put something in, is whether we are following
72
+ SQL and following HTML.
73
+
74
+ For SQL, we know that we have:
75
+ - a `row`, with landing, insert, update, and delete
76
+ - `rows`, with landing, insert, update, and delete
77
+
78
+ All of the complexity comes when we map those to HTML, and here again
79
+ HTML has a larger surface area but it is still finite.
80
+
81
+ If we end up with 'best practices', and they look like accidental
82
+ complexity, a bunch of stuff you have to learn, the test is whether
83
+ a best practice already exists for HTML or SQL, and whether it makes
84
+ sense in the combination.
85
+
86
+
87
+ ## LLM-suggested nugget, to be integrated later
88
+
89
+ On the critique that Loadbare has "different accidents" than React or
90
+ HTMX. Scoped claim:
91
+
92
+ > The data vocabulary is SQL data shapes, SELECT and DML applied to
93
+ > HTML. It invents no new data concepts. Everything else is a small,
94
+ > closed set covering what any browser app needs: navigation, request
95
+ > status, and showing what changed between results.
96
+
97
+ Supporting points:
98
+
99
+ - The developer of a server-bound app must know rows, keys, SELECT and
100
+ DML regardless of framework. That knowledge is essential complexity.
101
+ Applying it to HTML adds a mapping, not new concepts.
102
+ - The same SQL-shape decision that limits the vocabulary keeps the
103
+ browser processor finite, which is what fits it in the 300ms budget.
104
+ - The non-SQL families today: navigation (`lb-url*`, `lb-path`,
105
+ `lb-page*`), request lifecycle (`lb-request-*`), changes between
106
+ result sets (`lb-row-live/leaving/moved/changed`, `lb-group-live/leaving`),
107
+ and structure (`lb-hub`, `lb-exp-template`, `lb-exp-slot`, `lb-release`).
108
+ - The accidents most likely to accumulate are in the mapping rules SQL
109
+ does not dictate: which element a row becomes, what a re-landing does,
110
+ how a key identifies an element.
111
+ - Design rule: a new term must be a SQL concept or belong to a named
112
+ family. A new family, or growth in the non-SQL families, is the
113
+ signal the critique is starting to land.