@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/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
|
@@ -19,14 +19,8 @@ import { failedFrom, isFailed, problemOf } from '#shared/Failed.ts'
|
|
|
19
19
|
import type { Problem } from '#shared/Failed.ts'
|
|
20
20
|
import { showLine, type ShownCall, showSending } from '#shared/outbox.ts'
|
|
21
21
|
import { onPrincipalRotated } from '#shared/principalRotated.ts'
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
produceInto,
|
|
25
|
-
type Reactive,
|
|
26
|
-
reactive,
|
|
27
|
-
refuseInto,
|
|
28
|
-
standIn,
|
|
29
|
-
} from '#shared/reactive/index.ts'
|
|
22
|
+
import { isReactive, type Reactive, reactive } from '#shared/reactive/index.ts'
|
|
23
|
+
import { produceInto, refuseInto, standIn } from '#shared/reactive/producer.ts'
|
|
30
24
|
import { rpcRequest } from '#shared/rpcRequest.ts'
|
|
31
25
|
|
|
32
26
|
const DATABASE = 'abide-outbox'
|
|
@@ -69,9 +63,9 @@ let askedToPersist = false
|
|
|
69
63
|
*/
|
|
70
64
|
const VALUES = new Map<string, Reactive<unknown>>()
|
|
71
65
|
/** What wants to hear the line empty of calls not yet delivered (18.35). */
|
|
72
|
-
const DRAINED
|
|
66
|
+
const DRAINED = new Set<() => void>()
|
|
73
67
|
|
|
74
|
-
|
|
68
|
+
let shelf: Shelf =
|
|
75
69
|
typeof indexedDB === 'undefined' ? memoryShelf() : databaseShelf()
|
|
76
70
|
const channel =
|
|
77
71
|
typeof BroadcastChannel === 'undefined'
|
|
@@ -87,7 +81,25 @@ export function undelivered(): number {
|
|
|
87
81
|
|
|
88
82
|
/** Runs `listener` each time the line holds no call waiting to be delivered (18.35). */
|
|
89
83
|
export function whenDelivered(listener: () => void): void {
|
|
90
|
-
DRAINED.
|
|
84
|
+
DRAINED.add(listener)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Empties the line and its timers onto a fresh in-memory shelf.
|
|
89
|
+
*
|
|
90
|
+
* A page never does; a test does between cases, rather than discarding a call at a time and waiting
|
|
91
|
+
* out each discard's write.
|
|
92
|
+
*/
|
|
93
|
+
export function resetOutbox(): void {
|
|
94
|
+
clearTimeout(retrying)
|
|
95
|
+
retrying = undefined
|
|
96
|
+
backoff = RETRY_FLOOR_MS
|
|
97
|
+
held = false
|
|
98
|
+
waiting = false
|
|
99
|
+
LINE = []
|
|
100
|
+
VALUES.clear()
|
|
101
|
+
shelf = memoryShelf()
|
|
102
|
+
publish()
|
|
91
103
|
}
|
|
92
104
|
|
|
93
105
|
/**
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { Params } from '#shared/matchRoute.ts'
|
|
2
|
+
|
|
3
|
+
/** A route an address matched, and the params it matched with. */
|
|
4
|
+
export type Found<RouteRow> = { route: RouteRow; params: Params }
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* What a navigation knows before it asks anything: where it is going, and what stands.
|
|
8
|
+
*
|
|
9
|
+
* `match` is the router's own, so 16.10's mount and 30.9's ranking are decided where they always
|
|
10
|
+
* were.
|
|
11
|
+
*/
|
|
12
|
+
export type Bearing<RouteRow> = {
|
|
13
|
+
target: URL
|
|
14
|
+
/** The document's origin, which an address outside of is no route (30.10). */
|
|
15
|
+
origin: string
|
|
16
|
+
/** 35.18 — the document is an error page's, so leaving it is a document load. */
|
|
17
|
+
errorPage: boolean
|
|
18
|
+
/** The route the standing page was matched by. */
|
|
19
|
+
standing: RouteRow | undefined
|
|
20
|
+
/** The path the standing chain was mounted for. */
|
|
21
|
+
mounted: string | undefined
|
|
22
|
+
match: (pathname: string) => Found<RouteRow> | undefined
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** What the ask and the load came back with (30.15). */
|
|
26
|
+
export type Heard<RouteRow> = {
|
|
27
|
+
/** The route the ask was made for — the `ask` outcome's `found`. */
|
|
28
|
+
found: Found<RouteRow>
|
|
29
|
+
/** The server's answer; `undefined` where the fetch failed. */
|
|
30
|
+
answer: { status: number; url: string; redirected: boolean } | undefined
|
|
31
|
+
/** Whether the asked-for route's chunks loaded. */
|
|
32
|
+
loaded: boolean
|
|
33
|
+
online: boolean
|
|
34
|
+
/** Whether the navigation pushes, rather than arriving at a history step the bar already took. */
|
|
35
|
+
pushed: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* How a navigation ends.
|
|
40
|
+
*
|
|
41
|
+
* `document` hands `href` to a document load. `follow` moves the address within the standing page.
|
|
42
|
+
* `ask` is the first call's answer where the server must be asked first. `offline` is 30.32's: the
|
|
43
|
+
* page stays, unless the service worker saved `href` (45.12). `mount` mounts `found` at `at`.
|
|
44
|
+
*/
|
|
45
|
+
type Outcome<RouteRow> =
|
|
46
|
+
| Settled<RouteRow>
|
|
47
|
+
| { kind: 'ask'; found: Found<RouteRow> }
|
|
48
|
+
| { kind: 'offline'; href: string }
|
|
49
|
+
| { kind: 'mount'; at: URL; found: Found<RouteRow> }
|
|
50
|
+
|
|
51
|
+
/** An outcome that loads nothing, which either call can answer. */
|
|
52
|
+
export type Settled<RouteRow> =
|
|
53
|
+
| { kind: 'document'; href: string }
|
|
54
|
+
| { kind: 'follow'; at: URL; found: Found<RouteRow> }
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Decides how a navigation ends, from what it knows: called once before the ask, and once more
|
|
58
|
+
* with what was `heard` where the first call answered `ask`.
|
|
59
|
+
*
|
|
60
|
+
* Decides and touches nothing — the router executes the answer. A `mount` whose `found` is not
|
|
61
|
+
* `heard.found` lands on a route whose chunks were not loaded yet, and the router hands that
|
|
62
|
+
* address to the document where they will not load (30.27).
|
|
63
|
+
*/
|
|
64
|
+
export function outcomeOf<RouteRow>(
|
|
65
|
+
bearing: Bearing<RouteRow>,
|
|
66
|
+
): Settled<RouteRow> | Extract<Outcome<RouteRow>, { kind: 'ask' }>
|
|
67
|
+
export function outcomeOf<RouteRow>(
|
|
68
|
+
bearing: Bearing<RouteRow>,
|
|
69
|
+
heard: Heard<RouteRow>,
|
|
70
|
+
): Settled<RouteRow> | Extract<Outcome<RouteRow>, { kind: 'offline' | 'mount' }>
|
|
71
|
+
export function outcomeOf<RouteRow>(
|
|
72
|
+
bearing: Bearing<RouteRow>,
|
|
73
|
+
heard?: Heard<RouteRow>,
|
|
74
|
+
): Outcome<RouteRow> {
|
|
75
|
+
const target = bearing.target
|
|
76
|
+
if (heard === undefined) {
|
|
77
|
+
// A CROSS-ORIGIN ADDRESS IS NOT A ROUTE. Handing it to `pushState` throws, and treating
|
|
78
|
+
// it as a miss would leave the caller with a navigation that silently did nothing.
|
|
79
|
+
if (target.origin !== bearing.origin || bearing.errorPage)
|
|
80
|
+
return { kind: 'document', href: target.href }
|
|
81
|
+
const found = bearing.match(target.pathname)
|
|
82
|
+
// 30.10 — NOT A ROUTE THIS BUILD KNOWS, so it is the server's to answer — which is a
|
|
83
|
+
// document load, and the one case where 30.7 does not apply because there is no route.
|
|
84
|
+
if (found === undefined) return { kind: 'document', href: target.href }
|
|
85
|
+
// 30.21, 30.23, 30.24 — THE ROUTE STANDING, AT ITS OWN PATH: another query, another
|
|
86
|
+
// fragment or the same address. The page reads its address through `route`, so it follows
|
|
87
|
+
// rather than being rebuilt, and there is no path for `middleware` to decide anything new
|
|
88
|
+
// about.
|
|
89
|
+
if (
|
|
90
|
+
found.route === bearing.standing &&
|
|
91
|
+
bearing.mounted === target.pathname
|
|
92
|
+
)
|
|
93
|
+
return { kind: 'follow', at: target, found }
|
|
94
|
+
return { kind: 'ask', found }
|
|
95
|
+
}
|
|
96
|
+
const answer = heard.answer
|
|
97
|
+
// 30.32 — OFFLINE, THE DOCUMENT LOAD BELOW CAN ONLY PAINT THE BROWSER'S ERROR PAGE over a page
|
|
98
|
+
// that works. A history step has already moved the bar, so it is left to the document.
|
|
99
|
+
if (answer === undefined && !heard.online && heard.pushed)
|
|
100
|
+
return { kind: 'offline', href: target.href }
|
|
101
|
+
// 30.18 — A REFUSAL, A RESPONSE MIDDLEWARE ANSWERED ITSELF, or no answer at all: the document
|
|
102
|
+
// asks again and paints whatever the server says. A fetch that failed — offline, or a redirect
|
|
103
|
+
// to another origin that CORS will not let it follow — goes to the address it was sent to.
|
|
104
|
+
// 30.27 — AND CHUNKS THAT WOULD NOT LOAD, with the server down or a deploy that removed them.
|
|
105
|
+
if (answer?.status !== 204 || !heard.loaded)
|
|
106
|
+
return { kind: 'document', href: answer?.url ?? target.href }
|
|
107
|
+
// 30.17 — REDIRECTED, and the server already said yes to where it landed: the answer is the
|
|
108
|
+
// 204 at the end of the chain. That address is mounted in place of the one asked for, and a
|
|
109
|
+
// route the client has no row for is the document's (30.10).
|
|
110
|
+
const landed = answer.redirected ? new URL(answer.url) : target
|
|
111
|
+
const landing = answer.redirected
|
|
112
|
+
? bearing.match(landed.pathname)
|
|
113
|
+
: heard.found
|
|
114
|
+
if (landing === undefined) return { kind: 'document', href: landed.href }
|
|
115
|
+
// 30.23 — THE ROUTE STANDING, AT ANOTHER PATH: `middleware` has said yes to the path, and the
|
|
116
|
+
// page it matched is the one already mounted, so it follows.
|
|
117
|
+
if (landing.route === bearing.standing)
|
|
118
|
+
return { kind: 'follow', at: landed, found: landing }
|
|
119
|
+
return { kind: 'mount', at: landed, found: landing }
|
|
120
|
+
}
|
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
|
/**
|
package/src/ui/routing.ts
CHANGED
|
@@ -16,4 +16,5 @@
|
|
|
16
16
|
|
|
17
17
|
export { listenForRoutes } from './listenForRoutes.ts'
|
|
18
18
|
export { registerWorker } from './registerWorker.ts'
|
|
19
|
-
export { type PageMount
|
|
19
|
+
export { type PageMount } from './LevelChain.ts'
|
|
20
|
+
export { type Route, Router, startRouter } from './Router.ts'
|
package/src/ui/rows.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { REGION_CLOSE, ROW_OPEN } from '#shared/ANCHORS.ts'
|
|
2
2
|
import { moveItem, rowItem } from '#shared/reactive/index.ts'
|
|
3
|
-
import {
|
|
3
|
+
import { patchEpoch } from '#shared/reactive/patchMarks.ts'
|
|
4
4
|
|
|
5
5
|
import { leave } from './leave.ts'
|
|
6
6
|
import { moveNode } from './moveNode.ts'
|
|
@@ -291,7 +291,7 @@ function newRow(key: unknown, data: unknown, node: ChildNode, at: number): Row {
|
|
|
291
291
|
// for the record's whole life.
|
|
292
292
|
scope: EMPTY_SCOPE,
|
|
293
293
|
at,
|
|
294
|
-
seen:
|
|
294
|
+
seen: patchEpoch(),
|
|
295
295
|
}
|
|
296
296
|
}
|
|
297
297
|
|
package/src/ui/setAttribute.ts
CHANGED
|
@@ -35,10 +35,19 @@ export function setAttribute(
|
|
|
35
35
|
if (element.hasAttribute(name)) element.removeAttribute(name)
|
|
36
36
|
return
|
|
37
37
|
}
|
|
38
|
+
writeAttribute(element, name, next)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Writes `text` to `element`'s `name` unless it already reads `text` — `setAttribute`'s compare, shared with a spread. */
|
|
42
|
+
export function writeAttribute(
|
|
43
|
+
element: Element,
|
|
44
|
+
name: string,
|
|
45
|
+
text: string,
|
|
46
|
+
): void {
|
|
38
47
|
const standing = element.getAttribute(name)
|
|
39
|
-
if (standing ===
|
|
40
|
-
if (standing === null) addAttribute(element, name,
|
|
41
|
-
else element.setAttribute(name,
|
|
48
|
+
if (standing === text) return
|
|
49
|
+
if (standing === null) addAttribute(element, name, text)
|
|
50
|
+
else element.setAttribute(name, text)
|
|
42
51
|
}
|
|
43
52
|
|
|
44
53
|
/**
|
package/src/ui/socketWire.ts
CHANGED
|
@@ -7,16 +7,16 @@ import { applyPatch } from '#shared/applyPatch.ts'
|
|
|
7
7
|
import { mounted } from '#shared/mount.ts'
|
|
8
8
|
import type { BrowserFrame, ServerFrame } from '#shared/muxFrames.ts'
|
|
9
9
|
import { onPrincipalRotated } from '#shared/principalRotated.ts'
|
|
10
|
-
import { canonicalKey } from '#shared/reactive/
|
|
10
|
+
import { canonicalKey } from '#shared/reactive/argsContract.ts'
|
|
11
|
+
import { type Reactive } from '#shared/reactive/index.ts'
|
|
12
|
+
import { patchEpoch, writtenSince } from '#shared/reactive/patchMarks.ts'
|
|
11
13
|
import {
|
|
12
14
|
heldGenerationOf,
|
|
13
15
|
loadOf,
|
|
14
16
|
patchInto,
|
|
15
17
|
produceInto,
|
|
16
|
-
type Reactive,
|
|
17
18
|
writtenLocally,
|
|
18
|
-
} from '#shared/reactive/
|
|
19
|
-
import { PATCHES } from '#shared/reactive/PATCHES.ts'
|
|
19
|
+
} from '#shared/reactive/producer.ts'
|
|
20
20
|
|
|
21
21
|
import { HYDRATE_LOG } from './Mismatch.ts'
|
|
22
22
|
import { undelivered, whenDelivered } from './outboxQueue.ts'
|
|
@@ -64,18 +64,13 @@ class Following {
|
|
|
64
64
|
inStep(): void {
|
|
65
65
|
const copy = this.entry()
|
|
66
66
|
this.generation = loadOf(copy) ?? heldGenerationOf(copy)
|
|
67
|
-
this.epoch =
|
|
67
|
+
this.epoch = patchEpoch()
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
/** Whether the copy was written to since it was last in step. 18.21. */
|
|
71
71
|
diverged(copy: unknown): boolean {
|
|
72
72
|
if (heldGenerationOf(copy) !== this.generation) return true
|
|
73
|
-
|
|
74
|
-
return (
|
|
75
|
-
held !== null &&
|
|
76
|
-
typeof held === 'object' &&
|
|
77
|
-
(PATCHES.written.get(held) ?? 0) > this.epoch
|
|
78
|
-
)
|
|
73
|
+
return writtenSince((copy as Reactive<unknown>).peek(), this.epoch)
|
|
79
74
|
}
|
|
80
75
|
|
|
81
76
|
/**
|
|
@@ -241,10 +236,17 @@ class Mux {
|
|
|
241
236
|
receive(frame: ServerFrame): void {
|
|
242
237
|
this.subscriptions.get(frame.id)?.receive(frame)
|
|
243
238
|
}
|
|
239
|
+
|
|
240
|
+
/** Lets go of every subscription and the socket, so a sign-in or a retry reaches nothing. */
|
|
241
|
+
dispose(): void {
|
|
242
|
+
clearTimeout(this.retrying)
|
|
243
|
+
this.retrying = undefined
|
|
244
|
+
this.subscriptions.clear()
|
|
245
|
+
this.reopen()
|
|
246
|
+
}
|
|
244
247
|
}
|
|
245
248
|
|
|
246
|
-
|
|
247
|
-
export const MUXES = new Map<string, Mux>()
|
|
249
|
+
const MUXES = new Map<string, Mux>()
|
|
248
250
|
|
|
249
251
|
/**
|
|
250
252
|
* The mux a handler's address is carried over, made on first use.
|
|
@@ -276,6 +278,19 @@ function muxFor(address: string): Mux {
|
|
|
276
278
|
*/
|
|
277
279
|
const FOLLOWING = new Map<string, Following>()
|
|
278
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Forgets every mux and follow this page opened, their timers and sockets with them.
|
|
283
|
+
*
|
|
284
|
+
* A page never does; a test process outlives one, and a later file's sign-in (18.25) would reopen
|
|
285
|
+
* every mux an earlier file left standing.
|
|
286
|
+
*/
|
|
287
|
+
export function resetSocketWire(): void {
|
|
288
|
+
for (const following of FOLLOWING.values()) clearTimeout(following.expiry)
|
|
289
|
+
FOLLOWING.clear()
|
|
290
|
+
for (const mux of MUXES.values()) mux.dispose()
|
|
291
|
+
MUXES.clear()
|
|
292
|
+
}
|
|
293
|
+
|
|
279
294
|
/**
|
|
280
295
|
* Reads again every followed copy written here, once the outbox holds no call waiting.
|
|
281
296
|
*
|
package/src/ui/wire.ts
CHANGED
|
@@ -29,13 +29,13 @@ import { isFailed } from '#shared/Failed.ts'
|
|
|
29
29
|
import { answerChunks } from '#shared/framing.ts'
|
|
30
30
|
import { HandlerRoute } from '#shared/HandlerRoute.ts'
|
|
31
31
|
import type { JsonValue } from '#shared/JsonValue.ts'
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
34
|
-
import { reactive, standIn, structural } from '#shared/reactive/index.ts'
|
|
32
|
+
import { standForLoadCall, startingLoadCall } from '#shared/loadCall.ts'
|
|
33
|
+
import { reactive, structural } from '#shared/reactive/index.ts'
|
|
35
34
|
import {
|
|
36
35
|
declareDefaults,
|
|
37
36
|
type Defaults,
|
|
38
37
|
} from '#shared/reactive/parameterDefaults.ts'
|
|
38
|
+
import { standIn } from '#shared/reactive/producer.ts'
|
|
39
39
|
import { declare, type Method } from '#shared/rpc.ts'
|
|
40
40
|
import { type RpcRequest, rpcRequest } from '#shared/rpcRequest.ts'
|
|
41
41
|
|
|
@@ -72,11 +72,11 @@ export function wire(
|
|
|
72
72
|
// ONE PARAMETER, so the value is keyed by args whatever the handler declared (16.6's arity rule):
|
|
73
73
|
// an unkeyed handler is reached with no args, which is one key.
|
|
74
74
|
const body = (args: Record<string, unknown> | undefined) => {
|
|
75
|
-
// 16.49, 16.81 — the load's own signal and stand-in,
|
|
76
|
-
|
|
77
|
-
const signal =
|
|
75
|
+
// 16.49, 16.81 — the load's own signal and stand-in, as the call starting it carried them.
|
|
76
|
+
const starting = startingLoadCall()
|
|
77
|
+
const signal = starting.signal
|
|
78
78
|
const request = requestOf(args)
|
|
79
|
-
let offline =
|
|
79
|
+
let offline = starting.offline
|
|
80
80
|
// A reload no call started — a refresh, or the one `whenOnline` asks — that gets no answer
|
|
81
81
|
// waits before asking again, where asking at once would get none on every tick.
|
|
82
82
|
const retryIn = offline === undefined ? UNANSWERED_RETRY_MS : 0
|
|
@@ -93,8 +93,7 @@ export function wire(
|
|
|
93
93
|
? queuedCall(request, address, args)
|
|
94
94
|
: undefined
|
|
95
95
|
let standing: { value: unknown } | undefined
|
|
96
|
-
if (queued !== undefined)
|
|
97
|
-
LOAD_OFFLINE.standing = standing = { value: undefined }
|
|
96
|
+
if (queued !== undefined) standing = standForLoadCall()
|
|
98
97
|
const stoodIn = (failed: Answer): unknown => {
|
|
99
98
|
const value = (offline as (args: unknown) => unknown)(args)
|
|
100
99
|
if (queued === undefined) {
|
|
@@ -145,7 +144,7 @@ export function wire(
|
|
|
145
144
|
): AsyncGenerator<unknown> {
|
|
146
145
|
// 16.49 — read on the first line: the core's pump takes the first step inside the reach, so
|
|
147
146
|
// this runs while the call that started the load still has its signal set.
|
|
148
|
-
const signal =
|
|
147
|
+
const signal = startingLoadCall().signal
|
|
149
148
|
const request = requestOf(args)
|
|
150
149
|
const answer = await answerOf(request, true, signal)
|
|
151
150
|
if (answer.ok) answeredOnce.add(request.key)
|
package/src/compiler/Finding.ts
DELETED
package/src/compiler/Refusals.ts
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { resolve } from 'node:path' // bun exposes no path helpers of its own
|
|
2
|
-
|
|
3
|
-
import { formatDiagnosis } from './formatDiagnosis.ts'
|
|
4
|
-
import { diagnosisAt } from './lineOf.ts'
|
|
5
|
-
import { PositionedRefusal } from './PositionedRefusal.ts'
|
|
6
|
-
import type { Diagnosis } from './TypeSession.ts'
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Where everything the compile refuses goes, each at the file it was refused in.
|
|
10
|
-
*
|
|
11
|
-
* TWO MODES AND ONE CALL: a build stops at a refusal, so a throwing sink throws it; `abide
|
|
12
|
-
* check` reports every one at its file and goes on (38.33), so a collecting sink keeps each as a
|
|
13
|
-
* finding at the file's first line. Every stage takes this and nothing else, so no stage decides for
|
|
14
|
-
* itself what a refusal does, and none spells a path its own way: a file is named relative to `root`
|
|
15
|
-
* or absolutely, and kept absolute.
|
|
16
|
-
*/
|
|
17
|
-
export class Refusals {
|
|
18
|
-
/** Every refusal kept, in the order met; empty in a throwing sink. */
|
|
19
|
-
readonly found: Diagnosis[] = []
|
|
20
|
-
/** What a file below the root is named relative to. */
|
|
21
|
-
readonly root: string
|
|
22
|
-
/** Whether a refusal is kept and the compile goes on, rather than thrown. */
|
|
23
|
-
readonly collecting: boolean
|
|
24
|
-
|
|
25
|
-
constructor(root: string, collecting: boolean) {
|
|
26
|
-
this.root = root
|
|
27
|
-
this.collecting = collecting
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Refuses at `file`, below the root or absolute: `thrown` is thrown, or kept. Handed the file's
|
|
32
|
-
* `text`, a `PositionedRefusal` is placed on its line and column, and thrown as that line.
|
|
33
|
-
*/
|
|
34
|
-
at(file: string, thrown: unknown, text?: string): void {
|
|
35
|
-
const positioned =
|
|
36
|
-
text !== undefined && thrown instanceof PositionedRefusal
|
|
37
|
-
if (!this.collecting && !positioned) throw thrown
|
|
38
|
-
const diagnosis = diagnosisAt(
|
|
39
|
-
resolve(this.root, file),
|
|
40
|
-
positioned ? text : undefined,
|
|
41
|
-
positioned ? thrown.offset : 0,
|
|
42
|
-
'error',
|
|
43
|
-
thrown instanceof Error ? thrown.message : String(thrown),
|
|
44
|
-
)
|
|
45
|
-
if (!this.collecting)
|
|
46
|
-
throw new Error(formatDiagnosis(diagnosis, process.cwd()))
|
|
47
|
-
this.found.push(diagnosis)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* What another stage kept, as this one takes it: every one kept, or every one thrown as one
|
|
52
|
-
* error, each at its file — a build that stops names all it found rather than a first one and a
|
|
53
|
-
* second run (11.37).
|
|
54
|
-
*/
|
|
55
|
-
adopt(found: readonly Diagnosis[]): void {
|
|
56
|
-
if (found.length === 0) return
|
|
57
|
-
if (this.collecting) {
|
|
58
|
-
this.found.push(...found)
|
|
59
|
-
return
|
|
60
|
-
}
|
|
61
|
-
const lines: string[] = []
|
|
62
|
-
for (const one of found) lines.push(formatDiagnosis(one, process.cwd()))
|
|
63
|
-
throw new Error(lines.join('\n'))
|
|
64
|
-
}
|
|
65
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { relative } from 'node:path' // bun exposes no path helpers of its own
|
|
2
|
-
|
|
3
|
-
import type { Diagnosis } from './TypeSession.ts'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* One finding as one `tsc`-style line.
|
|
7
|
-
*
|
|
8
|
-
* ONE LINE A FINDING, in the shape `tsc` and every editor's problem matcher already read:
|
|
9
|
-
* `file:line:column - error TS2322: …`, the path relative to where the command was run. A finding
|
|
10
|
-
* with no line — a whole-file or whole-app refusal — names the file alone.
|
|
11
|
-
*/
|
|
12
|
-
export function formatDiagnosis(diagnosis: Diagnosis, from: string): string {
|
|
13
|
-
const path = relative(from, diagnosis.path) || diagnosis.path
|
|
14
|
-
const where =
|
|
15
|
-
diagnosis.line === 0
|
|
16
|
-
? path
|
|
17
|
-
: `${path}:${diagnosis.line}:${diagnosis.column}`
|
|
18
|
-
const code = diagnosis.code === 0 ? '' : ` TS${diagnosis.code}`
|
|
19
|
-
return `${where} - ${diagnosis.category}${code}: ${diagnosis.message}`
|
|
20
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The directories an app's own files stand in: `root`, and the one above it.
|
|
3
|
-
*
|
|
4
|
-
* `root` is the app's root — `src/` — and the one above it is the package beside
|
|
5
|
-
* it. The app's `package.json` and `bunfig.toml` are found in the first that has one.
|
|
6
|
-
*/
|
|
7
|
-
export function appDirectories(root: string): string[] {
|
|
8
|
-
return [root, `${root}/..`]
|
|
9
|
-
}
|