@abide/abide 0.123.2 → 0.124.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 (150) hide show
  1. package/ABIDE.md +8 -5
  2. package/CHANGELOG.md +58 -0
  3. package/bin/abide.ts +36 -65
  4. package/docs/REGISTRY.md +45 -12
  5. package/docs/RULEBOOK.md +24 -14
  6. package/docs/abide.md +999 -459
  7. package/package.json +1 -1
  8. package/src/compiler/ARRAY_MUTATORS.ts +20 -0
  9. package/src/compiler/Diagnostics.ts +198 -0
  10. package/src/compiler/KeptChecker.ts +97 -0
  11. package/src/compiler/LoweredFile.ts +609 -0
  12. package/src/compiler/Lowering.ts +18 -1
  13. package/src/compiler/PositionedRefusal.ts +1 -1
  14. package/src/compiler/SHARED_NAMES.ts +2 -0
  15. package/src/compiler/ShadowHost.ts +670 -0
  16. package/src/compiler/ShadowMap.ts +1 -1
  17. package/src/compiler/TypeSession.ts +76 -1743
  18. package/src/compiler/appLayout.ts +28 -20
  19. package/src/compiler/appLints.ts +109 -0
  20. package/src/compiler/argsDefaultsIn.ts +1 -1
  21. package/src/compiler/checkApp.ts +64 -68
  22. package/src/compiler/checkedShadow.ts +2 -2
  23. package/src/compiler/classifyRead.ts +16 -1
  24. package/src/compiler/compileApp.ts +102 -71
  25. package/src/compiler/declaredExports.ts +46 -21
  26. package/src/compiler/diagnoseSources.ts +232 -0
  27. package/src/compiler/emitClient.ts +1 -1
  28. package/src/compiler/formatAbide.ts +7 -8
  29. package/src/compiler/handlersUnder.ts +32 -63
  30. package/src/compiler/lineOf.ts +0 -19
  31. package/src/compiler/matchersOf.ts +3 -3
  32. package/src/compiler/mountsOf.ts +3 -3
  33. package/src/compiler/reactiveReads.ts +235 -0
  34. package/src/compiler/reactiveTypes.ts +13 -0
  35. package/src/compiler/readSources.ts +16 -18
  36. package/src/compiler/readsIn.ts +181 -82
  37. package/src/compiler/routesDeclaration.ts +46 -0
  38. package/src/compiler/scaffold.ts +7 -2
  39. package/src/compiler/serveLanguage.ts +46 -74
  40. package/src/compiler/sharedModuleWrites.ts +1 -1
  41. package/src/compiler/splitImports.ts +2 -2
  42. package/src/compiler/templateWarnings.ts +1 -2
  43. package/src/compiler/trackingWarnings.ts +1 -1
  44. package/src/server/CLIENT_BUILD.ts +4 -32
  45. package/src/server/DesktopSession.ts +213 -0
  46. package/src/server/Followers.ts +399 -0
  47. package/src/server/MODULE_QUERIES.ts +6 -2
  48. package/src/server/PageRender.ts +201 -0
  49. package/src/server/RESUME_MILLISECONDS.ts +2 -0
  50. package/src/server/abideModules.ts +7 -7
  51. package/src/server/answerFailure.ts +3 -2
  52. package/src/server/answerRequest.ts +16 -369
  53. package/src/server/answerRpc.ts +48 -302
  54. package/src/server/answerSocket.ts +41 -490
  55. package/src/server/appBuild.ts +377 -0
  56. package/src/server/appKit.ts +1 -1
  57. package/src/server/appPlugins.ts +15 -17
  58. package/src/server/appTree.ts +6 -5
  59. package/src/server/argsContract.ts +209 -0
  60. package/src/server/bridgeRelay.ts +153 -65
  61. package/src/server/claudeCode.ts +30 -127
  62. package/src/server/config.ts +6 -7
  63. package/src/server/createApp.ts +38 -66
  64. package/src/server/csp.ts +1 -1
  65. package/src/server/deferredBuilds.ts +3 -16
  66. package/src/server/devWorker.ts +11 -15
  67. package/src/server/faceOf.ts +1 -1
  68. package/src/server/generatedSurfaces.ts +2 -1
  69. package/src/server/handlerFileMount.ts +40 -0
  70. package/src/server/index.ts +3 -3
  71. package/src/server/installServer.ts +5 -9
  72. package/src/server/invokeHandler.ts +230 -0
  73. package/src/server/mountHandlers.ts +184 -0
  74. package/src/server/openDesktopWindow.ts +308 -0
  75. package/src/server/packageDirectory.ts +14 -0
  76. package/src/server/preload.ts +21 -2
  77. package/src/server/principal.ts +5 -4
  78. package/src/server/remote.ts +2 -2
  79. package/src/server/render.ts +31 -59
  80. package/src/server/requestScope.ts +5 -36
  81. package/src/server/runCompiled.ts +1 -1
  82. package/src/server/runDesktop.ts +20 -448
  83. package/src/server/seedBuffer.ts +12 -12
  84. package/src/server/serveDev.ts +24 -58
  85. package/src/server/servedApp.ts +390 -0
  86. package/src/server/shell.ts +1 -1
  87. package/src/server/signInChanged.ts +20 -0
  88. package/src/server/surfaceOf.ts +3 -2
  89. package/src/server/versionOf.ts +7 -16
  90. package/src/shared/RPC_SERVER.ts +27 -31
  91. package/src/shared/Schema.ts +1 -2
  92. package/src/shared/applyPatch.ts +1 -1
  93. package/src/shared/index.ts +1 -1
  94. package/src/shared/{note.ts → issueReport.ts} +22 -0
  95. package/src/shared/loadCall.ts +66 -0
  96. package/src/shared/muxFrames.ts +1 -1
  97. package/src/shared/reactive/Keyed.ts +24 -16
  98. package/src/shared/reactive/Reactive.ts +37 -991
  99. package/src/shared/reactive/argsContract.ts +131 -4
  100. package/src/shared/reactive/byOf.ts +2 -5
  101. package/src/shared/reactive/callLoad.ts +8 -14
  102. package/src/shared/reactive/cellShape.ts +734 -0
  103. package/src/shared/reactive/index.ts +12 -23
  104. package/src/shared/reactive/pairReactive.ts +5 -3
  105. package/src/shared/reactive/patchLog.ts +79 -0
  106. package/src/shared/reactive/patchMarks.ts +34 -0
  107. package/src/shared/reactive/producer.ts +121 -0
  108. package/src/shared/reactive/recordWrites.ts +11 -14
  109. package/src/shared/reactive/scoped.ts +3 -2
  110. package/src/shared/reactive/selections.ts +209 -0
  111. package/src/shared/reactive/share.ts +4 -3
  112. package/src/shared/reactive/watch.ts +3 -4
  113. package/src/shared/rpc.ts +20 -24
  114. package/src/shared/validateJson.ts +1 -2
  115. package/src/shared/writePath.ts +29 -0
  116. package/src/ui/ArmRegion.ts +10 -4
  117. package/src/ui/Level.ts +11 -20
  118. package/src/ui/LevelChain.ts +272 -0
  119. package/src/ui/Router.ts +106 -313
  120. package/src/ui/Scope.ts +17 -3
  121. package/src/ui/TryRegion.ts +22 -23
  122. package/src/ui/bindSpread.ts +2 -4
  123. package/src/ui/listenForRoutes.ts +3 -10
  124. package/src/ui/outboxQueue.ts +23 -11
  125. package/src/ui/outcomeOf.ts +120 -0
  126. package/src/ui/reconcile.ts +4 -6
  127. package/src/ui/routing.ts +2 -1
  128. package/src/ui/rows.ts +2 -2
  129. package/src/ui/setAttribute.ts +12 -3
  130. package/src/ui/socketWire.ts +28 -13
  131. package/src/ui/wire.ts +9 -10
  132. package/src/compiler/Finding.ts +0 -2
  133. package/src/compiler/Refusals.ts +0 -65
  134. package/src/compiler/formatDiagnosis.ts +0 -20
  135. package/src/server/appDirectories.ts +0 -9
  136. package/src/server/argsFor.ts +0 -39
  137. package/src/server/buildDirectory.ts +0 -10
  138. package/src/server/coerceArgs.ts +0 -109
  139. package/src/server/derivedArgs.ts +0 -26
  140. package/src/server/runBuild.ts +0 -24
  141. package/src/server/serveBuild.ts +0 -144
  142. package/src/server/testBuild.ts +0 -220
  143. package/src/shared/LOAD_OFFLINE.ts +0 -15
  144. package/src/shared/LOAD_SIGNAL.ts +0 -10
  145. package/src/shared/issuesOf.ts +0 -18
  146. package/src/shared/reactive/ENTRY_LIVES.ts +0 -7
  147. package/src/shared/reactive/FOLLOWED.ts +0 -8
  148. package/src/shared/reactive/PATCHES.ts +0 -11
  149. package/src/shared/reactive/PATCH_SINKS.ts +0 -26
  150. package/src/shared/reactive/canonicalKey.ts +0 -95
package/src/shared/rpc.ts CHANGED
@@ -14,7 +14,7 @@
14
14
  // handler's own.
15
15
 
16
16
  import { DERIVED_ARGS } from './DERIVED_ARGS.ts'
17
- import { type Failed, isFailedNamed } from './Failed.ts'
17
+ import { type Failed, isFailed, isFailedNamed } from './Failed.ts'
18
18
  import { GLOBAL_GETS } from './GLOBAL_GETS.ts'
19
19
  import { HandlerRoute } from './HandlerRoute.ts'
20
20
  import { IN_PROCESS_ORIGIN } from './IN_PROCESS_ORIGIN.ts'
@@ -23,18 +23,18 @@ import { compose, type Rung } from './Middleware.ts'
23
23
  import { outbox, type QueuedCall, sendingFor } from './outbox.ts'
24
24
  import { principalRotated } from './principalRotated.ts'
25
25
  import {
26
+ acceptedArgs,
26
27
  contractOf,
27
28
  type Gated,
28
29
  gateOf,
30
+ type HeldToAnnotation,
29
31
  keyOf,
30
- normalized,
31
32
  } from './reactive/argsContract.ts'
32
33
  import { callLoad } from './reactive/callLoad.ts'
33
34
  import {
34
35
  type AdoptedFailures,
35
36
  type AdoptedProduced,
36
37
  type ChunkOf,
37
- chunksOfStream,
38
38
  handlerValue,
39
39
  isKeyed,
40
40
  type Kept,
@@ -45,14 +45,17 @@ import {
45
45
  type Refusals,
46
46
  type Shaping,
47
47
  STREAMING,
48
- streamingOrSettled,
49
48
  type Streams,
50
49
  } from './reactive/index.ts'
51
50
  import { MUTATION } from './reactive/MUTATION.ts'
52
- import { dropAnswered } from './reactive/Reactive.ts'
51
+ import {
52
+ chunksOfStream,
53
+ dropAnswered,
54
+ streamingOrSettled,
55
+ } from './reactive/producer.ts'
53
56
  import { builtHere, isForwarder } from './reactive/scoped.ts'
54
57
  import { SELECTION_MEMBERS } from './reactive/SELECTION_MEMBERS.ts'
55
- import { checkArgs, validationError } from './refuse.ts'
58
+ import { checkArgs } from './refuse.ts'
56
59
  import { RPC_SERVER } from './RPC_SERVER.ts'
57
60
  import { rpcRequest } from './rpcRequest.ts'
58
61
  import type { Schema } from './Schema.ts'
@@ -162,16 +165,6 @@ type Derived<Member, Whole = Member> = Member extends
162
165
  : never
163
166
  : never
164
167
 
165
- /**
166
- * 19.9 — the issues an `Rpc`'s args leave against the schema its annotation derives, `null` where
167
- * they hold: `undefined` until the module its handler is loaded through stamps it (`derivedArgs`),
168
- * and so in a browser always. A function rather than the schema, so this seam never carries the
169
- * validator (19.13).
170
- */
171
- export type HeldToAnnotation = {
172
- [DERIVED_ARGS]: ((args: unknown) => Issues<unknown> | null) | undefined
173
- }
174
-
175
168
  /** Which generated surfaces carry a handler (20.1). */
176
169
  export type Clients = {
177
170
  ui?: boolean
@@ -453,12 +446,14 @@ export function declare(method: Method): Declare {
453
446
  return route
454
447
  }
455
448
  const call = (given?: unknown, options?: CallOptions): unknown => {
456
- const args = normalized(contract, given)
457
- // 15.8, 19.9 — AND HELD TO ITS ANNOTATION, where the build derived a schema off it: a
458
- // type an in-process caller wrote holds nothing at run time (`as never`, a value off
459
- // JSON), so the call is refused as the wire refuses it rather than handed to the body.
460
- const issues = rpc[DERIVED_ARGS]?.(args) ?? null
461
- if (issues !== null) return reactive(() => validationError(issues))
449
+ // 15.8, 19.9 — the args every way of calling the handler is held to.
450
+ let args: unknown
451
+ try {
452
+ args = acceptedArgs(rpc, given)
453
+ } catch (refused) {
454
+ if (!isFailed(refused)) throw refused
455
+ return reactive(() => refused)
456
+ }
462
457
  const route =
463
458
  rpc.address === undefined ? undefined : routeAt(rpc.address)
464
459
  if (route?.segments !== undefined) {
@@ -539,14 +534,15 @@ export function declare(method: Method): Declare {
539
534
  }) satisfies Gated<unknown>
540
535
  const members = rpc as unknown as Record<string, unknown>
541
536
  // 7.10, 7.18 — A TRIGGER ON A SERVER REACHES EVERY BROWSER HOLDING WHAT IT NAMES, through the
542
- // mux (18.3). `null` in a browser, where the trigger is the copy's own.
537
+ // mux (18.3). None follow in a browser, where the trigger is the copy's own.
543
538
  for (const name of SELECTION_MEMBERS) {
544
539
  if (name === 'busy') continue
545
540
  members[name] =
546
541
  name === 'invalidate' || name === 'refresh'
547
542
  ? (pattern?: unknown): unknown => {
548
543
  const done = faceHere()[name](pattern)
549
- RPC_SERVER.broadcast?.(rpc, name, pattern)
544
+ for (const followers of RPC_SERVER.following)
545
+ followers.broadcast(rpc, name, pattern)
550
546
  return done
551
547
  }
552
548
  : (pattern?: unknown): unknown => faceHere()[name](pattern)
@@ -14,10 +14,9 @@
14
14
  // generated document carries them.
15
15
 
16
16
  import { isRecord } from './isRecord.ts'
17
+ import { issuesOf, note } from './issueReport.ts'
17
18
  import type { Issues } from './Issues.ts'
18
- import { issuesOf } from './issuesOf.ts'
19
19
  import type { JsonValue } from './JsonValue.ts'
20
- import { note } from './note.ts'
21
20
 
22
21
  type Node = { [key: string]: JsonValue }
23
22
 
@@ -93,3 +93,32 @@ function copyOf(value: unknown, step: string | number): Level {
93
93
  return (typeof step === 'number' ? [] : {}) as unknown as Level
94
94
  return { ...(value as object) }
95
95
  }
96
+
97
+ /**
98
+ * What `mutate` hands back, called on a copy of the array at `path` that `target` then holds.
99
+ *
100
+ * 31.9 — A MUTATOR IS A WRITE THROUGH ITS RECEIVER'S PATH. `tags.push(t)` run on the held array
101
+ * grows it in place, so the identity a reader wakes on never moves (31.8's failure, spelled as a
102
+ * call). The compiler lifts a call here only where the receiver's TYPE is an array, so a class's
103
+ * own `push` stays a call on the class. A level that is not plain data on the way (31.14), or a
104
+ * receiver that is not an array when the call runs, takes the call in place, waking nobody.
105
+ */
106
+ export function mutatePath<Result>(
107
+ target: { (): unknown; set(value: unknown): unknown },
108
+ path: readonly (string | number)[],
109
+ mutate: (receiver: never) => Result,
110
+ ): Result {
111
+ const value = target()
112
+ let receiver = value
113
+ let plain = true
114
+ for (const step of path) {
115
+ plain &&= isPlain(receiver)
116
+ receiver = (receiver as Level | null | undefined)?.[step]
117
+ }
118
+ if (!plain) return mutate(receiver as never)
119
+ if (!Array.isArray(receiver)) return mutate(receiver as never)
120
+ const copy = receiver.slice()
121
+ const result = mutate(copy as never)
122
+ target.set(writePath(value, path, copy))
123
+ return result
124
+ }
@@ -88,10 +88,7 @@ export class ArmRegion {
88
88
 
89
89
  swap(next: unknown): void {
90
90
  const region = this.region
91
- const filling =
92
- this.at !== undefined ||
93
- isEmptyRegion(region) ||
94
- this.paintedOther(next)
91
+ const filling = this.fills(next)
95
92
  const arm = this.armOf(next)
96
93
  const scope = this.scope
97
94
  if (filling) {
@@ -102,6 +99,15 @@ export class ArmRegion {
102
99
  this.at = next
103
100
  }
104
101
 
102
+ /** Whether swapping in `next` fills the region rather than hydrating what the server painted. */
103
+ fills(next: unknown): boolean {
104
+ return (
105
+ this.at !== undefined ||
106
+ isEmptyRegion(this.region) ||
107
+ this.paintedOther(next)
108
+ )
109
+ }
110
+
105
111
  armOf(next: unknown): Arm {
106
112
  return (this.arms[next as number] as ArmMakers[number])(undefined)
107
113
  }
package/src/ui/Level.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  currentShare,
3
+ enterShare,
3
4
  leaveShare,
4
- resumeShare,
5
5
  type ShareScope,
6
6
  } from '#shared/reactive/share.ts'
7
7
  import { untracked } from '#shared/reactive/tracking.ts'
@@ -14,18 +14,14 @@ import type { Scope } from './Scope.ts'
14
14
  /**
15
15
  * ONE LEVEL OF A ROUTED CHAIN — a layout or the page — and the head it contributes (39.1, 39.14).
16
16
  *
17
- * 41.11 — the router holds these rather than letting each level own the one below it, which is
18
- * what makes a shared layout something it can KEEP: a scope stopped from above takes its children
19
- * with it, and a layout that survives a navigation must not take the page it was holding a moment
20
- * ago. The module standing here is the router's `chain` at the same index.
17
+ * 41.11 — a `LevelChain` holds these rather than letting each level own the one below it.
21
18
  *
22
19
  * ON EVERY PAGE'S STATIC CLOSURE, because `contributeHead` is on the runtime barrel — so it knows
23
- * nothing of the router, and the router knows it only through `levels`.
20
+ * the chain it stands in only as what reconciles the head, and nothing of the router.
24
21
  */
25
22
  export class Level {
26
- /** The chain this level stands in, and where: a live head yields to every level below it. */
27
- levels: Level[]
28
- at: number
23
+ /** The chain this level stands in: a live head yields to every level below it. */
24
+ chain: { reconcile(): void }
29
25
  scope: Scope | undefined = undefined
30
26
  /**
31
27
  * Where this level's `<slot/>` is, learned when the level below it mounted there, and
@@ -64,9 +60,8 @@ export class Level {
64
60
  */
65
61
  contributed = new Set<HeadContribution>()
66
62
 
67
- constructor(levels: Level[], at: number) {
68
- this.levels = levels
69
- this.at = at
63
+ constructor(chain: { reconcile(): void }) {
64
+ this.chain = chain
70
65
  }
71
66
 
72
67
  /** Left by the level's mount (`emitClient`), painted or not. */
@@ -85,7 +80,7 @@ export class Level {
85
80
 
86
81
  /** Reconciles the document head from the chain this level stands in (`reconcileHead`). */
87
82
  reconcile(): void {
88
- Level.reconcile(this.levels)
83
+ this.chain.reconcile()
89
84
  }
90
85
 
91
86
  /**
@@ -94,11 +89,11 @@ export class Level {
94
89
  * a head read here is followed by its own watch, not by whatever wrote.
95
90
  */
96
91
  static reconcile(levels: Level[]): void {
97
- reconcileHead(untracked(Level.chain, levels, undefined))
92
+ reconcileHead(untracked(Level.contributors, levels, undefined))
98
93
  }
99
94
 
100
95
  /** Every contributor in `levels` and the head it reads now, outermost first. */
101
- static chain(
96
+ static contributors(
102
97
  levels: Level[],
103
98
  _?: undefined,
104
99
  ): [who: object, text: string][] {
@@ -146,11 +141,7 @@ export class Level {
146
141
  mount<Result>(run: () => Result): Result {
147
142
  // 39.1 — THE LEVEL RIDES THE SHARE SCOPE, so a component finds it as it finds a shared key,
148
143
  // whether it mounts now or in an arm or a row after (`contributeHead`).
149
- const back = resumeShare({
150
- parent: currentShare(),
151
- values: new Map(),
152
- level: this,
153
- })
144
+ const back = enterShare(this)
154
145
  this.unapplied = false
155
146
  let unapplied: boolean
156
147
  let result: Result
@@ -0,0 +1,272 @@
1
+ import type { Params } from '#shared/matchRoute.ts'
2
+ import type { ShareScope } from '#shared/reactive/share.ts'
3
+ import { route } from '#shared/route.ts'
4
+ import { setUpLevels } from '#shared/setUpLevels.ts'
5
+
6
+ import { LAYER_MOUNT } from './LAYER_NAMES.ts'
7
+ import { Level } from './Level.ts'
8
+ import { beginLayer, endLayer } from './measureLayer.ts'
9
+ import { fillRegion, type Region } from './regions.ts'
10
+ import type { Scope, Stoppable } from './Scope.ts'
11
+
12
+ /**
13
+ * A page's client mount, called against the region the router owns.
14
+ *
15
+ * The same default export a page already has — the router is a CALLER of a page exactly as a page
16
+ * is a caller of a component, which is why nothing about the emitted module changes to be routable.
17
+ */
18
+ export type PageMount = (
19
+ root: Region,
20
+ props?: Record<string, unknown>,
21
+ // 39.6 — A LEVEL'S HEAD IS ITS OWN MOUNT'S TO APPLY, from inside the instance its slots read
22
+ // (`emitClient`), so the router hands the module a region and says it is the router by handing
23
+ // it the level it stands at — a file invoked as a component too is mounted without it, and
24
+ // applies no head a row. 39.14 — the level is where the mount leaves its head for the router
25
+ // to read when it withdraws.
26
+ routed?: Level,
27
+ ) => Scope
28
+
29
+ export type PageModule = { default: PageMount }
30
+
31
+ /**
32
+ * The routed chain standing in the document — its layouts outermost first and the page last — and
33
+ * the head they contribute (39.1, 39.14).
34
+ *
35
+ * 41.11 — THE CHAIN HOLDS ITS LEVELS rather than each level owning the one below it, which is what
36
+ * makes a shared layout something a navigation can KEEP: a scope stopped from above takes its
37
+ * children with it, and a layout that survives a navigation must not take the page it was holding.
38
+ *
39
+ * 39.14 — EVERY WAY A LEVEL LEAVES IS BRACKETED HERE: the head reconciled while the leaving levels
40
+ * still read what is on screen, so what the server painted for them is adopted as theirs, and again
41
+ * once they are gone — and a leaving level's contributions are cleared before its scope stops, or
42
+ * each component's `HeadContribution.stop` would withdraw and reconcile on its own.
43
+ */
44
+ export class LevelChain {
45
+ /** The region the outermost level mounts into. */
46
+ private readonly root: Region
47
+ private readonly levels: Level[]
48
+ /**
49
+ * The modules the address resolved to, one per level.
50
+ *
51
+ * A kept layout's `<slot/>` mounts from it whenever its region opens — read at that moment
52
+ * rather than captured when the layout mounted, or a slot reopened after a navigation put back
53
+ * the page the reader had left.
54
+ */
55
+ private modules: PageModule[]
56
+
57
+ constructor(root: Region) {
58
+ this.root = root
59
+ this.levels = []
60
+ this.modules = []
61
+ }
62
+
63
+ /** How many levels stand. */
64
+ get depth(): number {
65
+ return this.levels.length
66
+ }
67
+
68
+ /** The scope of the level at `at` — negative counts from the page — or `undefined`. */
69
+ scopeAt(at: number): Scope | undefined {
70
+ return this.levels.at(at)?.scope
71
+ }
72
+
73
+ /** Reconciles the document head from every level standing (`Level.reconcile`). */
74
+ reconcile(): void {
75
+ Level.reconcile(this.levels)
76
+ }
77
+
78
+ /**
79
+ * How many levels of `arriving` are already standing.
80
+ *
81
+ * Layouts only: a page reached here is another route's, the same route's being followed instead
82
+ * (30.23). EVERY STANDING LEVEL IS COMPARED, the page included: a page is never a layout, so it
83
+ * cannot match one, and a layout whose slot is closed stands with nothing below it.
84
+ *
85
+ * By the MOUNT rather than by the loader or the module record: the table holds an arrow per
86
+ * row, so two routes under one layout name the same file through two different functions, and a
87
+ * table written by hand may answer with a fresh record each call.
88
+ */
89
+ sharedWith(arriving: PageModule[]): number {
90
+ const layouts = arriving.length - 1
91
+ let shared = 0
92
+ while (
93
+ shared < layouts &&
94
+ shared < this.levels.length &&
95
+ (this.modules[shared] as PageModule).default ===
96
+ (arriving[shared] as PageModule).default
97
+ )
98
+ shared += 1
99
+ return shared
100
+ }
101
+
102
+ /**
103
+ * Mounts `modules` from the top into the root region the server painted. No head is reconciled:
104
+ * a painted level's mount hydrates, and the head is the one the server wrote.
105
+ */
106
+ hydrate(modules: PageModule[]): void {
107
+ this.modules = modules
108
+ this.mountMeasured(0, this.root)
109
+ }
110
+
111
+ /**
112
+ * Keeps the first `kept` levels and mounts `modules` below them, reconciling the head around it.
113
+ *
114
+ * `between` runs once the leaving levels have stopped and their region is emptied, and before
115
+ * the arriving ones are set up — the address written and `route` moved, which a setup reads.
116
+ *
117
+ * The arriving part goes into the root where nothing is kept, and into the deepest kept layout's
118
+ * slot where something is — NOWHERE where that slot's region is closed: the slot mounts it off
119
+ * `modules` when it opens, and not into a region no longer in the document.
120
+ */
121
+ replace(kept: number, modules: PageModule[], between: () => void): void {
122
+ this.reconcile()
123
+ this.stopFrom(kept)
124
+ const into =
125
+ kept === 0 ? this.root : (this.levels[kept - 1] as Level).slot
126
+ // EMPTIED THROUGH THE PRIMITIVE EVERY OTHER REGION IS, which carries the 41.10 refusal for a
127
+ // region no longer in a document that a hand-written walk swallows.
128
+ if (into !== undefined) fillRegion(into)
129
+ between()
130
+ this.modules = modules
131
+ // 39.6 — each arriving level applies its own head as it mounts into its emptied region; a
132
+ // kept layout is not mounted again, so neither is its head.
133
+ if (into !== undefined) this.mountMeasured(kept, into)
134
+ // 39.14 — what the leaving levels keyed and the arriving ones did not goes back to the
135
+ // nearest kept layout's, else leaves; their keyless nodes leave with them (39.16).
136
+ this.reconcile()
137
+ }
138
+
139
+ /**
140
+ * AT THE TOP OF THE CHAIN, NOT INSIDE `mountFrom`: it recurses one level per layout, and
141
+ * `shares()` SUMS every measure of a name inside the op — so a pair inside the recursion would
142
+ * count the innermost level once per level above it.
143
+ */
144
+ private mountMeasured(from: number, into: Region): void {
145
+ beginLayer(LAYER_MOUNT)
146
+ try {
147
+ this.mountFrom(from, into)
148
+ } finally {
149
+ endLayer(LAYER_MOUNT)
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Stops every level from `at` down, inside out, and drops them.
155
+ *
156
+ * Stopped from the INSIDE OUT, so a layout's disposers run after the page's — the order a
157
+ * scope's own teardown already has, and the one an author reading two `watch` cleanups in
158
+ * two files would expect.
159
+ */
160
+ private stopFrom(at: number): void {
161
+ for (let index = this.levels.length - 1; index >= at; index -= 1) {
162
+ const level = this.levels[index] as Level
163
+ // OUT OF THE CHAIN BEFORE IT STOPS, so a slot stop it fires on the way finds it gone
164
+ // (`release`) and leaves the head to whoever stopped it.
165
+ this.levels.length = index
166
+ // The level's head leaves whole, so none of its components withdraws its own.
167
+ level.contributed.clear()
168
+ level.scope?.stop()
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Mounts the chain from level `at` into `region`, each level handed the next as `children`.
174
+ *
175
+ * 35.1 — each level is handed the one below it as `children` (32.18), so a layout puts the page
176
+ * in its `<slot/>` with the same mechanism a caller writing markup between two tags uses — and
177
+ * the chain learns where that slot is on the way past, which is what lets it swap the page
178
+ * there later without the layout knowing.
179
+ *
180
+ * THE CHILD'S SCOPE IS THE CHAIN'S, not the layout's, so a kept layout cannot stop the page
181
+ * arriving under it. What `mount` hands the layout is a stop for WHATEVER STANDS IN THE SLOT
182
+ * when the slot's region leaves — a `<slot/>` inside a closed `{#if}` — rather than for the
183
+ * page that happened to be mounted first. And the slot mounts from `modules`, so a slot that
184
+ * opens again mounts the page the address resolves to now.
185
+ */
186
+ private mountFrom(at: number, region: Region): void {
187
+ setUpLevels(
188
+ at,
189
+ this.modules.length,
190
+ this.levels[at - 1]?.share,
191
+ ROUTE_PROPS,
192
+ (below, props) => this.setUp(below, region, props),
193
+ )
194
+ this.markUp(at, region)
195
+ }
196
+
197
+ /**
198
+ * Runs level `at`'s setup over `props`, leaving its markup on the level for `markUp`, and
199
+ * answers the share scope the setup left.
200
+ */
201
+ private setUp(
202
+ at: number,
203
+ region: Region,
204
+ props: Record<string, (() => unknown) | undefined>,
205
+ ): ShareScope | null {
206
+ const module = this.modules[at] as PageModule
207
+ const level = new Level(this)
208
+ this.levels[at] = level
209
+ if (at < this.modules.length - 1)
210
+ props.children = () => ({
211
+ // NO BUILD, because the level below carries its own (32.17): a module a route
212
+ // reaches fills the region it is mounted against when that region is empty, so
213
+ // the build and the mount are one call rather than two.
214
+ build: undefined,
215
+ mount: (slot: Region): Stoppable => {
216
+ // The level below as `mountFrom` set it up, the first time its slot opens; a
217
+ // slot opening again sets it up afresh.
218
+ const pending = this.levels[at + 1]?.markup !== undefined
219
+ if (!pending) this.release(level, at)
220
+ level.slot = slot
221
+ if (pending) this.markUp(at + 1, slot)
222
+ else this.mountFrom(at + 1, slot)
223
+ return { stop: () => this.release(level, at) }
224
+ },
225
+ })
226
+ // The region a level's markup lands in is its slot above, known only once that markup
227
+ // runs; a module mounted by the chain leaves its markup rather than using this one.
228
+ // A deferred level answers the scope its setup made, so the watches that setup opened are
229
+ // stopped with the level even where its markup never runs — a slot that never opens.
230
+ level.scope = level.mount(() => module.default(region, props, level))
231
+ return level.share
232
+ }
233
+
234
+ /** Runs the markup level `at`'s setup left, into `region`. */
235
+ private markUp(at: number, region: Region): void {
236
+ const level = this.levels[at] as Level
237
+ const markup = level.markup
238
+ if (markup === undefined) return
239
+ level.markup = undefined
240
+ level.scope = level.mount(() => markup(region))
241
+ }
242
+
243
+ /**
244
+ * Stops what stands below `level` and forgets its slot.
245
+ *
246
+ * A no-op once the level itself has been replaced, which is how a stop handed to a layout that
247
+ * has since left does nothing.
248
+ */
249
+ private release(level: Level, at: number): void {
250
+ if (this.levels[at] !== level) return
251
+ level.slot = undefined
252
+ // 39.14, 39.16 — THE LEVELS BELOW LEAVE WITH THE SLOT'S REGION, and their head with them.
253
+ this.reconcile()
254
+ this.stopFrom(at + 1)
255
+ this.reconcile()
256
+ }
257
+ }
258
+
259
+ /**
260
+ * The props a routed page or layout is set up over, `children` aside: `params` as a live accessor.
261
+ *
262
+ * 33.4 — A PROP IS AN ACCESSOR. A page is mounted as any other component is, so its one prop is
263
+ * passed the way `renderComponent` passes a child's and the way `answerRequest` paints one: a
264
+ * function per key. Passed as the plain record it lowered to `params()` on a value that was not
265
+ * one, so every page with a `[param]` in its address hydrated dead — with its server markup
266
+ * already on screen, which is what kept it quiet.
267
+ *
268
+ * 33.5 — AND A LIVE ONE: `params` reads `route.params`, which every navigation writes. A layout
269
+ * kept across `/users/1` -> `/users/2` is not mounted again, so an accessor over the params it was
270
+ * mounted with went on answering user 1.
271
+ */
272
+ const ROUTE_PROPS = { params: (): Params => route.params() }