@abide/abide 0.103.0 → 0.105.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/CHANGELOG.md +29 -0
- package/bin/abide.ts +80 -173
- package/docs/REGISTRY.md +27 -27
- package/docs/RULEBOOK.md +68 -24
- package/docs/abide.md +31 -46
- package/package.json +1 -1
- package/src/compiler/Lowering.ts +15 -5
- package/src/compiler/Refusals.ts +57 -0
- package/src/compiler/TypeSession.ts +46 -89
- package/src/compiler/appLayout.ts +447 -0
- package/src/compiler/appMap.ts +21 -25
- package/src/compiler/checkApp.ts +61 -97
- package/src/compiler/checkedShadow.ts +12 -10
- package/src/compiler/classifyRead.ts +3 -6
- package/src/compiler/compileApp.ts +183 -478
- package/src/compiler/componentGraph.ts +2 -71
- package/src/compiler/declaredExports.ts +79 -60
- package/src/compiler/emitClient.ts +11 -13
- package/src/compiler/emitComponent.ts +21 -6
- package/src/compiler/emitSource.ts +20 -20
- package/src/compiler/handlersUnder.ts +46 -36
- package/src/compiler/hookCalls.ts +152 -0
- package/src/compiler/mountsOf.ts +31 -54
- package/src/compiler/projectShadow.ts +1 -1
- package/src/compiler/readSources.ts +27 -42
- package/src/compiler/readsIn.ts +5 -16
- package/src/compiler/resolverIn.ts +77 -0
- package/src/server/abideModules.ts +152 -146
- package/src/server/answerRequest.ts +18 -4
- package/src/server/answerSocket.ts +136 -76
- package/src/server/answerValue.ts +28 -15
- package/src/server/appTree.ts +59 -73
- package/src/server/claudeApi.ts +2 -5
- package/src/server/clientBundle.ts +198 -0
- package/src/server/coerceArgs.ts +1 -1
- package/src/server/commands.ts +93 -57
- package/src/server/createApp.ts +90 -16
- package/src/server/deferredBuilds.ts +175 -0
- package/src/server/devWorker.ts +107 -319
- package/src/server/faceOf.ts +120 -0
- package/src/server/fenceServer.ts +10 -18
- package/src/server/generatedSurfaces.ts +12 -53
- package/src/server/index.ts +3 -0
- package/src/server/placement.ts +167 -0
- package/src/server/remote.ts +14 -42
- package/src/server/render.ts +25 -17
- package/src/server/renderComponent.ts +9 -5
- package/src/server/requestScope.ts +10 -4
- package/src/server/responseRelay.ts +389 -0
- package/src/server/runCompiled.ts +9 -12
- package/src/server/runDesktop.ts +6 -9
- package/src/server/serveBuild.ts +26 -73
- package/src/server/serveDev.ts +19 -176
- package/src/server/streamEvent.ts +29 -0
- package/src/server/versionOf.ts +1 -2
- package/src/server/withAbideSources.ts +2 -1
- package/src/shared/HandlerRoute.ts +68 -0
- package/src/shared/REFUSAL_KEY.ts +5 -0
- package/src/shared/RESERVED_DIRECTORIES.ts +6 -0
- package/src/shared/answerOf.ts +105 -78
- package/src/shared/filledPath.ts +1 -9
- package/src/shared/framing.ts +127 -128
- package/src/shared/queryText.ts +15 -7
- package/src/shared/reactive/Cursor.ts +69 -124
- package/src/shared/reactive/Reactive.ts +296 -162
- package/src/shared/reactive/callLoad.ts +4 -18
- package/src/shared/reactive/index.ts +11 -8
- package/src/shared/reactive/pairReactive.ts +1 -1
- package/src/shared/reactive/scoped.ts +4 -3
- package/src/shared/reactive/share.ts +11 -1
- package/src/shared/routePattern.ts +0 -110
- package/src/shared/rpc.ts +17 -24
- package/src/shared/rpcRequest.ts +18 -47
- package/src/shared/takeEarlyCall.ts +12 -37
- package/src/shared/url.ts +1 -1
- package/src/ui/Level.ts +276 -0
- package/src/ui/List.ts +17 -17
- package/src/ui/Router.ts +20 -87
- package/src/ui/applyHead.ts +92 -79
- package/src/ui/index.ts +4 -2
- package/src/ui/reconcile.ts +4 -110
- package/src/ui/rows.ts +123 -2
- package/src/ui/wire.ts +12 -27
- package/src/compiler/hooksCalled.ts +0 -35
- package/src/server/clientBuildOptions.ts +0 -54
- package/src/server/refuseAddressCollision.ts +0 -37
- package/src/shared/reactive/chunksOfStream.ts +0 -57
- package/src/ui/MOUNTING_LEVEL.ts +0 -18
package/src/ui/applyHead.ts
CHANGED
|
@@ -4,32 +4,57 @@
|
|
|
4
4
|
// the document. Nothing is added to the head to mark ownership — a marker would be a fact about
|
|
5
5
|
// the framework in a document an author writes, and the key is already what decides.
|
|
6
6
|
//
|
|
7
|
-
// 39.5 — A CONTRIBUTION WITH NO KEY APPENDS. It cannot replace anything
|
|
8
|
-
// it could be said to be the same as, and dropping it instead would lose
|
|
9
|
-
// contribution a page makes several of.
|
|
7
|
+
// 39.5 — A CONTRIBUTION WITH NO KEY APPENDS. It cannot replace anything ANOTHER contributor wrote,
|
|
8
|
+
// because there is nothing it could be said to be the same as, and dropping it instead would lose
|
|
9
|
+
// the one kind of contribution a page makes several of. What its OWN contributor wrote last is
|
|
10
|
+
// another matter: a head written again puts its keyless nodes where the last ones stood (39.15),
|
|
11
|
+
// and a head withdrawn takes them with it (39.16). Found the way a key is — off the document, by
|
|
12
|
+
// the markup the contributor last wrote — so a head the server painted is found the same as one
|
|
13
|
+
// written here.
|
|
10
14
|
|
|
11
15
|
import { headKeyOf } from '#shared/headKeyOf.ts'
|
|
12
|
-
import { MOUNTING_LEVEL } from './MOUNTING_LEVEL.ts'
|
|
13
16
|
import { markupNodes } from './parseMarkup.ts'
|
|
14
|
-
import { isEmptyRegion, type Region } from './regions.ts'
|
|
15
|
-
import type { Scope } from './Scope.ts'
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Writes `markup`'s contributions into the document head, a key `below` holds excepted.
|
|
20
|
+
*
|
|
21
|
+
* 39.4 — `below` is the head of every contributor deeper than the one writing, which a head
|
|
22
|
+
* re-applied live (39.13) must not undo: the deepest contributor wins whichever of them moved last.
|
|
23
|
+
*
|
|
24
|
+
* 39.15 — `last` is what the same contributor wrote before, whose keyless nodes this replaces in
|
|
25
|
+
* place, in order, rather than adding beside them. 39.14 — a key `last` held that `markup` does not
|
|
26
|
+
* is withdrawn to `outer`'s value for it, as a leaving contributor's is; a key list that did not
|
|
27
|
+
* move withdraws nothing and never asks `outer`.
|
|
28
|
+
*/
|
|
29
|
+
export function applyHead(
|
|
30
|
+
markup: string,
|
|
31
|
+
below = '',
|
|
32
|
+
last = '',
|
|
33
|
+
outer: () => string = () => '',
|
|
34
|
+
): void {
|
|
18
35
|
const head = document.head
|
|
19
36
|
if (markup === '') return
|
|
20
37
|
const incoming = markupNodes(head, markup)
|
|
21
38
|
const standingByKey = standingKeys(head)
|
|
39
|
+
const deeper = below === '' ? undefined : keyedIn(head, below)
|
|
40
|
+
const replaced: Node[] = []
|
|
41
|
+
const withdrawn = keyedIn(head, last, replaced)
|
|
42
|
+
let next = 0
|
|
22
43
|
for (const node of incoming) {
|
|
23
|
-
|
|
24
|
-
head.appendChild(node)
|
|
25
|
-
continue
|
|
26
|
-
}
|
|
27
|
-
const element = node as Element
|
|
28
|
-
const key = headKey(element)
|
|
44
|
+
const key = node.nodeType === 1 ? headKey(node as Element) : undefined
|
|
29
45
|
if (key === undefined) {
|
|
30
|
-
|
|
46
|
+
const standing = replaced[next]
|
|
47
|
+
next += 1
|
|
48
|
+
if (standing === undefined) head.appendChild(node)
|
|
49
|
+
// The same node left standing, as a key's is: a `<style>` put back as itself is not
|
|
50
|
+
// re-parsed into the cascade.
|
|
51
|
+
else if (!standing.isEqualNode(node))
|
|
52
|
+
head.replaceChild(node, standing)
|
|
31
53
|
continue
|
|
32
54
|
}
|
|
55
|
+
const element = node as Element
|
|
56
|
+
withdrawn.delete(key)
|
|
57
|
+
if (deeper?.has(key) === true) continue
|
|
33
58
|
const standing = standingByKey.get(key)
|
|
34
59
|
if (standing === undefined) {
|
|
35
60
|
head.appendChild(element)
|
|
@@ -45,31 +70,52 @@ export function applyHead(markup: string): void {
|
|
|
45
70
|
head.replaceChild(element, standing)
|
|
46
71
|
standingByKey.set(key, element)
|
|
47
72
|
}
|
|
73
|
+
for (; next < replaced.length; next += 1)
|
|
74
|
+
(replaced[next] as Node).parentNode?.removeChild(replaced[next] as Node)
|
|
75
|
+
if (withdrawn.size === 0) return
|
|
76
|
+
if (deeper !== undefined)
|
|
77
|
+
for (const key of deeper.keys()) withdrawn.delete(key)
|
|
78
|
+
restoreKeys(head, withdrawn, outer())
|
|
48
79
|
}
|
|
49
80
|
|
|
50
81
|
/**
|
|
51
82
|
* Puts back, or removes, what the leaving levels keyed and the arriving ones do not.
|
|
52
83
|
*
|
|
53
|
-
* 39.14 — THE NEXT-OUTER CONTRIBUTOR'S VALUE, OR NONE. `leaving`
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
84
|
+
* 39.14 — THE NEXT-OUTER CONTRIBUTOR'S VALUE, OR NONE. `leaving` is the head of the levels a
|
|
85
|
+
* navigation stopped; `around` answers `outer`, every contributor standing above them, and
|
|
86
|
+
* `arriving`, the heads of the levels it mounted. `outer` runs outermost first — the shell's keyed
|
|
87
|
+
* entries, then each kept layout's — so the LAST entry for a key in it is the one the server would
|
|
88
|
+
* paint there (39.4). A component leaving its region is withdrawn the same way, its level's
|
|
89
|
+
* contributors either side of it standing in for the two (`Level.headsAround`). What the arriving levels key they have
|
|
90
|
+
* already written (39.6), so only the keys they are silent on are touched — and 39.16, the leaving
|
|
91
|
+
* levels' keyless nodes leave with them.
|
|
58
92
|
*
|
|
59
|
-
* THE COMMON NAVIGATION
|
|
60
|
-
*
|
|
93
|
+
* THE COMMON NAVIGATION WITHDRAWS NOTHING BY KEY: two pages that both key a `<title>` leave
|
|
94
|
+
* `outer` built but never parsed, and a head keying nothing never asks `around`.
|
|
61
95
|
*/
|
|
62
96
|
export function withdrawHead(
|
|
63
97
|
leaving: string,
|
|
64
|
-
arriving: string,
|
|
65
|
-
outer: () => string,
|
|
98
|
+
around: () => [outer: string, arriving: string],
|
|
66
99
|
): void {
|
|
67
100
|
if (leaving === '') return
|
|
68
101
|
const head = document.head
|
|
69
|
-
const
|
|
102
|
+
const keyless: Node[] = []
|
|
103
|
+
const withdrawn = keyedIn(head, leaving, keyless)
|
|
104
|
+
for (const node of keyless) node.parentNode?.removeChild(node)
|
|
105
|
+
if (withdrawn.size === 0) return
|
|
106
|
+
const [outer, arriving] = around()
|
|
70
107
|
for (const key of keyedIn(head, arriving).keys()) withdrawn.delete(key)
|
|
108
|
+
restoreKeys(head, withdrawn, outer)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 39.14 — each of `withdrawn`'s keys back to `outer`'s last value for it, or removed. */
|
|
112
|
+
function restoreKeys(
|
|
113
|
+
head: HTMLHeadElement,
|
|
114
|
+
withdrawn: Map<string, Element>,
|
|
115
|
+
outer: string,
|
|
116
|
+
): void {
|
|
71
117
|
if (withdrawn.size === 0) return
|
|
72
|
-
const restored = keyedIn(head, outer
|
|
118
|
+
const restored = keyedIn(head, outer)
|
|
73
119
|
const standingByKey = standingKeys(head)
|
|
74
120
|
for (const key of withdrawn.keys()) {
|
|
75
121
|
const standing = standingByKey.get(key)
|
|
@@ -82,15 +128,29 @@ export function withdrawHead(
|
|
|
82
128
|
}
|
|
83
129
|
}
|
|
84
130
|
|
|
85
|
-
/**
|
|
86
|
-
|
|
131
|
+
/**
|
|
132
|
+
* The keyed elements `markup` parses to, by key, the last holding each (39.4).
|
|
133
|
+
*
|
|
134
|
+
* Where `standing` is given, each keyless node is matched to the first node standing in the head
|
|
135
|
+
* equal to it and not already matched, and the match pushed there (39.15, 39.16).
|
|
136
|
+
*/
|
|
137
|
+
function keyedIn(
|
|
138
|
+
head: HTMLHeadElement,
|
|
139
|
+
markup: string,
|
|
140
|
+
standing?: Node[],
|
|
141
|
+
): Map<string, Element> {
|
|
87
142
|
const keyed = new Map<string, Element>()
|
|
88
143
|
if (markup === '') return keyed
|
|
89
|
-
for (const node of markupNodes(head, markup))
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
144
|
+
for (const node of markupNodes(head, markup)) {
|
|
145
|
+
const key = node.nodeType === 1 ? headKey(node as Element) : undefined
|
|
146
|
+
if (key !== undefined) keyed.set(key, node as Element)
|
|
147
|
+
else if (standing !== undefined)
|
|
148
|
+
for (let at = head.firstChild; at !== null; at = at.nextSibling)
|
|
149
|
+
if (at.isEqualNode(node) && !standing.includes(at)) {
|
|
150
|
+
standing.push(at)
|
|
151
|
+
break
|
|
152
|
+
}
|
|
153
|
+
}
|
|
94
154
|
return keyed
|
|
95
155
|
}
|
|
96
156
|
|
|
@@ -130,50 +190,3 @@ function headKey(element: Element): string | undefined {
|
|
|
130
190
|
element.getAttribute(name),
|
|
131
191
|
)
|
|
132
192
|
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Keeps a level's head written from `markup` for as long as `scope` lives.
|
|
136
|
-
*
|
|
137
|
-
* 39.13 — A HEAD THAT READS A VALUE FOLLOWS IT for as long as its level is mounted, which is the
|
|
138
|
-
* scope's life. The first run only subscribes: the head it would write is already there, written
|
|
139
|
-
* by the server or applied by the level's mount ahead of its fill (39.6).
|
|
140
|
-
*/
|
|
141
|
-
export function liveHead(scope: Scope, markup: () => string): void {
|
|
142
|
-
let subscribing = true
|
|
143
|
-
scope.watch(() => {
|
|
144
|
-
const text = markup()
|
|
145
|
-
if (subscribing) subscribing = false
|
|
146
|
-
else applyHead(text)
|
|
147
|
-
})
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* A component's `<head>`, added to the page or layout the router is mounting it under (39.1).
|
|
152
|
-
*
|
|
153
|
-
* Joined to that level's head, so it is withdrawn as the level's is when it leaves (39.14), and
|
|
154
|
-
* deeper than the level's own, so it wins by key (39.4). Applied where the component builds its own
|
|
155
|
-
* markup, once the level is mounted (`MOUNTING_LEVEL.unapplied`); a region the server painted
|
|
156
|
-
* already carries the head the server wrote. Nothing where no level is mounting — a row grown after
|
|
157
|
-
* the fact titles nothing.
|
|
158
|
-
*
|
|
159
|
-
* A LIST, NOT A CLOSURE PER CONTRIBUTION: a `{#for}` of components with a `<head>` contributes once a
|
|
160
|
-
* row, and a chain of them is read through as deep a stack.
|
|
161
|
-
*/
|
|
162
|
-
export function contributeHead(region: Region, head: () => string): void {
|
|
163
|
-
const level = MOUNTING_LEVEL.current
|
|
164
|
-
if (level === undefined) return
|
|
165
|
-
let contributed = level.contributed
|
|
166
|
-
if (contributed === undefined) {
|
|
167
|
-
const own = level.head
|
|
168
|
-
const joined: (() => string)[] = []
|
|
169
|
-
contributed = joined
|
|
170
|
-
level.contributed = joined
|
|
171
|
-
level.head = () => {
|
|
172
|
-
let markup = own?.() ?? ''
|
|
173
|
-
for (const one of joined) markup += one()
|
|
174
|
-
return markup
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
contributed.push(head)
|
|
178
|
-
if (isEmptyRegion(region)) MOUNTING_LEVEL.unapplied += head()
|
|
179
|
-
}
|
package/src/ui/index.ts
CHANGED
|
@@ -39,15 +39,16 @@ export {
|
|
|
39
39
|
} from './adoptWalk.ts'
|
|
40
40
|
// 39.6 — a module mounted into an empty region applies its own head, from inside its instance
|
|
41
41
|
// (`emitClient`); only a module that carries its own markup and a `<head>` imports it.
|
|
42
|
-
export { applyHead
|
|
42
|
+
export { applyHead } from './applyHead.ts'
|
|
43
43
|
export { bindElement } from './bindElement.ts'
|
|
44
44
|
export { bindProperty } from './bindProperty.ts'
|
|
45
45
|
export { bindSpread } from './bindSpread.ts'
|
|
46
46
|
export { escapeless } from './escapeless.ts'
|
|
47
|
+
// 39.1 — a component's `<head>`, joined to the routed level mounting it.
|
|
48
|
+
export { contributeHead } from './Level.ts'
|
|
47
49
|
export { listAt, updateList } from './List.ts'
|
|
48
50
|
export { warnMismatch } from './Mismatch.ts'
|
|
49
51
|
export { markupNodes } from './parseMarkup.ts'
|
|
50
|
-
export { replaceRow } from './reconcile.ts'
|
|
51
52
|
export {
|
|
52
53
|
fillEmpty,
|
|
53
54
|
fillRegion,
|
|
@@ -55,6 +56,7 @@ export {
|
|
|
55
56
|
type Region,
|
|
56
57
|
} from './regions.ts'
|
|
57
58
|
export { rowKeyOf } from './rowKeyOf.ts'
|
|
59
|
+
export { replaceRow } from './rows.ts'
|
|
58
60
|
export { EMPTY_SCOPE, Scope } from './Scope.ts'
|
|
59
61
|
export { setAttribute } from './setAttribute.ts'
|
|
60
62
|
export { setClass } from './setClass.ts'
|
package/src/ui/reconcile.ts
CHANGED
|
@@ -1,71 +1,19 @@
|
|
|
1
1
|
import { PATCHES } from '#shared/reactive/PATCHES.ts'
|
|
2
2
|
import { LAYER_RECONCILE } from './LAYER_NAMES.ts'
|
|
3
|
-
import type { List
|
|
3
|
+
import type { List } from './List.ts'
|
|
4
4
|
import { HYDRATE_LOG } from './Mismatch.ts'
|
|
5
5
|
import { beginLayer, endLayer } from './measureLayer.ts'
|
|
6
|
-
import { moveRow,
|
|
7
|
-
import { EMPTY_SCOPE, type Scope } from './Scope.ts'
|
|
6
|
+
import { dropRow, makeRow, moveRow, type Row } from './rows.ts'
|
|
8
7
|
|
|
9
8
|
// 41.8 — a KEYED list, reconciled by moving the nodes that are already there rather than by
|
|
10
9
|
// rebuilding them. A rebuild produces the right screen and the wrong document: focus, selection,
|
|
11
10
|
// scroll position and anything a row was in the middle of go with the nodes that were replaced.
|
|
12
11
|
//
|
|
13
|
-
// What a row is
|
|
12
|
+
// What a row is — its record and its nodes, an element or an anchored range — is `rows.ts`'s. Most lists
|
|
14
13
|
// carry no per-row marker at all: the nodes between the region's anchors ARE the rows, and only a
|
|
15
14
|
// body with several top-level nodes gets one anchor per ROW. That is ~2.5 bytes gzipped a row, not
|
|
16
15
|
// the per-SLOT marker `benchGrid.ts` measured doubling a page.
|
|
17
16
|
|
|
18
|
-
export type Row = {
|
|
19
|
-
key: unknown
|
|
20
|
-
/**
|
|
21
|
-
* The value the row was built from.
|
|
22
|
-
*
|
|
23
|
-
* A row that survives a reorder keeps its NODES, which is the whole point — but if the value
|
|
24
|
-
* behind that key is a different object, its content is stale and the row is remade. Identity
|
|
25
|
-
* is the signal here, the inverse of the case where a freshly built wrapper defeats one.
|
|
26
|
-
*/
|
|
27
|
-
data: unknown
|
|
28
|
-
/**
|
|
29
|
-
* The row's first node: its element, or its `ROW_OPEN` anchor where the body has several —
|
|
30
|
-
* the start of a range, not the nodes `make` handed back (`rows.ts`).
|
|
31
|
-
*/
|
|
32
|
-
node: ChildNode
|
|
33
|
-
/** The row's bindings, stopped when it leaves. */
|
|
34
|
-
scope: Scope
|
|
35
|
-
/**
|
|
36
|
-
* Where this row stood in the list the document currently shows.
|
|
37
|
-
*
|
|
38
|
-
* The placement walk needs "already in order" to be a question about POSITIONS rather than
|
|
39
|
-
* about nodes, and asking the document — `compareDocumentPosition`, or counting siblings — is
|
|
40
|
-
* the walk this exists to avoid. Written by every reconcile, so it is a fact about the last one
|
|
41
|
-
* rather than about the caller, and a hydration seed sets it because that IS the order the
|
|
42
|
-
* server painted.
|
|
43
|
-
*/
|
|
44
|
-
at: number
|
|
45
|
-
/**
|
|
46
|
-
* The patch epoch the row was made at (32.33).
|
|
47
|
-
*
|
|
48
|
-
* Its item written past this is content the row never saw, under the same object a kept row is
|
|
49
|
-
* matched by.
|
|
50
|
-
*/
|
|
51
|
-
seen: number
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Rebuilds a row in place from `next`, keeping its record.
|
|
56
|
-
*
|
|
57
|
-
* 41.10 — a row that cannot be hydrated is rebuilt: the fresh nodes go where the row stood and the
|
|
58
|
-
* row's record takes the new first node, so the reconcile still reaches all of it.
|
|
59
|
-
*/
|
|
60
|
-
export function replaceRow(row: Row, next: Node[]): void {
|
|
61
|
-
const first = row.node
|
|
62
|
-
const parent = first.parentNode
|
|
63
|
-
if (parent === null) return
|
|
64
|
-
for (const node of next) parent.insertBefore(node, first)
|
|
65
|
-
removeRow(first)
|
|
66
|
-
row.node = next[0] as ChildNode
|
|
67
|
-
}
|
|
68
|
-
|
|
69
17
|
/**
|
|
70
18
|
* Reconciles a keyed list's rows to `data`, moving survivors rather than remaking them.
|
|
71
19
|
*
|
|
@@ -193,7 +141,7 @@ export function reconcile(list: List, data: unknown[]): void {
|
|
|
193
141
|
const existing = survivor[at]
|
|
194
142
|
if (existing === undefined || existing === null) {
|
|
195
143
|
const row = makeRow(
|
|
196
|
-
list,
|
|
144
|
+
list.setUp,
|
|
197
145
|
parent,
|
|
198
146
|
anchor,
|
|
199
147
|
keys[at],
|
|
@@ -234,50 +182,6 @@ export function reconcile(list: List, data: unknown[]): void {
|
|
|
234
182
|
}
|
|
235
183
|
}
|
|
236
184
|
|
|
237
|
-
/** A new row, built and inserted before `before`. */
|
|
238
|
-
export function makeRow(
|
|
239
|
-
list: List,
|
|
240
|
-
parent: Node,
|
|
241
|
-
before: Node,
|
|
242
|
-
key: unknown,
|
|
243
|
-
item: unknown,
|
|
244
|
-
at: number,
|
|
245
|
-
): Row {
|
|
246
|
-
// 14.8 — set up ONCE for the row, and the same builders make it and mount it.
|
|
247
|
-
const builders = list.setUp(item, at)
|
|
248
|
-
const nodes = builders.make(parent.ownerDocument as Document, item, at)
|
|
249
|
-
for (const node of nodes) parent.insertBefore(node, before)
|
|
250
|
-
return rowOf(builders, key, item, nodes[0] as ChildNode, at)
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* A row record over nodes already in the document, mounted.
|
|
255
|
-
*
|
|
256
|
-
* The one place a row's record is written, so its shape is fixed once for every path that makes one
|
|
257
|
-
* — a made row and a hydrated one alike. The mount is handed the RECORD, because a row it rebuilds
|
|
258
|
-
* (41.10) has a new first node to write back.
|
|
259
|
-
*/
|
|
260
|
-
export function rowOf(
|
|
261
|
-
builders: RowBuilders,
|
|
262
|
-
key: unknown,
|
|
263
|
-
item: unknown,
|
|
264
|
-
node: ChildNode,
|
|
265
|
-
at: number,
|
|
266
|
-
): Row {
|
|
267
|
-
const row: Row = {
|
|
268
|
-
key,
|
|
269
|
-
data: item,
|
|
270
|
-
node,
|
|
271
|
-
// A `Scope` rather than `undefined` until the mount returns, so the field holds one class
|
|
272
|
-
// for the record's whole life.
|
|
273
|
-
scope: EMPTY_SCOPE,
|
|
274
|
-
at,
|
|
275
|
-
seen: PATCHES.epoch,
|
|
276
|
-
}
|
|
277
|
-
row.scope = builders.mount(row, item, at)
|
|
278
|
-
return row
|
|
279
|
-
}
|
|
280
|
-
|
|
281
185
|
/**
|
|
282
186
|
* Whether a patch wrote to the row's item since the row was made.
|
|
283
187
|
*
|
|
@@ -291,16 +195,6 @@ function patchedSince(row: Row): boolean {
|
|
|
291
195
|
return (PATCHES.written.get(item) ?? 0) > row.seen
|
|
292
196
|
}
|
|
293
197
|
|
|
294
|
-
/**
|
|
295
|
-
* Stops a row's bindings, then removes its nodes.
|
|
296
|
-
*
|
|
297
|
-
* Stopped first, so a binding cannot wake over a node that is already gone.
|
|
298
|
-
*/
|
|
299
|
-
export function dropRow(row: Row): void {
|
|
300
|
-
row.scope.stop()
|
|
301
|
-
removeRow(row.node)
|
|
302
|
-
}
|
|
303
|
-
|
|
304
198
|
/**
|
|
305
199
|
* Warns about a repeated `{#for}` key, once per block.
|
|
306
200
|
*
|
package/src/ui/rows.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { REGION_CLOSE, ROW_OPEN } from '#shared/ANCHORS.ts'
|
|
2
|
+
import { PATCHES } from '#shared/reactive/PATCHES.ts'
|
|
2
3
|
import { leave } from './leave.ts'
|
|
3
4
|
import { moveNode } from './moveNode.ts'
|
|
4
5
|
import { closeOf, opensRegion, type Region, removeUntil } from './regions.ts'
|
|
6
|
+
import { EMPTY_SCOPE, type Scope } from './Scope.ts'
|
|
5
7
|
|
|
6
|
-
// WHAT A ROW IS
|
|
8
|
+
// WHAT A ROW IS — its record, how it is made, mounted, rebuilt and dropped, and what it is in the
|
|
9
|
+
// document — decided here and nowhere else. `reconcile.ts` decides only WHICH rows move. A row is its ELEMENT where the
|
|
7
10
|
// body has one; a body with several top-level nodes — `<dt>{k}</dt><dd>{v}</dd>` — is the RANGE
|
|
8
11
|
// from its `ROW_OPEN` anchor to the next row's anchor or the region's close, as a region is its
|
|
9
12
|
// pair. A range rather than the nodes `make` handed back: a region at the row's top level fills
|
|
@@ -12,6 +15,55 @@ import { closeOf, opensRegion, type Region, removeUntil } from './regions.ts'
|
|
|
12
15
|
// AN ANCHOR IS A COMMENT AND A SINGLE ROOT IS AN ELEMENT, so a row's first node says which kind of
|
|
13
16
|
// row it is and no flag is carried beside it.
|
|
14
17
|
|
|
18
|
+
export type Row = {
|
|
19
|
+
key: unknown
|
|
20
|
+
/**
|
|
21
|
+
* The value the row was built from.
|
|
22
|
+
*
|
|
23
|
+
* A row that survives a reorder keeps its NODES, which is the whole point — but if the value
|
|
24
|
+
* behind that key is a different object, its content is stale and the row is remade. Identity
|
|
25
|
+
* is the signal here, the inverse of the case where a freshly built wrapper defeats one.
|
|
26
|
+
*/
|
|
27
|
+
data: unknown
|
|
28
|
+
/**
|
|
29
|
+
* The row's first node: its element, or its `ROW_OPEN` anchor where the body has several —
|
|
30
|
+
* the start of a range, not the nodes `make` handed back.
|
|
31
|
+
*/
|
|
32
|
+
node: ChildNode
|
|
33
|
+
/** The row's bindings, stopped when it leaves. */
|
|
34
|
+
scope: Scope
|
|
35
|
+
/**
|
|
36
|
+
* Where this row stood in the list the document currently shows.
|
|
37
|
+
*
|
|
38
|
+
* The placement walk needs "already in order" to be a question about POSITIONS rather than
|
|
39
|
+
* about nodes, and asking the document — `compareDocumentPosition`, or counting siblings — is
|
|
40
|
+
* the walk this exists to avoid. Written by every reconcile, so it is a fact about the last one
|
|
41
|
+
* rather than about the caller, and a hydration seed sets it because that IS the order the
|
|
42
|
+
* server painted.
|
|
43
|
+
*/
|
|
44
|
+
at: number
|
|
45
|
+
/**
|
|
46
|
+
* The patch epoch the row was made at (32.33).
|
|
47
|
+
*
|
|
48
|
+
* Its item written past this is content the row never saw, under the same object a kept row is
|
|
49
|
+
* matched by.
|
|
50
|
+
*/
|
|
51
|
+
seen: number
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type RowBuilders = {
|
|
55
|
+
/**
|
|
56
|
+
* A row's nodes, BUILT in the document the list stands in — handed over by the reconcile
|
|
57
|
+
* rather than found by the row, which is the one place a row's document is decided.
|
|
58
|
+
*/
|
|
59
|
+
make: (document: Document, item: unknown, at: number) => Node[]
|
|
60
|
+
/**
|
|
61
|
+
* Handed the row's RECORD rather than its nodes: a row it rebuilds (41.10) writes its new
|
|
62
|
+
* first node back there.
|
|
63
|
+
*/
|
|
64
|
+
mount: (row: Row, item: unknown, at: number) => Scope
|
|
65
|
+
}
|
|
66
|
+
|
|
15
67
|
/**
|
|
16
68
|
* The anchor ending the anchored row whose first node is `first`: the next `ROW_OPEN` or
|
|
17
69
|
* `REGION_CLOSE` at the row's own depth, or null where there is none in this parent.
|
|
@@ -37,7 +89,7 @@ function rowEnd(first: Node): Comment | null {
|
|
|
37
89
|
}
|
|
38
90
|
|
|
39
91
|
/** Removes a row's nodes, from its first node to its end. */
|
|
40
|
-
|
|
92
|
+
function removeRow(first: ChildNode): void {
|
|
41
93
|
if (first.nodeType !== 8) {
|
|
42
94
|
leave(first)
|
|
43
95
|
return
|
|
@@ -97,3 +149,72 @@ export function rowsIn(region: Region, anchored: boolean): ChildNode[] {
|
|
|
97
149
|
if (node.nodeType === 1) found.push(node)
|
|
98
150
|
return found
|
|
99
151
|
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Rebuilds a row in place from `next`, keeping its record.
|
|
155
|
+
*
|
|
156
|
+
* 41.10 — a row that cannot be hydrated is rebuilt: the fresh nodes go where the row stood and the
|
|
157
|
+
* row's record takes the new first node, so the reconcile still reaches all of it.
|
|
158
|
+
*/
|
|
159
|
+
export function replaceRow(row: Row, next: Node[]): void {
|
|
160
|
+
const first = row.node
|
|
161
|
+
const parent = first.parentNode
|
|
162
|
+
if (parent === null) return
|
|
163
|
+
for (const node of next) parent.insertBefore(node, first)
|
|
164
|
+
removeRow(first)
|
|
165
|
+
row.node = next[0] as ChildNode
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** A new row, built and inserted before `before`. */
|
|
169
|
+
export function makeRow(
|
|
170
|
+
setUp: (item: unknown, at: number) => RowBuilders,
|
|
171
|
+
parent: Node,
|
|
172
|
+
before: Node,
|
|
173
|
+
key: unknown,
|
|
174
|
+
item: unknown,
|
|
175
|
+
at: number,
|
|
176
|
+
): Row {
|
|
177
|
+
// 14.8 — set up ONCE for the row, and the same builders make it and mount it.
|
|
178
|
+
const builders = setUp(item, at)
|
|
179
|
+
const nodes = builders.make(parent.ownerDocument as Document, item, at)
|
|
180
|
+
for (const node of nodes) parent.insertBefore(node, before)
|
|
181
|
+
return rowOf(builders, key, item, nodes[0] as ChildNode, at)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* A row record over nodes already in the document, mounted.
|
|
186
|
+
*
|
|
187
|
+
* The one place a row's record is written, so its shape is fixed once for every path that makes one
|
|
188
|
+
* — a made row and a hydrated one alike. The mount is handed the RECORD, because a row it rebuilds
|
|
189
|
+
* (41.10) has a new first node to write back.
|
|
190
|
+
*/
|
|
191
|
+
export function rowOf(
|
|
192
|
+
builders: RowBuilders,
|
|
193
|
+
key: unknown,
|
|
194
|
+
item: unknown,
|
|
195
|
+
node: ChildNode,
|
|
196
|
+
at: number,
|
|
197
|
+
): Row {
|
|
198
|
+
const row: Row = {
|
|
199
|
+
key,
|
|
200
|
+
data: item,
|
|
201
|
+
node,
|
|
202
|
+
// A `Scope` rather than `undefined` until the mount returns, so the field holds one class
|
|
203
|
+
// for the record's whole life.
|
|
204
|
+
scope: EMPTY_SCOPE,
|
|
205
|
+
at,
|
|
206
|
+
seen: PATCHES.epoch,
|
|
207
|
+
}
|
|
208
|
+
row.scope = builders.mount(row, item, at)
|
|
209
|
+
return row
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Stops a row's bindings, then removes its nodes.
|
|
214
|
+
*
|
|
215
|
+
* Stopped first, so a binding cannot wake over a node that is already gone.
|
|
216
|
+
*/
|
|
217
|
+
export function dropRow(row: Row): void {
|
|
218
|
+
row.scope.stop()
|
|
219
|
+
removeRow(row.node)
|
|
220
|
+
}
|
package/src/ui/wire.ts
CHANGED
|
@@ -17,10 +17,11 @@
|
|
|
17
17
|
// An entry of its own (`@abide/abide/wire`) rather than a name on the runtime barrel: only a generated
|
|
18
18
|
// wrapper imports it, and a page with no handler pays nothing for it.
|
|
19
19
|
|
|
20
|
-
import { answered, answerOf } from '#shared/answerOf.ts'
|
|
20
|
+
import { type Answer, answered, answerOf } from '#shared/answerOf.ts'
|
|
21
21
|
import type { CopyOptions } from '#shared/CopyOptions.ts'
|
|
22
22
|
import { isFailed } from '#shared/Failed.ts'
|
|
23
|
-
import { answerChunks
|
|
23
|
+
import { answerChunks } from '#shared/framing.ts'
|
|
24
|
+
import { HandlerRoute } from '#shared/HandlerRoute.ts'
|
|
24
25
|
import { LOAD_SIGNAL } from '#shared/LOAD_SIGNAL.ts'
|
|
25
26
|
import { reactive, structural } from '#shared/reactive/index.ts'
|
|
26
27
|
import {
|
|
@@ -29,8 +30,6 @@ import {
|
|
|
29
30
|
} from '#shared/reactive/parameterDefaults.ts'
|
|
30
31
|
import { declare, type Method } from '#shared/rpc.ts'
|
|
31
32
|
import { rpcRequest } from '#shared/rpcRequest.ts'
|
|
32
|
-
import type { Answer } from '#shared/takeEarlyCall.ts'
|
|
33
|
-
import { versionInEtag } from '#shared/versionInEtag.ts'
|
|
34
33
|
import { followRpc } from './socketWire.ts'
|
|
35
34
|
|
|
36
35
|
/** The browser's `Rpc` for a handler: the server's declaration over a body that fetches. */
|
|
@@ -42,6 +41,8 @@ export function wire(
|
|
|
42
41
|
// 11.29 — and the defaults its parameter writes, which the copy keys by as the server does.
|
|
43
42
|
defaults?: Defaults,
|
|
44
43
|
): unknown {
|
|
44
|
+
// 16.9 — the handler's route, compiled once for every call through the copy.
|
|
45
|
+
const route = new HandlerRoute(address)
|
|
45
46
|
// ONE PARAMETER, so the value is keyed by args whatever the handler declared (16.6's arity rule):
|
|
46
47
|
// an unkeyed handler is reached with no args, which is one key.
|
|
47
48
|
const body = (args: Record<string, unknown> | undefined) => {
|
|
@@ -51,11 +52,7 @@ export function wire(
|
|
|
51
52
|
follow(args, answer)
|
|
52
53
|
return answered(answer)
|
|
53
54
|
}
|
|
54
|
-
const answer = answerOf(
|
|
55
|
-
rpcRequest(method, address, args),
|
|
56
|
-
false,
|
|
57
|
-
signal,
|
|
58
|
-
)
|
|
55
|
+
const answer = answerOf(rpcRequest(method, route, args), false, signal)
|
|
59
56
|
return answer instanceof Promise ? answer.then(settle) : settle(answer)
|
|
60
57
|
}
|
|
61
58
|
// 16.32 — A STREAMING HANDLER'S COPY IS A STREAM OF ITS OWN, whose chunks land as the answer's
|
|
@@ -68,13 +65,13 @@ export function wire(
|
|
|
68
65
|
// this runs while the call that started the load still has its signal set.
|
|
69
66
|
const signal = LOAD_SIGNAL.current
|
|
70
67
|
const answer = await answerOf(
|
|
71
|
-
rpcRequest(method,
|
|
68
|
+
rpcRequest(method, route, args),
|
|
72
69
|
true,
|
|
73
70
|
signal,
|
|
74
71
|
)
|
|
75
72
|
follow(args, answer)
|
|
76
73
|
if (answer.body !== undefined && answer.body !== null) {
|
|
77
|
-
yield* answerChunks(answer.body, answer.
|
|
74
|
+
yield* answerChunks(answer.body, answer.framing)
|
|
78
75
|
return
|
|
79
76
|
}
|
|
80
77
|
const value = answered(answer)
|
|
@@ -88,22 +85,10 @@ export function wire(
|
|
|
88
85
|
// so a caller signed out since would be fed what this answer just refused it. Nor a binary,
|
|
89
86
|
// which the mux's JSON frames cannot carry.
|
|
90
87
|
const follow = (args: unknown, answer: Answer): void => {
|
|
91
|
-
if (
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
answer.status >= 300 ||
|
|
96
|
-
carriesBytes(answer.type)
|
|
97
|
-
)
|
|
98
|
-
return
|
|
99
|
-
const seconds = /max-age=(\d+)/.exec(answer.life ?? '')?.[1]
|
|
100
|
-
followRpc(
|
|
101
|
-
address,
|
|
102
|
-
args,
|
|
103
|
-
versionInEtag(answer.version),
|
|
104
|
-
seconds === undefined ? undefined : Number(seconds) * 1000,
|
|
105
|
-
() => (value as (args: unknown) => unknown)(args),
|
|
106
|
-
)
|
|
88
|
+
if (method === 'GET' && answer.followable)
|
|
89
|
+
followRpc(address, args, answer.version, answer.life, () =>
|
|
90
|
+
(value as (args: unknown) => unknown)(args),
|
|
91
|
+
)
|
|
107
92
|
}
|
|
108
93
|
// A KEYED VALUE OF ITS OWN where the handler's declares what its copy behaves by, so every entry —
|
|
109
94
|
// the copy a page walks — carries it: its key (32.32), its tags (11.43) and its comparator (5.3). A `GET` alone: a mutation's value is written and read once.
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { tokenize } from './tokenize.ts'
|
|
2
|
-
|
|
3
|
-
/** Every hook abide exports, by name. */
|
|
4
|
-
export const HOOK_NAMES: ReadonlySet<string> = new Set([
|
|
5
|
-
'middleware',
|
|
6
|
-
'onStart',
|
|
7
|
-
'onStop',
|
|
8
|
-
'onError',
|
|
9
|
-
'onConfig',
|
|
10
|
-
'onHealth',
|
|
11
|
-
'onPrincipal',
|
|
12
|
-
'onLauncher',
|
|
13
|
-
'onMenu',
|
|
14
|
-
])
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Each of abide's hook names called in `source`, in the order written: the name, then `(`, and not
|
|
18
|
-
* a declaration — a member call included, since `server.onStart(…)` is abide's hook all the same.
|
|
19
|
-
* Read, not run — what `abide routes` lists and the build's refusals of a hook called where it is
|
|
20
|
-
* never read (38.107) go by.
|
|
21
|
-
*/
|
|
22
|
-
export function hooksCalled(source: string): string[] {
|
|
23
|
-
const called: string[] = []
|
|
24
|
-
let previous = ''
|
|
25
|
-
let named: string | undefined
|
|
26
|
-
for (const token of tokenize(source)) {
|
|
27
|
-
if (named !== undefined && token.text === '(') called.push(named)
|
|
28
|
-
named =
|
|
29
|
-
HOOK_NAMES.has(token.text) && previous !== 'function'
|
|
30
|
-
? token.text
|
|
31
|
-
: undefined
|
|
32
|
-
previous = token.text
|
|
33
|
-
}
|
|
34
|
-
return called
|
|
35
|
-
}
|