@loadbare/app 0.18.2 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -17
- package/dist/build/assemble.d.ts +7 -1
- package/dist/build/assemble.d.ts.map +1 -1
- package/dist/build/assemble.js +81 -11
- package/dist/build/assemble.js.map +1 -1
- package/dist/build/cli.d.ts +3 -0
- package/dist/build/cli.d.ts.map +1 -1
- package/dist/build/cli.js +6 -1
- package/dist/build/cli.js.map +1 -1
- package/dist/build/declared.d.ts +25 -0
- package/dist/build/declared.d.ts.map +1 -0
- package/dist/build/declared.js +62 -0
- package/dist/build/declared.js.map +1 -0
- package/dist/build/pages.d.ts +6 -0
- package/dist/build/pages.d.ts.map +1 -1
- package/dist/build/pages.js +32 -12
- package/dist/build/pages.js.map +1 -1
- package/docs/comparison.md +697 -484
- package/docs/departures.md +63 -0
- package/docs/miswarts.md +140 -0
- package/docs/opinions.md +155 -0
- package/docs/reference/builder.md +20 -4
- package/docs/reference/chrome.md +45 -10
- package/docs/reference/css.md +0 -1
- package/docs/reference/custom-elements.md +3 -25
- package/docs/reference/data-binding.md +265 -48
- package/docs/reference/names.md +250 -0
- package/docs/reference/overview.md +6 -0
- package/docs/reference/page-files.md +77 -9
- package/docs/reference/server.md +54 -1
- package/docs/roadmap.md +55 -43
- package/docs/testing.md +62 -28
- package/docs/theory.md +348 -108
- package/docs/tutorials/040-displaying-data.md +1 -1
- package/package.json +1 -1
- package/skills/loadbare-app/SKILL.md +19 -13
- package/skills/loadbare-app/references/builder.md +20 -4
- package/skills/loadbare-app/references/chrome.md +45 -10
- package/skills/loadbare-app/references/css.md +0 -1
- package/skills/loadbare-app/references/custom-elements.md +3 -25
- package/skills/loadbare-app/references/data-binding.md +265 -48
- package/skills/loadbare-app/references/miswarts.md +140 -0
- package/skills/loadbare-app/references/names.md +250 -0
- package/skills/loadbare-app/references/overview.md +6 -0
- package/skills/loadbare-app/references/page-files.md +77 -9
- package/skills/loadbare-app/references/server.md +54 -1
- package/docs/TECHREF-1.0.md +0 -2312
- package/docs/possible-ideas.md +0 -314
- package/docs/theory-old.md +0 -839
- package/docs/what-does-loadbare-extend.md +0 -124
- package/skills/loadbare-app/references/TECHREF-1.0.md +0 -2312
package/docs/testing.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Testing
|
|
2
2
|
|
|
3
|
+
> LLM-authored and LLM-maintained. This is acceptable because the
|
|
4
|
+
> document is fact-based, describing tests that exist in this package, and
|
|
5
|
+
> is written to be consumed by an LLM working on the package.
|
|
6
|
+
|
|
3
7
|
A contributor's document, and a companion to [theory.md](./theory.md). It
|
|
4
8
|
describes how this package tests itself.
|
|
5
9
|
|
|
@@ -26,6 +30,23 @@ All four tiers run under `npm test` today and need no dependency that is not
|
|
|
26
30
|
already installed. A fifth environment — a real browser — is discussed at the
|
|
27
31
|
end and is not built.
|
|
28
32
|
|
|
33
|
+
## What the tests are evidence for
|
|
34
|
+
|
|
35
|
+
[Theory](./theory.md) makes three claims that can be checked, and this suite
|
|
36
|
+
carries evidence for two of them.
|
|
37
|
+
|
|
38
|
+
- **Correctness.** Most of what Theory's correctness section names is
|
|
39
|
+
tested here: rows landed by key, an unchanged row left alone with what the
|
|
40
|
+
user typed, an answer in the wrong shape left out, a write sent once, a
|
|
41
|
+
refusal shown on the controls it names, and a server that refuses what a
|
|
42
|
+
page did not declare.
|
|
43
|
+
- **The vocabulary rule.** The markup checks in tier 1 enforce it at build
|
|
44
|
+
time: an `lb-` attribute outside the set the developer writes is a build
|
|
45
|
+
that fails.
|
|
46
|
+
- **Performance.** No tier measures the 300ms round trip. Node and jsdom
|
|
47
|
+
have no network and no paint, so they are not evidence for it, and this
|
|
48
|
+
suite does not try.
|
|
49
|
+
|
|
29
50
|
## The runner
|
|
30
51
|
|
|
31
52
|
`node:test`, run through `tsx`:
|
|
@@ -78,17 +99,14 @@ the most of what Loadbare promises, so it is first.
|
|
|
78
99
|
- more than one `lb-exp-slot`
|
|
79
100
|
|
|
80
101
|
**Markup checks.** The assembled chrome and every page are checked before
|
|
81
|
-
anything ships
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
- a chrome without exactly one `<lb-hub>` and one empty `<main>` inside it,
|
|
90
|
-
and a page carrying either
|
|
91
|
-
- a widget script declaring an `lb` method Loadbare does not define
|
|
102
|
+
anything ships, against the list in
|
|
103
|
+
[The markup checks](./reference/data-binding.md#the-markup-checks). Each one
|
|
104
|
+
is a build that must not produce output. The checks of `lb-query` and
|
|
105
|
+
`lb-request` names take each page's declarations as `loadDeclared` reads
|
|
106
|
+
them, and `loadDeclared` is tested against real page modules: names read
|
|
107
|
+
off objects built with spreads and computed keys, nothing declared for a
|
|
108
|
+
page with no modules, nothing left in the output directory, and a module
|
|
109
|
+
that throws named in the error.
|
|
92
110
|
|
|
93
111
|
A page file's `<title>` is lifted out and stamped as `lb-page-title`.
|
|
94
112
|
|
|
@@ -118,7 +136,7 @@ Review the golden files rather than blessing them. `npm run test:golden -w
|
|
|
118
136
|
@loadbare/demo` rewrites them; the diff is the point, and a diff nobody read
|
|
119
137
|
is a test nobody ran.
|
|
120
138
|
|
|
121
|
-
The rest of the build — `assemble`, `elements`, `pages`, `styles`,
|
|
139
|
+
The rest of the build — `assemble`, `declared`, `elements`, `pages`, `styles`,
|
|
122
140
|
`package-css` — is tested the same way and in the same tier, since none of it
|
|
123
141
|
needs a browser either.
|
|
124
142
|
|
|
@@ -126,7 +144,7 @@ needs a browser either.
|
|
|
126
144
|
a temporary project, which carries a stand-in `@loadbare/app` pointing at
|
|
127
145
|
the hub's source so the test needs no prior build. It is tested for its
|
|
128
146
|
default `src` and `dist`, every file it writes, `--minify`, a failed build's
|
|
129
|
-
exit code, and `--watch` rebuilding on a change, skipping a file it does not
|
|
147
|
+
exit code, a name the page does not declare, and `--watch` rebuilding on a change, skipping a file it does not
|
|
130
148
|
watch, and surviving a failed build.
|
|
131
149
|
|
|
132
150
|
## Tier 2 — The engine
|
|
@@ -149,11 +167,22 @@ watch, and surviving a failed build.
|
|
|
149
167
|
operator away from silently reversing
|
|
150
168
|
- a handler returning `url()` answers with the `lb-url` item alone, and runs
|
|
151
169
|
no refresh set
|
|
170
|
+
- a `row` query that finds no row answers `null`
|
|
171
|
+
- at startup, a malformed order, an unknown column type, and a crud request
|
|
172
|
+
that refreshes its own `rows` query are refused
|
|
173
|
+
- an update or delete for several rows runs once per key, lays the patches
|
|
174
|
+
together, and refreshes once
|
|
175
|
+
- `onPageEnter` may return `url()` or `choices()`, and `onChoices` hears
|
|
176
|
+
every choice the user makes
|
|
177
|
+
- a refusal carries a message, columns or both, and passes through the
|
|
178
|
+
engine untouched
|
|
152
179
|
|
|
153
180
|
`tests/lb-express.test.ts` runs against a listening app: the page comes from
|
|
154
181
|
the path, the query parms reach `contextFor`, a `url()` answer is followed by
|
|
155
|
-
the page loaded at the new URL, an undeclared request is refused,
|
|
156
|
-
malformed body does not throw
|
|
182
|
+
the page loaded at the new URL, an undeclared request is refused, a
|
|
183
|
+
malformed body does not throw, an oversized body is refused, a refusal
|
|
184
|
+
answers 422 with its words, and any other throw answers 500 with nothing in
|
|
185
|
+
it.
|
|
157
186
|
|
|
158
187
|
## Tier 3 — Landing
|
|
159
188
|
|
|
@@ -187,6 +216,10 @@ evidence here.
|
|
|
187
216
|
groups and leaving them out of counts and aggregates (`lb-choices.test.ts`)
|
|
188
217
|
- `lb-show` moves an element into a template and back, and landing reaches
|
|
189
218
|
inside it
|
|
219
|
+
- a row whose values arrive as it already shows them is not filled again,
|
|
220
|
+
so what the user typed into it stays
|
|
221
|
+
- a `row` query answered with no row empties the columns it names and drops
|
|
222
|
+
its key
|
|
190
223
|
|
|
191
224
|
Two properties, written as loops rather than with a library. Applying the
|
|
192
225
|
same rows twice is applying them once. And rows reached through any sequence
|
|
@@ -252,6 +285,17 @@ What the hub is tested for here:
|
|
|
252
285
|
loaded there
|
|
253
286
|
- `hidden` comes off the body once a page has landed, including the page
|
|
254
287
|
that failed to load, and a link whose page fails to load marks no element
|
|
288
|
+
- an answer that returns after the user has moved to another URL does not
|
|
289
|
+
land
|
|
290
|
+
- focus goes to the first element the user can operate once a page's
|
|
291
|
+
queries land
|
|
292
|
+
- a form pointed at a row by `commandfor` fills from the row the hub holds,
|
|
293
|
+
and a row not yet entered sends no update or delete
|
|
294
|
+
- ticked `lb-select` rows travel as `keys`, and the selection clears once
|
|
295
|
+
the request succeeds
|
|
296
|
+
- a refusal lands its message on the element that committed and on each
|
|
297
|
+
control it names
|
|
298
|
+
- `lb-request-done` reaches the element that committed and its ancestors
|
|
255
299
|
|
|
256
300
|
Widgets are small and their logic is local, so jsdom carries them: a value
|
|
257
301
|
reaches the control the widget owns, the widget is a form-associated control
|
|
@@ -282,18 +326,8 @@ No browser test runner is installed and none of the following exists. They
|
|
|
282
326
|
are recorded here as the shape of the work, not as coverage:
|
|
283
327
|
|
|
284
328
|
1. a cold load fills `<main>` with no empty flash
|
|
285
|
-
2. the wizard never displays a step the server has
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
The last is the one worth a browser. Loadbare's central claim is that no
|
|
289
|
-
markup exists in the DOM that is not in view-source, and that the difference
|
|
290
|
-
between the two is exactly the dynamic half. That is a property, it is stated
|
|
291
|
-
precisely in [theory.md](./theory.md), and it should be enforced mechanically
|
|
292
|
-
rather than believed: fetch the shell as text, compare the element set
|
|
293
|
-
against the live document after interaction, and assert that the only
|
|
294
|
-
additions are clones of row templates.
|
|
295
|
-
|
|
296
|
-
See [TODO.md](../TODO.md) for this and the rest of the open work.
|
|
329
|
+
2. the wizard page in `@loadbare/demo` never displays a step the server has
|
|
330
|
+
not confirmed
|
|
297
331
|
|
|
298
332
|
## Continuous integration
|
|
299
333
|
|
|
@@ -307,7 +341,7 @@ tiers here stay fast enough to run on every save.
|
|
|
307
341
|
## One change the tests want
|
|
308
342
|
|
|
309
343
|
The client reports through `console.error` and `console.warn` directly, from
|
|
310
|
-
|
|
344
|
+
more than twenty places across `hub/` and `@loadbare/widgets`. Routing those through a
|
|
311
345
|
single `report()` in `core/` would give the tests one seam to observe instead
|
|
312
346
|
of a global to mock per file, and would put the framework's error channel in
|
|
313
347
|
the same file as the rest of its vocabulary. It is a small change and it is
|
package/docs/theory.md
CHANGED
|
@@ -1,113 +1,353 @@
|
|
|
1
1
|
# The Theory of Loadbare/app
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
3
|
+
By Ken Downs
|
|
4
|
+
October 9, 2026
|
|
5
|
+
|
|
6
|
+
The primary goals of Loadbare/app are performance, correctness, and
|
|
7
|
+
developer ergonomics for server-bound applications. These goals motivate
|
|
8
|
+
all design and implementation decisions. The motivations cannot be
|
|
9
|
+
cleanly separated in implementation, they must all be true at once, and
|
|
10
|
+
often overlap and support each other.
|
|
11
|
+
|
|
12
|
+
Correctness: there is precise and perfect integrity between the database
|
|
13
|
+
and the screen as of the last round
|
|
14
|
+
trip. User writes hit the database once. Constraint violations are
|
|
15
|
+
intelligible and rendered in context.
|
|
16
|
+
|
|
17
|
+
Developer ergonomics means the simplest coding path results in an
|
|
18
|
+
application that is correct and performant. Getting the right
|
|
19
|
+
answer painted on the screen must be the lowest friction way to
|
|
20
|
+
write the code. This means many things, but it definitely rules
|
|
21
|
+
out elegant abstractions, excessive boilerplate and housekeeping code.
|
|
22
|
+
It must be difficult to express a solution that is slow or does not
|
|
23
|
+
maintain integrity between database and UI.
|
|
24
|
+
|
|
25
|
+
I admire elegance, but it is very dangerous when it becomes a goal
|
|
26
|
+
for its own sake. An elegant abstraction appeals from its own
|
|
27
|
+
design rather than from what it does for the user: it
|
|
28
|
+
hides the platform instead of mapping onto it, and it must be learned and
|
|
29
|
+
serviced forever.
|
|
30
|
+
|
|
31
|
+
From the user's perspective, performance is an aspect of correctness. A
|
|
32
|
+
slow round trip leaves the user wondering if what they see is correct. The
|
|
33
|
+
ideal user experience is certainty and trust in the application, which
|
|
34
|
+
requires a cumulative experience of an expectation:reward ratio of
|
|
35
|
+
exactly 1:1. All application behaviors that create doubt and
|
|
36
|
+
confusion erode the expectation:reward ratio, and must be removed
|
|
37
|
+
and made difficult to create by construction. A slow application cannot
|
|
38
|
+
provide an expectation:reward ratio of 1:1 because it is constantly
|
|
39
|
+
creating doubt.
|
|
40
|
+
|
|
41
|
+
## Currency is not a motivator
|
|
42
|
+
|
|
43
|
+
Currency is not a motivator for 1.0.
|
|
44
|
+
By currency, we mean the screen may be updated, absent any action by
|
|
45
|
+
the current user, due to the actions of other users or automated processes.
|
|
46
|
+
The class of apps that require currency are different in kind from those
|
|
47
|
+
that require only correctness. Unprompted updates may even violate
|
|
48
|
+
the Loadbare/app definition of correctness.
|
|
49
|
+
|
|
50
|
+
Currency can be considered when use cases present, and once it is
|
|
51
|
+
reconciled to correctness.
|
|
52
|
+
|
|
53
|
+
## From motivation to means
|
|
54
|
+
|
|
55
|
+
With the motivations firmly established, everything after is
|
|
56
|
+
technical decisions to establish the means.
|
|
57
|
+
|
|
58
|
+
This can be confusing, because sometimes a means and motivation
|
|
59
|
+
look the same. For example, I personally prefer to code static
|
|
60
|
+
HTML, and would like a data-driven framework that allows that.
|
|
61
|
+
It so happens Loadbare/app works that way. But it would be a
|
|
62
|
+
tedious and unproductive waste of words trying to determine if
|
|
63
|
+
that was a preference, motivation or means.
|
|
64
|
+
|
|
65
|
+
The three primary motivations force all design and implementation
|
|
66
|
+
decisions. All of my other preferences and motivations, such as
|
|
67
|
+
low boilerplate, declarative systems, static HTML and many others
|
|
68
|
+
were all worked in along the way, but always in a way that
|
|
69
|
+
supported the major three motivations.
|
|
70
|
+
|
|
71
|
+
I am happy to say I think it worked. In the sections below,
|
|
72
|
+
you will see how it was done. My own preferences and secondary
|
|
73
|
+
motivations will be plain to see in the decisions, but they
|
|
74
|
+
are always presented as what they are: a means to create a correct
|
|
75
|
+
and performant application with high developer ergonomics.
|
|
76
|
+
|
|
77
|
+
## The organizing principle
|
|
78
|
+
|
|
79
|
+
Every framework needs an organizing principle that determines implementation
|
|
80
|
+
decisions and the developer experience. For Loadbare/app, a very early
|
|
81
|
+
decision established that principle: Loadbare uses SQL vocabulary and
|
|
82
|
+
understands two SQL data shapes: a `row` or a set of `rows`. We speak
|
|
83
|
+
of rows, columns, keys, insert, update, and delete. We use the word
|
|
84
|
+
"query" when talking about named SQL SELECT statements.
|
|
85
|
+
|
|
86
|
+
Loadbare/app does not understand non-atomic column values. If a
|
|
87
|
+
document is retrieved from a store that embeds child rows into a
|
|
88
|
+
parent row, it must be converted into two separate query results.
|
|
89
|
+
|
|
90
|
+
A relational
|
|
91
|
+
expert will quickly point out that `rows` subsumes `row` and only
|
|
92
|
+
one shape is necessary, but in a UI they are not subsumed. A form
|
|
93
|
+
with its singleton form controls per column is expressed with
|
|
94
|
+
fundamentally different mechanics than a `<ul>`, `<select>` or
|
|
95
|
+
`<table>`. This is a UI framework, not a relational store, so the
|
|
96
|
+
data shapes are optimized for making user interfaces.
|
|
97
|
+
|
|
98
|
+
This one decision serves all three motivations. Two shapes and three
|
|
99
|
+
writes make a small and closed vocabulary, so coding the framework
|
|
100
|
+
is a finite and terminating task. For correctness, a finite task has
|
|
101
|
+
no open-ended edge cases to get wrong, and because every row arrives
|
|
102
|
+
with its key, the screen always knows exactly which row in the
|
|
103
|
+
database it is showing. For performance, every task the framework
|
|
104
|
+
must perform is itself finite, so each one can be measured and
|
|
105
|
+
optimized. For ergonomics, a developer of a database application must
|
|
106
|
+
already know rows, keys, SELECT and DML. Loadbare/app asks them to
|
|
107
|
+
map that knowledge onto HTML, not to learn new concepts.
|
|
108
|
+
|
|
109
|
+
This organizing principle also guards us from creating elegant
|
|
110
|
+
abstractions. Anything new must be a SQL concept, an HTML one, or a
|
|
111
|
+
member of a small, named family of things every browser application
|
|
112
|
+
needs, and an elegant abstraction is none of these. An elegant abstraction works against
|
|
113
|
+
all three motivations at once: it is a place where what the developer
|
|
114
|
+
wrote and what actually happens can drift apart, it adds work to every
|
|
115
|
+
round trip, and it is one more thing to learn and service instead of
|
|
116
|
+
the application. That last cost is where yak-shaving begins.
|
|
117
|
+
|
|
118
|
+
With this decision, Loadbare/app becomes a natural fit for SQL applications,
|
|
119
|
+
and can also be used if back-end data can be formatted to the two
|
|
120
|
+
shapes of `row` and `rows`.
|
|
121
|
+
|
|
122
|
+
## The performance goal
|
|
123
|
+
|
|
124
|
+
Because performance must be protected at all times, we need to
|
|
125
|
+
get specific about that first. All downstream decisions must know
|
|
126
|
+
the target they are protecting.
|
|
127
|
+
|
|
128
|
+
Research on response time gives two limits. Near 100ms, a response
|
|
129
|
+
feels instantaneous, as if caused directly by the user's action. Near
|
|
130
|
+
one second, the user's flow of thought begins to break (Miller 1968;
|
|
131
|
+
Card, Moran and Newell 1983; Nielsen 1993). Between the two, every
|
|
132
|
+
reduction pays, and the payoff grows sharply below a second (Doherty
|
|
133
|
+
and Thadhani 1982).
|
|
134
|
+
|
|
135
|
+
A server-bound application cannot reach 100ms, because the round trip
|
|
136
|
+
alone takes longer. So Loadbare/app sets its own target: a median of
|
|
137
|
+
300ms from click to paint, as close to instantaneous as a round trip
|
|
138
|
+
allows, and well inside the limit where doubt begins.
|
|
139
|
+
|
|
140
|
+
With a 300ms budget, we must allocate 200ms as typical wire time
|
|
141
|
+
when an application and user are in the same region, leaving 100ms
|
|
142
|
+
for everything that happens in the browser, the application server,
|
|
143
|
+
and the database.
|
|
144
|
+
|
|
145
|
+
The database's response time is not under the control of
|
|
146
|
+
Loadbare/app, and neither is the application's own server work,
|
|
147
|
+
such as authentication or picking up a connection from the pool.
|
|
148
|
+
So Loadbare/app must be thin, and get its job done in low
|
|
149
|
+
double-digit milliseconds, leaving as much of the 100ms as possible
|
|
150
|
+
to the work that belongs to the application.
|
|
151
|
+
|
|
152
|
+
This is where the organizing principle pays off a second time.
|
|
153
|
+
Having already ruled out elegant abstractions, we extend the rule to
|
|
154
|
+
reimplementations of browser behavior. The browser already parses
|
|
155
|
+
HTML, finds elements by selector, clones templates and manages focus,
|
|
156
|
+
and it does those things in native code faster than any framework
|
|
157
|
+
can do them again in JavaScript. Every millisecond Loadbare/app
|
|
158
|
+
spends redoing the browser's work is a millisecond taken from the
|
|
159
|
+
database.
|
|
160
|
+
|
|
161
|
+
## The correctness goal
|
|
162
|
+
|
|
163
|
+
Performance has a number, and we can hold a stopwatch to it.
|
|
164
|
+
Correctness has a definition instead, given at the top of this
|
|
165
|
+
document: integrity between database and screen as of the last round
|
|
166
|
+
trip, writes that hit the database once, and constraint violations
|
|
167
|
+
that are intelligible and rendered in context. We protect it by
|
|
168
|
+
making wrong states hard to reach, and by refusing or reporting the
|
|
169
|
+
ones we cannot rule out.
|
|
170
|
+
|
|
171
|
+
The foundation is an observation about data applications. Most
|
|
172
|
+
frameworks support a DOM that is fully mutable at any time for any
|
|
173
|
+
reason. But in a data application, the screen changes for one main
|
|
174
|
+
reason: the server answered with rows. The other changes, such as a
|
|
175
|
+
button waiting on its request, are a small and closed set. So
|
|
176
|
+
Loadbare/app changes the screen only from what the server sent, and
|
|
177
|
+
never ahead of it. There are no optimistic updates, and no guesses
|
|
178
|
+
about what the database probably did.
|
|
179
|
+
|
|
180
|
+
Because every row arrives with its key, the hub matches what arrived
|
|
181
|
+
to what is shown by key, never by position. A row that is gone
|
|
182
|
+
leaves, a row that changed is filled, and a row that did not change
|
|
183
|
+
is left alone, along with anything the user typed into it and has not
|
|
184
|
+
sent. An answer the server could not have meant for the current
|
|
185
|
+
screen is discarded: if the user moved to another page while a
|
|
186
|
+
request was in flight, the write still happened, but its answer does
|
|
187
|
+
not land on a page it says nothing about. When a query answers in a
|
|
188
|
+
shape it did not declare, the page goes without it. A page missing
|
|
189
|
+
one query is better than a page showing the wrong thing.
|
|
190
|
+
|
|
191
|
+
Writes hit the database once because a request comes from the
|
|
192
|
+
document, not from the developer's code. The developer names the
|
|
193
|
+
request, and the hub supplies the query and key from where the
|
|
194
|
+
element sits, so the developer cannot send the wrong key. An element
|
|
195
|
+
whose request is in flight ignores the next click, so a double click
|
|
196
|
+
cannot become a double write.
|
|
197
|
+
|
|
198
|
+
Constraints belong to the database, or to the application's own
|
|
199
|
+
server code. The framework's job is to carry the violation back to
|
|
200
|
+
the user in terms they can act on. A handler refuses a request with
|
|
201
|
+
words written for the user, either for the request as a whole or for
|
|
202
|
+
each column, and the browser shows them on the controls they belong
|
|
203
|
+
to. Anything else that goes wrong on the server reaches the browser
|
|
204
|
+
as a plain failure, so a stack trace or a database message never
|
|
205
|
+
lands in front of a user.
|
|
206
|
+
|
|
207
|
+
Finally, correctness by construction. Whatever can be checked before
|
|
208
|
+
the application runs is checked, and a mistake stops the build or the
|
|
209
|
+
server rather than shipping. The builder refuses a custom element it
|
|
210
|
+
cannot find and any `lb-` attribute the developer is not allowed to
|
|
211
|
+
write. The server refuses, at startup, patterns that are known to be
|
|
212
|
+
wrong, and refuses any request a page did not declare, so the wire
|
|
213
|
+
cannot reach anything the page has not published. What cannot be
|
|
214
|
+
checked ahead of time is reported on the console the moment it
|
|
215
|
+
happens, rather than silently papered over.
|
|
216
|
+
|
|
217
|
+
Each of these removes a source of doubt. Taken together, they are
|
|
218
|
+
how Loadbare/app keeps the expectation:reward ratio at 1:1 from the
|
|
219
|
+
correctness side, as the 300ms budget does from the performance side.
|
|
220
|
+
|
|
221
|
+
## Guarding the vocabulary
|
|
222
|
+
|
|
223
|
+
A small, clean vocabulary is a means, and it serves ergonomics first.
|
|
224
|
+
Every term a developer must learn that is not about their own
|
|
225
|
+
application is accidental complexity, time spent servicing the tool
|
|
226
|
+
instead of building the app. A messy vocabulary also hurts
|
|
227
|
+
correctness, because overlapping terms create edge cases, and edge
|
|
228
|
+
cases are where the framework and the developer disagree about what
|
|
229
|
+
the code means.
|
|
230
|
+
|
|
231
|
+
With apologies to Brooks and Pike:
|
|
232
|
+
|
|
233
|
+
> Essential complexity should dominate. Show me your abstractions and
|
|
234
|
+
> tooling and I shall remain mystified. Show me your tables and your
|
|
235
|
+
> rendering vocabulary and I shall be enlightened - the organization of
|
|
236
|
+
> the code will be self-evident.
|
|
237
|
+
|
|
238
|
+
A fair critic might say Loadbare/app does not have low accidental
|
|
239
|
+
complexity, only different accidental complexity. I take the
|
|
240
|
+
critique seriously, and it cannot be answered by counting terms. The
|
|
241
|
+
browser is inherently complex, so any complete solution will be
|
|
242
|
+
non-trivial, and vocabulary size alone is not a good proxy. What
|
|
243
|
+
matters more is where the terms come from.
|
|
244
|
+
|
|
245
|
+
Most of the vocabulary comes from SQL, which a developer of a
|
|
246
|
+
database application already knows:
|
|
247
|
+
|
|
75
248
|
- a `row`, with landing, insert, update, and delete
|
|
76
249
|
- `rows`, with landing, insert, update, and delete
|
|
77
250
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
251
|
+
The rest of it comes from HTML, which they also already know.
|
|
252
|
+
|
|
253
|
+
Not everything is SQL or HTML. Any browser application also needs
|
|
254
|
+
navigation, request status, and a way to show what changed between
|
|
255
|
+
one result and the next. These are small, closed families of their
|
|
256
|
+
own. So the rule is that a new term must be a SQL concept, an HTML
|
|
257
|
+
one, or belong to a named family. Any mechanism that is not
|
|
258
|
+
directly serving SQL, HTML or a named family is a mistake and will, sooner
|
|
259
|
+
or later, obstruct progress on the main motivations.
|
|
260
|
+
|
|
261
|
+
The real complexity comes when we map SQL onto HTML. HTML has a
|
|
262
|
+
larger surface area than SQL, but it is still finite. The accidents
|
|
263
|
+
most likely to pile up are in the mapping rules SQL does not dictate:
|
|
264
|
+
which element a row becomes, what happens when a row lands again, and
|
|
265
|
+
how a key identifies an element. That is where to look first when
|
|
266
|
+
the framework starts to feel like something you have to learn.
|
|
267
|
+
|
|
268
|
+
The same test applies to "best practices." If we end up with a list
|
|
269
|
+
of them, and they look like a bunch of stuff you have to learn, we
|
|
270
|
+
ask whether the practice already exists in SQL or HTML, and whether
|
|
271
|
+
it makes sense in the combination. If it does not, it is accidental
|
|
272
|
+
complexity, and the framework should absorb it or drop it.
|
|
273
|
+
|
|
274
|
+
The payoff is that the whole vocabulary fits in one technical
|
|
275
|
+
reference. That means it fits in a developer's head, and in an
|
|
276
|
+
LLM's context window.
|
|
277
|
+
|
|
278
|
+
## Conclusion
|
|
279
|
+
|
|
280
|
+
Loadbare/app is not for every application. It fits when the data is
|
|
281
|
+
rows, or can easily be shaped into rows, and when user actions
|
|
282
|
+
map to reading and writing those rows. It does not fit an
|
|
283
|
+
application whose data will not take the two shapes, and it does not
|
|
284
|
+
yet fit an application that needs currency. For a database
|
|
285
|
+
application, which describes a great many of the applications
|
|
286
|
+
people actually use at work, I believe it fits very well.
|
|
287
|
+
|
|
288
|
+
These are claims that can be checked. Performance has a
|
|
289
|
+
number: a median round trip of 300ms from click to paint.
|
|
290
|
+
Correctness has a definition: integrity between database and screen
|
|
291
|
+
as of the last round trip, writes that hit the database once, and
|
|
292
|
+
constraint violations rendered in context. The vocabulary has a
|
|
293
|
+
rule: every term is SQL, HTML, or a member of a named family. If
|
|
294
|
+
Loadbare/app misses the number, breaks the definition, or grows past
|
|
295
|
+
the rule, the theory has failed in a way anyone can see.
|
|
296
|
+
|
|
297
|
+
Code written for Loadbare/app should also last. Pages are HTML and
|
|
298
|
+
CSS, widgets are custom elements, the server is Express, and the
|
|
299
|
+
page code is queries against the application's own database. Each
|
|
300
|
+
of those is a standard or a mature tool in its own right, so code
|
|
301
|
+
written today is exposed to very little framework churn tomorrow.
|
|
302
|
+
As long as we have browsers, a Loadbare/app application should run.
|
|
303
|
+
|
|
304
|
+
The same properties matter more now that much code is written by
|
|
305
|
+
LLMs. SQL and HTML are about as well represented in an LLM's
|
|
306
|
+
training as anything can be, and the named families, navigation,
|
|
307
|
+
request status and showing what changed, are concepts every web
|
|
308
|
+
framework has. The model already knows nearly everything, so the
|
|
309
|
+
context we provide only has to spell out the mappings between them.
|
|
310
|
+
That is why the vocabulary fits in one reference, and one reference
|
|
311
|
+
fits in a context window. A framework that refuses mistakes at build time and at
|
|
312
|
+
startup catches the LLM's mistakes along with the developer's. The
|
|
313
|
+
correct application being the easiest one to write is just as true
|
|
314
|
+
when the writer is a model.
|
|
315
|
+
|
|
316
|
+
None of this is new to me. I first wrote this framework in 2003, as
|
|
317
|
+
Andromeda, before Node existed and before we had jQuery. The
|
|
318
|
+
foundation of Andromeda lives on in `@loadbare/db`, which makes the
|
|
319
|
+
data correct by declaring business logic in the schema. The
|
|
320
|
+
conviction behind Andromeda's web layer was that database
|
|
321
|
+
applications deserve a framework built for them. I am just as
|
|
322
|
+
convinced today, and `@loadbare/app` carries that correctness the
|
|
323
|
+
rest of the way, from the database to the paint.
|
|
324
|
+
|
|
325
|
+
So the whole theory comes down to this. Loadbare/app exists to make
|
|
326
|
+
a server-bound application correct, and to deliver that correctness
|
|
327
|
+
fast. The correct, fast application must also be the easiest one to
|
|
328
|
+
write. One early decision, that the framework speaks SQL and
|
|
329
|
+
understands only a `row` and a set of `rows`, serves all three at
|
|
330
|
+
once, and every other decision in this document is a means to the
|
|
331
|
+
same end.
|
|
332
|
+
|
|
333
|
+
## Appendix: Opinions, Departures and Miswarts
|
|
334
|
+
|
|
335
|
+
In an imperfect world, well defined rules do not always converge to
|
|
336
|
+
perfection. Loadbare/app makes two significant
|
|
337
|
+
[departures](./departures.md) from what this document implies, in
|
|
338
|
+
navigation and on the wire, and a few [miswarts](./miswarts.md)
|
|
339
|
+
appeared during development.
|
|
340
|
+
|
|
341
|
+
The decisions in this document force some positions on the developer.
|
|
342
|
+
They are listed in one place as [opinions](./opinions.md).
|
|
343
|
+
|
|
344
|
+
## References
|
|
345
|
+
|
|
346
|
+
- Card, S. K., Moran, T. P. and Newell, A. *The Psychology of
|
|
347
|
+
Human-Computer Interaction.* Lawrence Erlbaum, 1983.
|
|
348
|
+
- Doherty, W. J. and Thadhani, A. J. "The Economic Value of Rapid
|
|
349
|
+
Response Time." IBM, November 1982.
|
|
350
|
+
- Miller, R. B. "Response time in man-computer conversational
|
|
351
|
+
transactions." *Proceedings of the AFIPS Fall Joint Computer
|
|
352
|
+
Conference*, 1968.
|
|
353
|
+
- Nielsen, J. *Usability Engineering*, chapter 5. Academic Press, 1993.
|
|
@@ -130,7 +130,7 @@ requests to the correct page code.
|
|
|
130
130
|
import express from "express";
|
|
131
131
|
import { readFileSync } from "node:fs";
|
|
132
132
|
import { hubRoutes } from "@loadbare/app/express";
|
|
133
|
-
import { hub } from "./dist/pages";
|
|
133
|
+
import { hub } from "./dist/pages.ts";
|
|
134
134
|
import { openDb } from "./src/database";
|
|
135
135
|
|
|
136
136
|
const app = express();
|