@abide/abide 0.123.1 → 0.123.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/bin/abide.ts +36 -65
- package/docs/REGISTRY.md +2 -2
- package/docs/RULEBOOK.md +2 -2
- package/docs/abide.md +2 -2
- package/package.json +1 -1
- package/src/compiler/Diagnostics.ts +198 -0
- package/src/compiler/KeptChecker.ts +97 -0
- package/src/compiler/Lowering.ts +1 -1
- package/src/compiler/PositionedRefusal.ts +1 -1
- package/src/compiler/TypeSession.ts +62 -85
- package/src/compiler/appLayout.ts +28 -20
- package/src/compiler/argsDefaultsIn.ts +1 -1
- package/src/compiler/checkApp.ts +64 -68
- package/src/compiler/checkedShadow.ts +1 -1
- package/src/compiler/compileApp.ts +101 -70
- package/src/compiler/declaredExports.ts +2 -4
- package/src/compiler/emitClient.ts +1 -1
- package/src/compiler/formatAbide.ts +2 -2
- package/src/compiler/handlersUnder.ts +17 -29
- package/src/compiler/lineOf.ts +0 -19
- package/src/compiler/matchersOf.ts +3 -3
- package/src/compiler/mountsOf.ts +3 -3
- package/src/compiler/readSources.ts +16 -18
- package/src/compiler/readsIn.ts +1 -1
- package/src/compiler/serveLanguage.ts +23 -57
- package/src/compiler/sharedModuleWrites.ts +1 -1
- 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/RESUME_MILLISECONDS.ts +2 -0
- package/src/server/answerRequest.ts +11 -300
- package/src/server/answerRpc.ts +12 -54
- package/src/server/answerSocket.ts +8 -35
- package/src/server/appBuild.ts +377 -0
- 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 +30 -64
- package/src/server/devWorker.ts +11 -15
- package/src/server/handlerFileMount.ts +40 -0
- package/src/server/index.ts +2 -2
- package/src/server/packageDirectory.ts +14 -0
- package/src/server/preload.ts +3 -2
- package/src/server/principal.ts +4 -3
- package/src/server/remote.ts +1 -1
- package/src/server/runCompiled.ts +1 -1
- package/src/server/runDesktop.ts +1 -1
- package/src/server/seedBuffer.ts +1 -1
- package/src/server/serveDev.ts +24 -58
- package/src/server/servedApp.ts +390 -0
- package/src/server/signInChanged.ts +20 -0
- package/src/server/surfaceOf.ts +2 -1
- package/src/server/versionOf.ts +6 -15
- package/src/shared/applyPatch.ts +1 -1
- package/src/shared/muxFrames.ts +1 -1
- package/src/shared/reactive/Keyed.ts +1 -1
- package/src/shared/reactive/Reactive.ts +21 -8
- package/src/shared/reactive/argsContract.ts +131 -4
- package/src/shared/reactive/index.ts +1 -0
- package/src/shared/reactive/patchLog.ts +79 -0
- package/src/shared/reactive/patchMarks.ts +34 -0
- package/src/shared/reactive/recordWrites.ts +11 -14
- package/src/shared/reactive/share.ts +4 -3
- package/src/shared/rpc.ts +12 -19
- package/src/shared/validateJson.ts +14 -3
- 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 +54 -245
- 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 +21 -3
- 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 +26 -11
- 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 -95
- package/src/server/runBuild.ts +0 -24
- package/src/server/serveBuild.ts +0 -144
- package/src/server/testBuild.ts +0 -220
- 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/ui/Router.ts
CHANGED
|
@@ -6,36 +6,14 @@ import { unmounted } from '#shared/mount.ts'
|
|
|
6
6
|
import { NAVIGATION_HEADER } from '#shared/NAVIGATION_HEADER.ts'
|
|
7
7
|
import { OFFLINE_SAVED } from '#shared/OFFLINE_CACHES.ts'
|
|
8
8
|
import { REACHED_MESSAGE } from '#shared/REACHED_MESSAGE.ts'
|
|
9
|
-
import type { ShareScope } from '#shared/reactive/share.ts'
|
|
10
9
|
import { arrive, route } from '#shared/route.ts'
|
|
11
10
|
import { rankRoutes, type Segment } from '#shared/routePattern.ts'
|
|
12
|
-
import { setUpLevels } from '#shared/setUpLevels.ts'
|
|
13
11
|
import { paintedCallsLanded } from '#shared/takeEarlyCall.ts'
|
|
14
12
|
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
13
|
+
import { LAYER_NAVIGATE } from './LAYER_NAMES.ts'
|
|
14
|
+
import { LevelChain, type PageModule } from './LevelChain.ts'
|
|
17
15
|
import { beginLayer, endLayer } from './measureLayer.ts'
|
|
18
|
-
import {
|
|
19
|
-
import type { Scope, Stoppable } from './Scope.ts'
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* A page's client mount, called against the region the router owns.
|
|
23
|
-
*
|
|
24
|
-
* The same default export a page already has — the router is a CALLER of a page exactly as a page
|
|
25
|
-
* is a caller of a component, which is why nothing about the emitted module changes to be routable.
|
|
26
|
-
*/
|
|
27
|
-
export type PageMount = (
|
|
28
|
-
root: Region,
|
|
29
|
-
props?: Record<string, unknown>,
|
|
30
|
-
// 39.6 — A LEVEL'S HEAD IS ITS OWN MOUNT'S TO APPLY, from inside the instance its slots read
|
|
31
|
-
// (`emitClient`), so the router hands the module a region and says it is the router by handing
|
|
32
|
-
// it the level it stands at — a file invoked as a component too is mounted without it, and
|
|
33
|
-
// applies no head a row. 39.14 — the level is where the mount leaves its head for the router
|
|
34
|
-
// to read when it withdraws.
|
|
35
|
-
routed?: Level,
|
|
36
|
-
) => Scope
|
|
37
|
-
|
|
38
|
-
type PageModule = { default: PageMount }
|
|
16
|
+
import { pageRegion, type Region } from './regions.ts'
|
|
39
17
|
|
|
40
18
|
export type Route = {
|
|
41
19
|
/** The route literal, in the bracket spelling the page's directory is already written in. */
|
|
@@ -82,32 +60,25 @@ const SUPERSEDED = 'superseded'
|
|
|
82
60
|
*
|
|
83
61
|
* It holds what it put on the page, so a navigation can take it back off. A page mounted into the
|
|
84
62
|
* region owns a `Scope` (12.13) and the router is the only thing that can remove its nodes, so the
|
|
85
|
-
* router is where that scope is stopped.
|
|
63
|
+
* router — through its `LevelChain` — is where that scope is stopped. The router keeps the address,
|
|
64
|
+
* the history and the scroll.
|
|
86
65
|
*/
|
|
87
66
|
export class Router {
|
|
88
|
-
region: Region
|
|
89
|
-
routes: Compiled[]
|
|
67
|
+
readonly region: Region
|
|
68
|
+
private readonly routes: Compiled[]
|
|
90
69
|
/** The chain standing in the document, outermost layout first and the page last. */
|
|
91
|
-
levels:
|
|
92
|
-
/**
|
|
93
|
-
* The chain the address resolved to.
|
|
94
|
-
*
|
|
95
|
-
* A kept layout's `<slot/>` mounts from it whenever its region opens — read at that moment
|
|
96
|
-
* rather than captured when the layout mounted, or a slot reopened after a navigation put back
|
|
97
|
-
* the page the reader had left.
|
|
98
|
-
*/
|
|
99
|
-
chain: PageModule[]
|
|
70
|
+
readonly levels: LevelChain
|
|
100
71
|
/**
|
|
101
72
|
* The path the standing chain was mounted for: a step to it that moves only the query or the
|
|
102
73
|
* fragment keeps the page (30.23, 30.24).
|
|
103
74
|
*/
|
|
104
|
-
mounted: string | undefined
|
|
75
|
+
private mounted: string | undefined
|
|
105
76
|
/**
|
|
106
77
|
* The route the standing page was matched by.
|
|
107
78
|
*
|
|
108
79
|
* 30.23 — an address it matches again keeps it.
|
|
109
80
|
*/
|
|
110
|
-
standing: Compiled | undefined
|
|
81
|
+
private standing: Compiled | undefined
|
|
111
82
|
/**
|
|
112
83
|
* The ticket of the live navigation; a superseded one stops when it sees its own aborted.
|
|
113
84
|
*
|
|
@@ -124,40 +95,39 @@ export class Router {
|
|
|
124
95
|
* 30.31 — AN `AbortController`, so superseding a navigation also aborts the `middleware` request
|
|
125
96
|
* it is still waiting on, rather than leaving it to finish for nobody.
|
|
126
97
|
*/
|
|
127
|
-
navigation: AbortController
|
|
98
|
+
private navigation: AbortController
|
|
128
99
|
/**
|
|
129
100
|
* 35.18 — whether the document is an error page's. Its markup is no page this router has, so
|
|
130
101
|
* nothing hydrates over it, and leaving it is a document load.
|
|
131
102
|
*/
|
|
132
|
-
errorPage: boolean
|
|
103
|
+
private readonly errorPage: boolean
|
|
133
104
|
/** The place in this document's history of the entry the address bar stands at (`Entry`). */
|
|
134
|
-
index: number
|
|
105
|
+
private index: number
|
|
135
106
|
/**
|
|
136
107
|
* 30.29 — the scroll each entry was left at, by its `index`.
|
|
137
108
|
*
|
|
138
109
|
* HELD HERE AS WELL AS IN `history.state`, because a back or forward step has already moved the
|
|
139
110
|
* bar when `popstate` arrives, so the entry it left can no longer be written to.
|
|
140
111
|
*/
|
|
141
|
-
scrolls: (Scroll | undefined)[]
|
|
112
|
+
private readonly scrolls: (Scroll | undefined)[]
|
|
142
113
|
/**
|
|
143
114
|
* The routes whose chunks `prefetch` has already asked for.
|
|
144
115
|
*
|
|
145
116
|
* `pointerover` bubbles, so a pointer crossing a link's children asks again at every edge, and
|
|
146
117
|
* after the first time the answer is the module cache's anyway.
|
|
147
118
|
*/
|
|
148
|
-
prefetched: Set<Compiled>
|
|
119
|
+
private readonly prefetched: Set<Compiled>
|
|
149
120
|
/**
|
|
150
121
|
* Whether a navigation's write is a view transition, as the build read it (39.7, 39.8).
|
|
151
122
|
*
|
|
152
123
|
* Read off the compiled `<style>` blocks. Nothing here looks at the document's stylesheets.
|
|
153
124
|
*/
|
|
154
|
-
transitions: boolean
|
|
125
|
+
private readonly transitions: boolean
|
|
155
126
|
|
|
156
127
|
constructor(region: Region, routes: Compiled[], transitions: boolean) {
|
|
157
128
|
this.region = region
|
|
158
129
|
this.routes = routes
|
|
159
|
-
this.levels =
|
|
160
|
-
this.chain = []
|
|
130
|
+
this.levels = new LevelChain(region)
|
|
161
131
|
this.mounted = undefined
|
|
162
132
|
this.standing = undefined
|
|
163
133
|
this.navigation = new AbortController()
|
|
@@ -214,10 +184,30 @@ export class Router {
|
|
|
214
184
|
* fills itself from the template it carries. One module, both ways in — the `fillEmpty`
|
|
215
185
|
* the emitter already writes is the whole of the difference.
|
|
216
186
|
*/
|
|
217
|
-
|
|
187
|
+
go(url: URL | string, options: NavigateOptions = {}): Promise<void> {
|
|
188
|
+
return this.travel(url, options, PUSHED)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Arrives at the entry a back or forward step has already moved the address bar to.
|
|
193
|
+
*
|
|
194
|
+
* The page is mounted where the bar already points and nothing is pushed for it. 30.20 — a step
|
|
195
|
+
* within the standing page, back over an in-page `#anchor` jump, is `follow`'s: nothing asked,
|
|
196
|
+
* nothing remounted. 30.29 — the scroll is put back once the page it belongs to is mounted.
|
|
197
|
+
*/
|
|
198
|
+
popped(): Promise<void> {
|
|
199
|
+
return this.travel(
|
|
200
|
+
location.href,
|
|
201
|
+
{ replace: true, keepScroll: true },
|
|
202
|
+
this.stepped(),
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** `go`, arriving as `step` says: a push, or a history step with the scroll it restores. */
|
|
207
|
+
private async travel(
|
|
218
208
|
url: URL | string,
|
|
219
|
-
options: NavigateOptions
|
|
220
|
-
step: Step
|
|
209
|
+
options: NavigateOptions,
|
|
210
|
+
step: Step,
|
|
221
211
|
): Promise<void> {
|
|
222
212
|
const target =
|
|
223
213
|
typeof url === 'string' ? new URL(url, location.href) : url
|
|
@@ -354,7 +344,7 @@ export class Router {
|
|
|
354
344
|
* browser pushed itself — a link to a fragment — so it is stamped as the next entry, and
|
|
355
345
|
* arrived at as a push.
|
|
356
346
|
*/
|
|
357
|
-
stepped(): Step {
|
|
347
|
+
private stepped(): Step {
|
|
358
348
|
this.scrolls[this.index] = [scrollX, scrollY]
|
|
359
349
|
const state = history.state as Partial<Entry> | null
|
|
360
350
|
if (typeof state?.index !== 'number') {
|
|
@@ -468,37 +458,15 @@ export class Router {
|
|
|
468
458
|
// honest is which fraction of click -> paint the router's WORK is.
|
|
469
459
|
beginLayer(LAYER_NAVIGATE)
|
|
470
460
|
try {
|
|
471
|
-
// 41.11 — WHAT THE TWO ADDRESSES SHARE IS NOT REBUILT.
|
|
472
|
-
|
|
473
|
-
//
|
|
474
|
-
//
|
|
475
|
-
// exports is the same function either way.
|
|
476
|
-
const shared = this.sharedLevels(arriving)
|
|
477
|
-
// RECONCILED BEFORE THE STOP, while the leaving levels' heads read what they read on
|
|
478
|
-
// screen: what the server painted for them is adopted as theirs, so it leaves with them.
|
|
479
|
-
Level.reconcile(this.levels)
|
|
461
|
+
// 41.11 — WHAT THE TWO ADDRESSES SHARE IS NOT REBUILT.
|
|
462
|
+
const kept = this.levels.sharedWith(arriving)
|
|
463
|
+
// Read before anything is written: read after a region was emptied, it forces a layout
|
|
464
|
+
// and reads the offset that layout clamped.
|
|
480
465
|
const left: Scroll = [scrollX, scrollY]
|
|
481
|
-
this.
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
// NOWHERE, where that slot's region is closed: the arriving part is mounted by the slot
|
|
486
|
-
// when it opens, off `chain`, and not into a region no longer in the document.
|
|
487
|
-
const into =
|
|
488
|
-
shared === 0
|
|
489
|
-
? this.region
|
|
490
|
-
: (this.levels[shared - 1] as Level).slot
|
|
491
|
-
// EMPTIED THROUGH THE PRIMITIVE EVERY OTHER REGION IS. `fillRegion(region)` with no
|
|
492
|
-
// build is this removal, and it carries the 41.10 refusal for a region no longer in a
|
|
493
|
-
// document that a hand-written walk swallows.
|
|
494
|
-
if (into !== undefined) fillRegion(into)
|
|
495
|
-
this.writeAddress(target, options.replace === true, left)
|
|
496
|
-
// 39.6 — each arriving level applies its own head as it mounts into its emptied region,
|
|
497
|
-
// outermost first; a kept layout is not mounted again, so neither is its head.
|
|
498
|
-
this.mountChain(target, found, arriving, shared, into)
|
|
499
|
-
// 39.14 — what the leaving levels keyed and the arriving ones did not goes back to the
|
|
500
|
-
// nearest kept layout's, else leaves; their keyless nodes leave with them (39.16).
|
|
501
|
-
Level.reconcile(this.levels)
|
|
466
|
+
this.levels.replace(kept, arriving, () => {
|
|
467
|
+
this.writeAddress(target, options.replace === true, left)
|
|
468
|
+
this.arriveAt(target, found)
|
|
469
|
+
})
|
|
502
470
|
// 30.29 — where the entry was left, on a history step; else the top of the page, or —
|
|
503
471
|
// as the browser does on a load — the fragment's element.
|
|
504
472
|
if (step.scroll !== undefined)
|
|
@@ -509,134 +477,6 @@ export class Router {
|
|
|
509
477
|
}
|
|
510
478
|
}
|
|
511
479
|
|
|
512
|
-
/**
|
|
513
|
-
* How many levels of the arriving chain are already standing.
|
|
514
|
-
*
|
|
515
|
-
* Layouts only: a page reached here is another route's, the same route's being followed instead
|
|
516
|
-
* (30.23).
|
|
517
|
-
*
|
|
518
|
-
* EVERY STANDING LEVEL IS COMPARED, the page included: a page is never a layout, so it cannot
|
|
519
|
-
* match one, and a layout whose slot is closed stands with nothing below it.
|
|
520
|
-
*/
|
|
521
|
-
private sharedLevels(arriving: PageModule[]): number {
|
|
522
|
-
const layouts = arriving.length - 1
|
|
523
|
-
let shared = 0
|
|
524
|
-
while (
|
|
525
|
-
shared < layouts &&
|
|
526
|
-
shared < this.levels.length &&
|
|
527
|
-
(this.chain[shared] as PageModule).default ===
|
|
528
|
-
(arriving[shared] as PageModule).default
|
|
529
|
-
)
|
|
530
|
-
shared += 1
|
|
531
|
-
return shared
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
/**
|
|
535
|
-
* Stops every level from `at` down, inside out, and drops them.
|
|
536
|
-
*
|
|
537
|
-
* Stopped from the INSIDE OUT, so a layout's disposers run after the page's — the order a
|
|
538
|
-
* scope's own teardown already has, and the one an author reading two `watch` cleanups in
|
|
539
|
-
* two files would expect.
|
|
540
|
-
*/
|
|
541
|
-
private stopFrom(at: number): void {
|
|
542
|
-
for (let index = this.levels.length - 1; index >= at; index -= 1) {
|
|
543
|
-
const level = this.levels[index] as Level
|
|
544
|
-
// OUT OF THE CHAIN BEFORE IT STOPS, so a slot stop it fires on the way finds it gone
|
|
545
|
-
// (`release`) and leaves the head to whoever stopped it.
|
|
546
|
-
this.levels.length = index
|
|
547
|
-
// The level's head leaves whole, so none of its components withdraws its own.
|
|
548
|
-
level.contributed.clear()
|
|
549
|
-
level.scope?.stop()
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
/**
|
|
554
|
-
* Mounts the chain from level `at` into `region`, each level handed the next as `children`.
|
|
555
|
-
*
|
|
556
|
-
* 35.1 — each level is handed the one below it as `children` (32.18), so a layout puts the page
|
|
557
|
-
* in its `<slot/>` with the same mechanism a caller writing markup between two tags uses — and
|
|
558
|
-
* the router learns where that slot is on the way past, which is what lets it swap the page
|
|
559
|
-
* there later without the layout knowing.
|
|
560
|
-
*
|
|
561
|
-
* THE CHILD'S SCOPE IS THE ROUTER'S, not the layout's, so a kept layout cannot stop the page
|
|
562
|
-
* arriving under it. What `mount` hands the layout is a stop for WHATEVER STANDS IN THE SLOT
|
|
563
|
-
* when the slot's region leaves — a `<slot/>` inside a closed `{#if}` — rather than for the
|
|
564
|
-
* page that happened to be mounted first. And the slot mounts from `chain`, so a slot that
|
|
565
|
-
* opens again mounts the page the address resolves to now.
|
|
566
|
-
*/
|
|
567
|
-
private mountFrom(at: number, region: Region): void {
|
|
568
|
-
setUpLevels(
|
|
569
|
-
at,
|
|
570
|
-
this.chain.length,
|
|
571
|
-
this.levels[at - 1]?.share,
|
|
572
|
-
ROUTE_PROPS,
|
|
573
|
-
(below, props) => this.setUp(below, region, props),
|
|
574
|
-
)
|
|
575
|
-
this.markUp(at, region)
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
/**
|
|
579
|
-
* Runs level `at`'s setup over `props`, leaving its markup on the level for `markUp`, and
|
|
580
|
-
* answers the share scope the setup left.
|
|
581
|
-
*/
|
|
582
|
-
private setUp(
|
|
583
|
-
at: number,
|
|
584
|
-
region: Region,
|
|
585
|
-
props: Record<string, (() => unknown) | undefined>,
|
|
586
|
-
): ShareScope | null {
|
|
587
|
-
const module = this.chain[at] as PageModule
|
|
588
|
-
const level = new Level(this.levels, at)
|
|
589
|
-
this.levels[at] = level
|
|
590
|
-
if (at < this.chain.length - 1)
|
|
591
|
-
props.children = () => ({
|
|
592
|
-
// NO BUILD, because the level below carries its own (32.17): a module a route
|
|
593
|
-
// reaches fills the region it is mounted against when that region is empty, so
|
|
594
|
-
// the build and the mount are one call rather than two.
|
|
595
|
-
build: undefined,
|
|
596
|
-
mount: (slot: Region): Stoppable => {
|
|
597
|
-
// The level below as `mountFrom` set it up, the first time its slot opens; a
|
|
598
|
-
// slot opening again sets it up afresh.
|
|
599
|
-
const pending = this.levels[at + 1]?.markup !== undefined
|
|
600
|
-
if (!pending) this.release(level, at)
|
|
601
|
-
level.slot = slot
|
|
602
|
-
if (pending) this.markUp(at + 1, slot)
|
|
603
|
-
else this.mountFrom(at + 1, slot)
|
|
604
|
-
return { stop: () => this.release(level, at) }
|
|
605
|
-
},
|
|
606
|
-
})
|
|
607
|
-
// The region a level's markup lands in is its slot above, known only once that markup
|
|
608
|
-
// runs; a module mounted by the router leaves its markup rather than using this one.
|
|
609
|
-
// A deferred level answers the scope its setup made, so the watches that setup opened are
|
|
610
|
-
// stopped with the level even where its markup never runs — a slot that never opens.
|
|
611
|
-
level.scope = level.mount(() => module.default(region, props, level))
|
|
612
|
-
return level.share
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
/** Runs the markup level `at`'s setup left, into `region`. */
|
|
616
|
-
private markUp(at: number, region: Region): void {
|
|
617
|
-
const level = this.levels[at] as Level
|
|
618
|
-
const markup = level.markup
|
|
619
|
-
if (markup === undefined) return
|
|
620
|
-
level.markup = undefined
|
|
621
|
-
level.scope = level.mount(() => markup(region))
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
/**
|
|
625
|
-
* Stops what stands below `level` and forgets its slot.
|
|
626
|
-
*
|
|
627
|
-
* A no-op once the level itself has been replaced, which is how a stop handed to a layout that
|
|
628
|
-
* has since left does nothing.
|
|
629
|
-
*/
|
|
630
|
-
private release(level: Level, at: number): void {
|
|
631
|
-
if (this.levels[at] !== level) return
|
|
632
|
-
level.slot = undefined
|
|
633
|
-
// 39.14, 39.16 — THE LEVELS BELOW LEAVE WITH THE SLOT'S REGION, and their head with them:
|
|
634
|
-
// reconciled first, so what the server painted for them is adopted as theirs.
|
|
635
|
-
Level.reconcile(this.levels)
|
|
636
|
-
this.stopFrom(at + 1)
|
|
637
|
-
Level.reconcile(this.levels)
|
|
638
|
-
}
|
|
639
|
-
|
|
640
480
|
/**
|
|
641
481
|
* Hydrates the page the server painted, wiring it where it stands.
|
|
642
482
|
*
|
|
@@ -663,38 +503,22 @@ export class Router {
|
|
|
663
503
|
if (ticket.signal.aborted) return
|
|
664
504
|
// THE HYDRATE SHARE. Same name as the navigation's mount because it is the same work on the
|
|
665
505
|
// same chain — what differs is the op it is a fraction of.
|
|
666
|
-
this.
|
|
506
|
+
this.arriveAt(new URL(location.href), found)
|
|
507
|
+
this.levels.hydrate(standing)
|
|
667
508
|
// 30.30 — A RELOAD lands on an entry that carries the scroll `remember` wrote on the way out.
|
|
668
509
|
const scroll = (history.state as Partial<Entry> | null)?.scroll
|
|
669
510
|
if (scroll != null) scrollTo(scroll[0], scroll[1])
|
|
670
511
|
}
|
|
671
512
|
|
|
672
513
|
/**
|
|
673
|
-
* Records the arriving
|
|
514
|
+
* Records the arriving route as standing at `target`.
|
|
674
515
|
*
|
|
675
|
-
*
|
|
676
|
-
* `shares()` SUMS every measure of a name inside the op — so a pair inside the recursion would
|
|
677
|
-
* count the innermost level once per level above it. 23.1 — WRITTEN BEFORE THE ARRIVING PAGE'S
|
|
678
|
-
* SETUP, which reads it.
|
|
516
|
+
* 23.1 — WRITTEN BEFORE THE ARRIVING PAGE'S SETUP, which reads it.
|
|
679
517
|
*/
|
|
680
|
-
private
|
|
681
|
-
target: URL,
|
|
682
|
-
found: Found,
|
|
683
|
-
modules: PageModule[],
|
|
684
|
-
from: number,
|
|
685
|
-
into: Region | undefined,
|
|
686
|
-
): void {
|
|
518
|
+
private arriveAt(target: URL, found: Found): void {
|
|
687
519
|
arrive(target.href, found.route.pattern, found.params)
|
|
688
|
-
this.chain = modules
|
|
689
520
|
this.standing = found.route
|
|
690
521
|
this.mounted = target.pathname
|
|
691
|
-
if (into === undefined) return
|
|
692
|
-
beginLayer(LAYER_MOUNT)
|
|
693
|
-
try {
|
|
694
|
-
this.mountFrom(from, into)
|
|
695
|
-
} finally {
|
|
696
|
-
endLayer(LAYER_MOUNT)
|
|
697
|
-
}
|
|
698
522
|
}
|
|
699
523
|
}
|
|
700
524
|
|
|
@@ -764,21 +588,6 @@ async function ask(
|
|
|
764
588
|
}
|
|
765
589
|
}
|
|
766
590
|
|
|
767
|
-
/**
|
|
768
|
-
* The props a routed page or layout is set up over, `children` aside: `params` as a live accessor.
|
|
769
|
-
*
|
|
770
|
-
* 33.4 — A PROP IS AN ACCESSOR. A page is mounted as any other component is, so its one prop is
|
|
771
|
-
* passed the way `renderComponent` passes a child's and the way `answerRequest` paints one: a
|
|
772
|
-
* function per key. Passed as the plain record it lowered to `params()` on a value that was not
|
|
773
|
-
* one, so every page with a `[param]` in its address hydrated dead — with its server markup
|
|
774
|
-
* already on screen, which is what kept it quiet.
|
|
775
|
-
*
|
|
776
|
-
* 33.5 — AND A LIVE ONE: `params` reads `route.params`, which every navigation writes. A layout
|
|
777
|
-
* kept across `/users/1` -> `/users/2` is not mounted again, so an accessor over the params it was
|
|
778
|
-
* mounted with went on answering user 1.
|
|
779
|
-
*/
|
|
780
|
-
const ROUTE_PROPS = { params: (): Params => route.params() }
|
|
781
|
-
|
|
782
591
|
function entryAt(index: number, scroll: Scroll | null): Entry {
|
|
783
592
|
return { index, scroll }
|
|
784
593
|
}
|
package/src/ui/Scope.ts
CHANGED
|
@@ -11,8 +11,22 @@ type Boundary = {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
/** The boundary a scope built now falls under, or `undefined` outside every `{#try}` body. */
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
let ambientBoundary: Boundary | undefined
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Runs `run` with every scope it builds falling under `boundary` (32.16), restoring the one around it.
|
|
18
|
+
*
|
|
19
|
+
* The ambient is this module's so `Scope`'s field reads a local at construction; `TryRegion` is the
|
|
20
|
+
* one writer, and a page with no `{#try}` does not import it.
|
|
21
|
+
*/
|
|
22
|
+
export function underBoundary<T>(boundary: Boundary, run: () => T): T {
|
|
23
|
+
const previous = ambientBoundary
|
|
24
|
+
ambientBoundary = boundary
|
|
25
|
+
try {
|
|
26
|
+
return run()
|
|
27
|
+
} finally {
|
|
28
|
+
ambientBoundary = previous
|
|
29
|
+
}
|
|
16
30
|
}
|
|
17
31
|
|
|
18
32
|
/**
|
|
@@ -54,7 +68,7 @@ export class Scope {
|
|
|
54
68
|
*
|
|
55
69
|
* 32.16 — read once at construction.
|
|
56
70
|
*/
|
|
57
|
-
boundary: Boundary | undefined =
|
|
71
|
+
boundary: Boundary | undefined = ambientBoundary
|
|
58
72
|
/**
|
|
59
73
|
* 32.67 — where in `stops` a row's item watches start (`itemWatch`), and how many there are:
|
|
60
74
|
* opened one after another by a row's mount, so two numbers find them and the row allocates
|
package/src/ui/TryRegion.ts
CHANGED
|
@@ -19,13 +19,8 @@ import { type Disposer, watchTracked } from '#shared/reactive/watch.ts'
|
|
|
19
19
|
|
|
20
20
|
import { type Arm, type ArmMakers, ArmRegion } from './ArmRegion.ts'
|
|
21
21
|
import { Mismatch } from './Mismatch.ts'
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
isEmptyRegion,
|
|
25
|
-
type Region,
|
|
26
|
-
removeUntil,
|
|
27
|
-
} from './regions.ts'
|
|
28
|
-
import { BOUNDARY, type Scope, type Stoppable } from './Scope.ts'
|
|
22
|
+
import { fillRegion, type Region, removeUntil } from './regions.ts'
|
|
23
|
+
import { type Scope, type Stoppable, underBoundary } from './Scope.ts'
|
|
29
24
|
|
|
30
25
|
/**
|
|
31
26
|
* The body until something in it fails, then the catch until the failure clears. Arm `0` is the
|
|
@@ -40,7 +35,7 @@ import { BOUNDARY, type Scope, type Stoppable } from './Scope.ts'
|
|
|
40
35
|
* anchor names as arm 1 (`namedArm`) — it keeps until the body has been tried behind it
|
|
41
36
|
* (`mountBehind`), and adopts where the body fails here too, so the catch the server sent is the
|
|
42
37
|
* one on the page. A body that succeeds here after failing there replaces it. So 41.24 is already
|
|
43
|
-
* this region's shape — its open names the arm — and `paintedOther`
|
|
38
|
+
* this region's shape — its open names the arm — and `paintedOther` answers no.
|
|
44
39
|
*/
|
|
45
40
|
export class TryRegion extends ArmRegion {
|
|
46
41
|
readonly failure: Reactive<unknown>
|
|
@@ -114,17 +109,13 @@ export class TryRegion extends ArmRegion {
|
|
|
114
109
|
* `again` IS WHAT A RETRY RERUNS: the binding that threw, or the body mounted again.
|
|
115
110
|
*/
|
|
116
111
|
guard<T>(run: () => T, again: () => unknown): T | undefined {
|
|
117
|
-
const previous = BOUNDARY.current
|
|
118
|
-
BOUNDARY.current = this
|
|
119
112
|
try {
|
|
120
|
-
return run
|
|
113
|
+
return underBoundary(this, run)
|
|
121
114
|
} catch (thrown) {
|
|
122
115
|
if (thrown instanceof Mismatch || thrown instanceof NotLanded)
|
|
123
116
|
throw thrown
|
|
124
117
|
this.fail(thrown, again)
|
|
125
118
|
return undefined
|
|
126
|
-
} finally {
|
|
127
|
-
BOUNDARY.current = previous
|
|
128
119
|
}
|
|
129
120
|
}
|
|
130
121
|
|
|
@@ -139,11 +130,9 @@ export class TryRegion extends ArmRegion {
|
|
|
139
130
|
const scope = this.scope as Scope
|
|
140
131
|
let next = first
|
|
141
132
|
while (next !== this.at) {
|
|
142
|
-
const
|
|
133
|
+
const filling = this.fills(next)
|
|
143
134
|
this.at = next
|
|
144
|
-
const arm =
|
|
145
|
-
this.failure.peek(),
|
|
146
|
-
)
|
|
135
|
+
const arm = this.armOf(next)
|
|
147
136
|
const standing = next === 0 ? this.standing : undefined
|
|
148
137
|
if (standing !== undefined) {
|
|
149
138
|
this.standing = undefined
|
|
@@ -151,7 +140,7 @@ export class TryRegion extends ArmRegion {
|
|
|
151
140
|
scope.own(standing)
|
|
152
141
|
continue
|
|
153
142
|
}
|
|
154
|
-
if (
|
|
143
|
+
if (filling) {
|
|
155
144
|
scope.stop()
|
|
156
145
|
if (
|
|
157
146
|
next === 0 &&
|
|
@@ -175,6 +164,17 @@ export class TryRegion extends ArmRegion {
|
|
|
175
164
|
}
|
|
176
165
|
}
|
|
177
166
|
|
|
167
|
+
/** The catch is handed the failure it binds. */
|
|
168
|
+
override armOf(next: unknown): Arm {
|
|
169
|
+
return (this.arms[next as number] as ArmMakers[number])(
|
|
170
|
+
this.failure.peek(),
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
override paintedOther(): boolean {
|
|
175
|
+
return false
|
|
176
|
+
}
|
|
177
|
+
|
|
178
178
|
private fail(error: unknown, again: () => unknown): void {
|
|
179
179
|
if (this.trying) {
|
|
180
180
|
if (this.triedFailed) return
|
|
@@ -238,14 +238,14 @@ export class TryRegion extends ArmRegion {
|
|
|
238
238
|
open: region.close.previousSibling as Comment,
|
|
239
239
|
close: region.close,
|
|
240
240
|
}
|
|
241
|
-
const previous = BOUNDARY.current
|
|
242
|
-
BOUNDARY.current = this
|
|
243
241
|
this.trying = true
|
|
244
242
|
this.triedFailed = false
|
|
245
243
|
let mounted: Stoppable | undefined
|
|
246
244
|
try {
|
|
247
|
-
|
|
248
|
-
|
|
245
|
+
mounted = underBoundary(this, () => {
|
|
246
|
+
fillRegion(body, arm.build)
|
|
247
|
+
return arm.mount(body)
|
|
248
|
+
})
|
|
249
249
|
} catch (thrown) {
|
|
250
250
|
fillRegion(body)
|
|
251
251
|
if (thrown instanceof Mismatch || thrown instanceof NotLanded)
|
|
@@ -254,7 +254,6 @@ export class TryRegion extends ArmRegion {
|
|
|
254
254
|
this.triedError = thrown
|
|
255
255
|
this.triedAgain = undefined
|
|
256
256
|
} finally {
|
|
257
|
-
BOUNDARY.current = previous
|
|
258
257
|
this.trying = false
|
|
259
258
|
}
|
|
260
259
|
if (!this.triedFailed) {
|
package/src/ui/bindSpread.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { spreadEntries } from '#shared/spread.ts'
|
|
6
6
|
|
|
7
7
|
import type { Scope } from './Scope.ts'
|
|
8
|
-
import {
|
|
8
|
+
import { writeAttribute } from './setAttribute.ts'
|
|
9
9
|
|
|
10
10
|
/** Keeps an element's spread attributes and `on<event>` listeners written as the value changes. */
|
|
11
11
|
export function bindSpread(
|
|
@@ -21,9 +21,7 @@ export function bindSpread(
|
|
|
21
21
|
const next = new Set<string>()
|
|
22
22
|
for (const [name, text] of spreadEntries(read(), explicit, wanted)) {
|
|
23
23
|
next.add(name)
|
|
24
|
-
|
|
25
|
-
if (standing === null) addAttribute(element, name, text)
|
|
26
|
-
else if (standing !== text) element.setAttribute(name, text)
|
|
24
|
+
writeAttribute(element, name, text)
|
|
27
25
|
}
|
|
28
26
|
for (const name of written)
|
|
29
27
|
if (!next.has(name)) element.removeAttribute(name)
|
|
@@ -29,11 +29,8 @@ export function listenForRoutes(router: Router): () => void {
|
|
|
29
29
|
if (url !== undefined) router.prefetch(url.pathname)
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
// BACK AND FORWARD HAVE ALREADY MOVED THE ADDRESS BAR by the time this runs
|
|
33
|
-
//
|
|
34
|
-
// standing page, back over an in-page `#anchor` jump, is `go`'s to keep: nothing asked, nothing
|
|
35
|
-
// remounted. 30.29 — the scroll is the router's to put back, once the page it belongs to is
|
|
36
|
-
// mounted: the browser's own restoration runs before that page exists.
|
|
32
|
+
// BACK AND FORWARD HAVE ALREADY MOVED THE ADDRESS BAR by the time this runs (`popped`). 30.29 —
|
|
33
|
+
// the browser's own scroll restoration runs before the page it restores exists, so it is off.
|
|
37
34
|
const onPop = (): void => {
|
|
38
35
|
if (router.match(location.pathname) === undefined) {
|
|
39
36
|
// The bar already stands at the entry being loaded, so the `pagehide` this reload
|
|
@@ -42,11 +39,7 @@ export function listenForRoutes(router: Router): () => void {
|
|
|
42
39
|
location.reload()
|
|
43
40
|
return
|
|
44
41
|
}
|
|
45
|
-
void router.
|
|
46
|
-
location.href,
|
|
47
|
-
{ replace: true, keepScroll: true },
|
|
48
|
-
router.stepped(),
|
|
49
|
-
)
|
|
42
|
+
void router.popped()
|
|
50
43
|
}
|
|
51
44
|
|
|
52
45
|
const listening = new AbortController()
|
package/src/ui/outboxQueue.ts
CHANGED
|
@@ -69,9 +69,9 @@ let askedToPersist = false
|
|
|
69
69
|
*/
|
|
70
70
|
const VALUES = new Map<string, Reactive<unknown>>()
|
|
71
71
|
/** What wants to hear the line empty of calls not yet delivered (18.35). */
|
|
72
|
-
const DRAINED
|
|
72
|
+
const DRAINED = new Set<() => void>()
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
let shelf: Shelf =
|
|
75
75
|
typeof indexedDB === 'undefined' ? memoryShelf() : databaseShelf()
|
|
76
76
|
const channel =
|
|
77
77
|
typeof BroadcastChannel === 'undefined'
|
|
@@ -87,7 +87,25 @@ export function undelivered(): number {
|
|
|
87
87
|
|
|
88
88
|
/** Runs `listener` each time the line holds no call waiting to be delivered (18.35). */
|
|
89
89
|
export function whenDelivered(listener: () => void): void {
|
|
90
|
-
DRAINED.
|
|
90
|
+
DRAINED.add(listener)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Empties the line and its timers onto a fresh in-memory shelf.
|
|
95
|
+
*
|
|
96
|
+
* A page never does; a test does between cases, rather than discarding a call at a time and waiting
|
|
97
|
+
* out each discard's write.
|
|
98
|
+
*/
|
|
99
|
+
export function resetOutbox(): void {
|
|
100
|
+
clearTimeout(retrying)
|
|
101
|
+
retrying = undefined
|
|
102
|
+
backoff = RETRY_FLOOR_MS
|
|
103
|
+
held = false
|
|
104
|
+
waiting = false
|
|
105
|
+
LINE = []
|
|
106
|
+
VALUES.clear()
|
|
107
|
+
shelf = memoryShelf()
|
|
108
|
+
publish()
|
|
91
109
|
}
|
|
92
110
|
|
|
93
111
|
/**
|
package/src/ui/reconcile.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { patchEpoch, writtenSince } from '#shared/reactive/patchMarks.ts'
|
|
2
2
|
|
|
3
3
|
import { LAYER_RECONCILE } from './LAYER_NAMES.ts'
|
|
4
4
|
import type { List } from './List.ts'
|
|
@@ -116,7 +116,7 @@ export function reconcile(list: List, data: unknown[]): void {
|
|
|
116
116
|
const value = data[at]
|
|
117
117
|
if (!Object.is(existing.data, value) || patchedSince(existing))
|
|
118
118
|
updateRow(existing, value)
|
|
119
|
-
existing.seen =
|
|
119
|
+
existing.seen = patchEpoch()
|
|
120
120
|
survivor[at] = existing
|
|
121
121
|
from[at] = existing.at
|
|
122
122
|
if (existing.at < lastSurvivor) ordered = false
|
|
@@ -210,10 +210,8 @@ function clear(list: List): void {
|
|
|
210
210
|
* one integer compare a row and never reaches the map.
|
|
211
211
|
*/
|
|
212
212
|
function patchedSince(row: Row): boolean {
|
|
213
|
-
if (row.seen ===
|
|
214
|
-
|
|
215
|
-
if (item === null || typeof item !== 'object') return false
|
|
216
|
-
return (PATCHES.written.get(item) ?? 0) > row.seen
|
|
213
|
+
if (row.seen === patchEpoch()) return false
|
|
214
|
+
return writtenSince(row.data, row.seen)
|
|
217
215
|
}
|
|
218
216
|
|
|
219
217
|
/**
|