@abide/abide 0.104.0 → 0.105.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/bin/abide.ts +80 -173
- package/docs/REGISTRY.md +17 -17
- package/docs/RULEBOOK.md +50 -12
- package/docs/abide.md +9 -9
- package/package.json +1 -1
- package/src/compiler/Lowering.ts +15 -5
- package/src/compiler/Refusals.ts +57 -0
- package/src/compiler/TypeSession.ts +45 -86
- package/src/compiler/appLayout.ts +447 -0
- package/src/compiler/appMap.ts +21 -20
- package/src/compiler/checkApp.ts +61 -97
- package/src/compiler/checkedShadow.ts +11 -9
- package/src/compiler/classifyRead.ts +3 -6
- package/src/compiler/compileApp.ts +180 -512
- 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/readSources.ts +27 -42
- package/src/compiler/readsIn.ts +5 -16
- package/src/compiler/resolverIn.ts +77 -0
- package/src/server/abideModules.ts +92 -123
- 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 +49 -71
- 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/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/withAbideSources.ts +2 -1
- package/src/shared/HandlerRoute.ts +68 -0
- package/src/shared/REFUSAL_KEY.ts +5 -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 +4 -2
- package/src/shared/reactive/Reactive.ts +285 -197
- 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 -123
- 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/leave.ts +5 -0
- package/src/ui/reconcile.ts +23 -104
- package/src/ui/regions.ts +27 -2
- 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 -33
- 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/leave.ts
CHANGED
|
@@ -23,6 +23,11 @@ export function holdWhileLeaving(
|
|
|
23
23
|
settling.then(done, done)
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/** Whether any element is leaving, so a removal has to ask each node whether it holds one. */
|
|
27
|
+
export function anyLeaving(): boolean {
|
|
28
|
+
return LEAVING.size !== 0
|
|
29
|
+
}
|
|
30
|
+
|
|
26
31
|
/** Removes `node`, or holds it where it is while an element it holds is leaving. */
|
|
27
32
|
export function leave(node: ChildNode): void {
|
|
28
33
|
if (LEAVING.size !== 0)
|
package/src/ui/reconcile.ts
CHANGED
|
@@ -1,71 +1,20 @@
|
|
|
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 {
|
|
7
|
-
import {
|
|
6
|
+
import { emptyRegion } from './regions.ts'
|
|
7
|
+
import { dropRow, makeRow, moveRow, type Row } from './rows.ts'
|
|
8
8
|
|
|
9
9
|
// 41.8 — a KEYED list, reconciled by moving the nodes that are already there rather than by
|
|
10
10
|
// rebuilding them. A rebuild produces the right screen and the wrong document: focus, selection,
|
|
11
11
|
// scroll position and anything a row was in the middle of go with the nodes that were replaced.
|
|
12
12
|
//
|
|
13
|
-
// What a row is
|
|
13
|
+
// What a row is — its record and its nodes, an element or an anchored range — is `rows.ts`'s. Most lists
|
|
14
14
|
// carry no per-row marker at all: the nodes between the region's anchors ARE the rows, and only a
|
|
15
15
|
// body with several top-level nodes gets one anchor per ROW. That is ~2.5 bytes gzipped a row, not
|
|
16
16
|
// the per-SLOT marker `benchGrid.ts` measured doubling a page.
|
|
17
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 (`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
18
|
/**
|
|
70
19
|
* Reconciles a keyed list's rows to `data`, moving survivors rather than remaking them.
|
|
71
20
|
*
|
|
@@ -73,6 +22,10 @@ export function replaceRow(row: Row, next: Node[]): void {
|
|
|
73
22
|
* survive are MOVED, never remade.
|
|
74
23
|
*/
|
|
75
24
|
export function reconcile(list: List, data: unknown[]): void {
|
|
25
|
+
if (data.length === 0) {
|
|
26
|
+
clear(list)
|
|
27
|
+
return
|
|
28
|
+
}
|
|
76
29
|
const region = list.region
|
|
77
30
|
const current = list.rows
|
|
78
31
|
const wanted = data.length
|
|
@@ -193,7 +146,7 @@ export function reconcile(list: List, data: unknown[]): void {
|
|
|
193
146
|
const existing = survivor[at]
|
|
194
147
|
if (existing === undefined || existing === null) {
|
|
195
148
|
const row = makeRow(
|
|
196
|
-
list,
|
|
149
|
+
list.setUp,
|
|
197
150
|
parent,
|
|
198
151
|
anchor,
|
|
199
152
|
keys[at],
|
|
@@ -234,48 +187,24 @@ export function reconcile(list: List, data: unknown[]): void {
|
|
|
234
187
|
}
|
|
235
188
|
}
|
|
236
189
|
|
|
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
190
|
/**
|
|
254
|
-
*
|
|
191
|
+
* Drops every row, emptying the region in one write rather than one removal a row.
|
|
255
192
|
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
193
|
+
* NOTHING WANTED IS ITS OWN SHAPE: there is no key to match and no order to keep, so the map,
|
|
194
|
+
* the survivor walk and the placement walk all answer nothing. Every row is still STOPPED first,
|
|
195
|
+
* as `dropRow` would, so a binding cannot wake over a node that is already gone.
|
|
259
196
|
*/
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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,
|
|
197
|
+
function clear(list: List): void {
|
|
198
|
+
const parent = list.region.close.parentNode
|
|
199
|
+
if (parent === null) return
|
|
200
|
+
beginLayer(LAYER_RECONCILE)
|
|
201
|
+
try {
|
|
202
|
+
for (const row of list.rows) row.scope.stop()
|
|
203
|
+
list.rows = []
|
|
204
|
+
emptyRegion(list.region, parent)
|
|
205
|
+
} finally {
|
|
206
|
+
endLayer(LAYER_RECONCILE)
|
|
276
207
|
}
|
|
277
|
-
row.scope = builders.mount(row, item, at)
|
|
278
|
-
return row
|
|
279
208
|
}
|
|
280
209
|
|
|
281
210
|
/**
|
|
@@ -291,16 +220,6 @@ function patchedSince(row: Row): boolean {
|
|
|
291
220
|
return (PATCHES.written.get(item) ?? 0) > row.seen
|
|
292
221
|
}
|
|
293
222
|
|
|
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
223
|
/**
|
|
305
224
|
* Warns about a repeated `{#for}` key, once per block.
|
|
306
225
|
*
|
package/src/ui/regions.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { REGION_CLOSE, REGION_OPEN } from '#shared/ANCHORS.ts'
|
|
2
2
|
import { LAYER_FILL } from './LAYER_NAMES.ts'
|
|
3
|
-
import { leave } from './leave.ts'
|
|
3
|
+
import { anyLeaving, leave } from './leave.ts'
|
|
4
4
|
import { Mismatch } from './Mismatch.ts'
|
|
5
5
|
import { beginLayer, endLayer } from './measureLayer.ts'
|
|
6
6
|
|
|
@@ -97,7 +97,7 @@ export function fillRegion(
|
|
|
97
97
|
throw new Mismatch(
|
|
98
98
|
`abide: this region is not in a document, so the client and the server disagree about this region's shape (41.10).`,
|
|
99
99
|
)
|
|
100
|
-
|
|
100
|
+
emptyRegion(region, parent)
|
|
101
101
|
if (build === undefined) return
|
|
102
102
|
beginLayer(LAYER_FILL)
|
|
103
103
|
try {
|
|
@@ -126,6 +126,31 @@ export function fillEmpty(
|
|
|
126
126
|
return true
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Removes everything between a region's anchors, which stay.
|
|
131
|
+
*
|
|
132
|
+
* ONE WRITE WHERE THE REGION IS ALL ITS PARENT HOLDS — a `<tbody>` or a `<ul>` around one
|
|
133
|
+
* `{#for}`, the common shape of a list emptied — and the anchors put back after it. A removal per
|
|
134
|
+
* node is a crossing into the engine per node; the engine's own clear is one.
|
|
135
|
+
*
|
|
136
|
+
* NODE BY NODE WHILE ANYTHING IS LEAVING (32.38), because a node holding a leaving element has to
|
|
137
|
+
* stay where it stands, and only `leave` asks.
|
|
138
|
+
*/
|
|
139
|
+
export function emptyRegion(region: Region, parent: ParentNode): void {
|
|
140
|
+
const { open, close } = region
|
|
141
|
+
if (
|
|
142
|
+
anyLeaving() ||
|
|
143
|
+
parent.firstChild !== open ||
|
|
144
|
+
parent.lastChild !== close
|
|
145
|
+
) {
|
|
146
|
+
removeUntil(open.nextSibling, close)
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
if (open.nextSibling === close) return
|
|
150
|
+
parent.textContent = ''
|
|
151
|
+
parent.append(open, close)
|
|
152
|
+
}
|
|
153
|
+
|
|
129
154
|
/** Removes `from` and every sibling after it, up to and not including `end`. */
|
|
130
155
|
export function removeUntil(from: ChildNode | null, end: Node | null): void {
|
|
131
156
|
let node = from
|
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
|
+
}
|