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