@abide/abide 0.104.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.
Files changed (83) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/bin/abide.ts +80 -173
  3. package/docs/REGISTRY.md +17 -17
  4. package/docs/RULEBOOK.md +50 -12
  5. package/docs/abide.md +9 -9
  6. package/package.json +1 -1
  7. package/src/compiler/Lowering.ts +15 -5
  8. package/src/compiler/Refusals.ts +57 -0
  9. package/src/compiler/TypeSession.ts +45 -86
  10. package/src/compiler/appLayout.ts +447 -0
  11. package/src/compiler/appMap.ts +21 -20
  12. package/src/compiler/checkApp.ts +61 -97
  13. package/src/compiler/checkedShadow.ts +11 -9
  14. package/src/compiler/classifyRead.ts +3 -6
  15. package/src/compiler/compileApp.ts +180 -512
  16. package/src/compiler/componentGraph.ts +2 -71
  17. package/src/compiler/declaredExports.ts +79 -60
  18. package/src/compiler/emitClient.ts +11 -13
  19. package/src/compiler/emitComponent.ts +21 -6
  20. package/src/compiler/emitSource.ts +20 -20
  21. package/src/compiler/handlersUnder.ts +46 -36
  22. package/src/compiler/hookCalls.ts +152 -0
  23. package/src/compiler/mountsOf.ts +31 -54
  24. package/src/compiler/readSources.ts +27 -42
  25. package/src/compiler/readsIn.ts +5 -16
  26. package/src/compiler/resolverIn.ts +77 -0
  27. package/src/server/abideModules.ts +92 -123
  28. package/src/server/answerRequest.ts +18 -4
  29. package/src/server/answerSocket.ts +136 -76
  30. package/src/server/answerValue.ts +28 -15
  31. package/src/server/appTree.ts +49 -71
  32. package/src/server/claudeApi.ts +2 -5
  33. package/src/server/clientBundle.ts +198 -0
  34. package/src/server/coerceArgs.ts +1 -1
  35. package/src/server/commands.ts +93 -57
  36. package/src/server/createApp.ts +90 -16
  37. package/src/server/deferredBuilds.ts +175 -0
  38. package/src/server/devWorker.ts +107 -319
  39. package/src/server/faceOf.ts +120 -0
  40. package/src/server/fenceServer.ts +10 -18
  41. package/src/server/index.ts +3 -0
  42. package/src/server/placement.ts +167 -0
  43. package/src/server/remote.ts +14 -42
  44. package/src/server/render.ts +25 -17
  45. package/src/server/renderComponent.ts +9 -5
  46. package/src/server/requestScope.ts +10 -4
  47. package/src/server/responseRelay.ts +389 -0
  48. package/src/server/runCompiled.ts +9 -12
  49. package/src/server/runDesktop.ts +6 -9
  50. package/src/server/serveBuild.ts +26 -73
  51. package/src/server/serveDev.ts +19 -176
  52. package/src/server/streamEvent.ts +29 -0
  53. package/src/server/withAbideSources.ts +2 -1
  54. package/src/shared/HandlerRoute.ts +68 -0
  55. package/src/shared/REFUSAL_KEY.ts +5 -0
  56. package/src/shared/answerOf.ts +105 -78
  57. package/src/shared/filledPath.ts +1 -9
  58. package/src/shared/framing.ts +127 -128
  59. package/src/shared/queryText.ts +15 -7
  60. package/src/shared/reactive/Cursor.ts +4 -2
  61. package/src/shared/reactive/Reactive.ts +285 -197
  62. package/src/shared/reactive/index.ts +11 -8
  63. package/src/shared/reactive/pairReactive.ts +1 -1
  64. package/src/shared/reactive/scoped.ts +4 -3
  65. package/src/shared/reactive/share.ts +11 -1
  66. package/src/shared/routePattern.ts +0 -123
  67. package/src/shared/rpc.ts +17 -24
  68. package/src/shared/rpcRequest.ts +18 -47
  69. package/src/shared/takeEarlyCall.ts +12 -37
  70. package/src/shared/url.ts +1 -1
  71. package/src/ui/Level.ts +276 -0
  72. package/src/ui/List.ts +17 -17
  73. package/src/ui/Router.ts +20 -87
  74. package/src/ui/applyHead.ts +92 -79
  75. package/src/ui/index.ts +4 -2
  76. package/src/ui/reconcile.ts +4 -110
  77. package/src/ui/rows.ts +123 -2
  78. package/src/ui/wire.ts +12 -27
  79. package/src/compiler/hooksCalled.ts +0 -35
  80. package/src/server/clientBuildOptions.ts +0 -54
  81. package/src/server/refuseAddressCollision.ts +0 -37
  82. package/src/shared/reactive/chunksOfStream.ts +0 -33
  83. package/src/ui/MOUNTING_LEVEL.ts +0 -18
@@ -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, because there is nothing
8
- // it could be said to be the same as, and dropping it instead would lose the one kind of
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
- export function applyHead(markup: string): void {
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
- if (node.nodeType !== 1) {
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
- head.appendChild(element)
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` and `arriving` are the heads of
54
- * the levels a navigation stopped and mounted; `outer` is every contributor standing above them,
55
- * outermost first — the shell's keyed entries, then each kept layout's — so the LAST entry for a
56
- * key in it is the one the server would paint there (39.4). What the arriving levels key they have
57
- * already written (39.6), so only the keys they are silent on are touched.
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 ENDS AT THE FIRST RETURN: two pages that both key a `<title>` withdraw
60
- * nothing, and `outer` is neither built nor parsed.
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 withdrawn = keyedIn(head, leaving)
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
- /** The keyed elements `markup` parses to, by key, the last holding each (39.4). */
86
- function keyedIn(head: HTMLHeadElement, markup: string): Map<string, Element> {
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
- if (node.nodeType === 1) {
91
- const key = headKey(node as Element)
92
- if (key !== undefined) keyed.set(key, node as Element)
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, contributeHead, liveHead } from './applyHead.ts'
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'
@@ -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, RowBuilders } from './List.ts'
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, removeRow } from './rows.ts'
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 in the document — its element, or its anchored range — is `rows.ts`'s. Most lists
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 IN THE DOCUMENT, decided here and nowhere else. A row is its ELEMENT where the
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
- export function removeRow(first: ChildNode): void {
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, carriesBytes } from '#shared/framing.ts'
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, address, args),
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.type)
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
- method !== 'GET' ||
93
- answer.status === undefined ||
94
- answer.status < 200 ||
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
- }