@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.
- package/ABIDE.md +8 -5
- package/CHANGELOG.md +58 -0
- package/bin/abide.ts +36 -65
- package/docs/REGISTRY.md +45 -12
- package/docs/RULEBOOK.md +24 -14
- package/docs/abide.md +999 -459
- package/package.json +1 -1
- package/src/compiler/ARRAY_MUTATORS.ts +20 -0
- package/src/compiler/Diagnostics.ts +198 -0
- package/src/compiler/KeptChecker.ts +97 -0
- package/src/compiler/LoweredFile.ts +609 -0
- package/src/compiler/Lowering.ts +18 -1
- package/src/compiler/PositionedRefusal.ts +1 -1
- package/src/compiler/SHARED_NAMES.ts +2 -0
- package/src/compiler/ShadowHost.ts +670 -0
- package/src/compiler/ShadowMap.ts +1 -1
- package/src/compiler/TypeSession.ts +76 -1743
- package/src/compiler/appLayout.ts +28 -20
- package/src/compiler/appLints.ts +109 -0
- package/src/compiler/argsDefaultsIn.ts +1 -1
- package/src/compiler/checkApp.ts +64 -68
- package/src/compiler/checkedShadow.ts +2 -2
- package/src/compiler/classifyRead.ts +16 -1
- package/src/compiler/compileApp.ts +102 -71
- package/src/compiler/declaredExports.ts +46 -21
- package/src/compiler/diagnoseSources.ts +232 -0
- package/src/compiler/emitClient.ts +1 -1
- package/src/compiler/formatAbide.ts +7 -8
- package/src/compiler/handlersUnder.ts +32 -63
- package/src/compiler/lineOf.ts +0 -19
- package/src/compiler/matchersOf.ts +3 -3
- package/src/compiler/mountsOf.ts +3 -3
- package/src/compiler/reactiveReads.ts +235 -0
- package/src/compiler/reactiveTypes.ts +13 -0
- package/src/compiler/readSources.ts +16 -18
- package/src/compiler/readsIn.ts +181 -82
- package/src/compiler/routesDeclaration.ts +46 -0
- package/src/compiler/scaffold.ts +7 -2
- package/src/compiler/serveLanguage.ts +46 -74
- package/src/compiler/sharedModuleWrites.ts +1 -1
- package/src/compiler/splitImports.ts +2 -2
- package/src/compiler/templateWarnings.ts +1 -2
- package/src/compiler/trackingWarnings.ts +1 -1
- package/src/server/CLIENT_BUILD.ts +4 -32
- package/src/server/DesktopSession.ts +213 -0
- package/src/server/Followers.ts +399 -0
- package/src/server/MODULE_QUERIES.ts +6 -2
- package/src/server/PageRender.ts +201 -0
- package/src/server/RESUME_MILLISECONDS.ts +2 -0
- package/src/server/abideModules.ts +7 -7
- package/src/server/answerFailure.ts +3 -2
- package/src/server/answerRequest.ts +16 -369
- package/src/server/answerRpc.ts +48 -302
- package/src/server/answerSocket.ts +41 -490
- package/src/server/appBuild.ts +377 -0
- package/src/server/appKit.ts +1 -1
- package/src/server/appPlugins.ts +15 -17
- package/src/server/appTree.ts +6 -5
- package/src/server/argsContract.ts +209 -0
- package/src/server/bridgeRelay.ts +153 -65
- package/src/server/claudeCode.ts +30 -127
- package/src/server/config.ts +6 -7
- package/src/server/createApp.ts +38 -66
- package/src/server/csp.ts +1 -1
- package/src/server/deferredBuilds.ts +3 -16
- package/src/server/devWorker.ts +11 -15
- package/src/server/faceOf.ts +1 -1
- package/src/server/generatedSurfaces.ts +2 -1
- package/src/server/handlerFileMount.ts +40 -0
- package/src/server/index.ts +3 -3
- package/src/server/installServer.ts +5 -9
- package/src/server/invokeHandler.ts +230 -0
- package/src/server/mountHandlers.ts +184 -0
- package/src/server/openDesktopWindow.ts +308 -0
- package/src/server/packageDirectory.ts +14 -0
- package/src/server/preload.ts +21 -2
- package/src/server/principal.ts +5 -4
- package/src/server/remote.ts +2 -2
- package/src/server/render.ts +31 -59
- package/src/server/requestScope.ts +5 -36
- package/src/server/runCompiled.ts +1 -1
- package/src/server/runDesktop.ts +20 -448
- package/src/server/seedBuffer.ts +12 -12
- package/src/server/serveDev.ts +24 -58
- package/src/server/servedApp.ts +390 -0
- package/src/server/shell.ts +1 -1
- package/src/server/signInChanged.ts +20 -0
- package/src/server/surfaceOf.ts +3 -2
- package/src/server/versionOf.ts +7 -16
- package/src/shared/RPC_SERVER.ts +27 -31
- package/src/shared/Schema.ts +1 -2
- package/src/shared/applyPatch.ts +1 -1
- package/src/shared/index.ts +1 -1
- package/src/shared/{note.ts → issueReport.ts} +22 -0
- package/src/shared/loadCall.ts +66 -0
- package/src/shared/muxFrames.ts +1 -1
- package/src/shared/reactive/Keyed.ts +24 -16
- package/src/shared/reactive/Reactive.ts +37 -991
- package/src/shared/reactive/argsContract.ts +131 -4
- package/src/shared/reactive/byOf.ts +2 -5
- package/src/shared/reactive/callLoad.ts +8 -14
- package/src/shared/reactive/cellShape.ts +734 -0
- package/src/shared/reactive/index.ts +12 -23
- package/src/shared/reactive/pairReactive.ts +5 -3
- package/src/shared/reactive/patchLog.ts +79 -0
- package/src/shared/reactive/patchMarks.ts +34 -0
- package/src/shared/reactive/producer.ts +121 -0
- package/src/shared/reactive/recordWrites.ts +11 -14
- package/src/shared/reactive/scoped.ts +3 -2
- package/src/shared/reactive/selections.ts +209 -0
- package/src/shared/reactive/share.ts +4 -3
- package/src/shared/reactive/watch.ts +3 -4
- package/src/shared/rpc.ts +20 -24
- package/src/shared/validateJson.ts +1 -2
- package/src/shared/writePath.ts +29 -0
- package/src/ui/ArmRegion.ts +10 -4
- package/src/ui/Level.ts +11 -20
- package/src/ui/LevelChain.ts +272 -0
- package/src/ui/Router.ts +106 -313
- package/src/ui/Scope.ts +17 -3
- package/src/ui/TryRegion.ts +22 -23
- package/src/ui/bindSpread.ts +2 -4
- package/src/ui/listenForRoutes.ts +3 -10
- package/src/ui/outboxQueue.ts +23 -11
- package/src/ui/outcomeOf.ts +120 -0
- package/src/ui/reconcile.ts +4 -6
- package/src/ui/routing.ts +2 -1
- package/src/ui/rows.ts +2 -2
- package/src/ui/setAttribute.ts +12 -3
- package/src/ui/socketWire.ts +28 -13
- package/src/ui/wire.ts +9 -10
- package/src/compiler/Finding.ts +0 -2
- package/src/compiler/Refusals.ts +0 -65
- package/src/compiler/formatDiagnosis.ts +0 -20
- package/src/server/appDirectories.ts +0 -9
- package/src/server/argsFor.ts +0 -39
- package/src/server/buildDirectory.ts +0 -10
- package/src/server/coerceArgs.ts +0 -109
- package/src/server/derivedArgs.ts +0 -26
- package/src/server/runBuild.ts +0 -24
- package/src/server/serveBuild.ts +0 -144
- package/src/server/testBuild.ts +0 -220
- package/src/shared/LOAD_OFFLINE.ts +0 -15
- package/src/shared/LOAD_SIGNAL.ts +0 -10
- package/src/shared/issuesOf.ts +0 -18
- package/src/shared/reactive/ENTRY_LIVES.ts +0 -7
- package/src/shared/reactive/FOLLOWED.ts +0 -8
- package/src/shared/reactive/PATCHES.ts +0 -11
- package/src/shared/reactive/PATCH_SINKS.ts +0 -26
- 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 {
|
|
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
|
|
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
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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).
|
|
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
|
-
|
|
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
|
|
package/src/shared/writePath.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/ui/ArmRegion.ts
CHANGED
|
@@ -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 —
|
|
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
|
-
*
|
|
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
|
|
27
|
-
|
|
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(
|
|
68
|
-
this.
|
|
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
|
-
|
|
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.
|
|
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
|
|
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 =
|
|
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() }
|