@abide/abide 0.123.3 → 0.124.1
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 +38 -0
- package/docs/REGISTRY.md +45 -12
- package/docs/RULEBOOK.md +29 -16
- package/docs/abide.md +999 -459
- package/package.json +1 -1
- package/src/compiler/ARRAY_MUTATORS.ts +20 -0
- package/src/compiler/LoweredFile.ts +609 -0
- package/src/compiler/Lowering.ts +46 -4
- 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 +71 -1715
- package/src/compiler/appLints.ts +109 -0
- package/src/compiler/checkedShadow.ts +1 -1
- package/src/compiler/classifyRead.ts +16 -1
- package/src/compiler/compileApp.ts +2 -2
- package/src/compiler/declaredExports.ts +44 -17
- package/src/compiler/diagnoseSources.ts +232 -0
- package/src/compiler/formatAbide.ts +5 -6
- package/src/compiler/handlersUnder.ts +16 -35
- package/src/compiler/projectShadow.ts +1 -0
- package/src/compiler/reactiveReads.ts +235 -0
- package/src/compiler/reactiveTypes.ts +13 -0
- package/src/compiler/readsIn.ts +180 -81
- package/src/compiler/routesDeclaration.ts +46 -0
- package/src/compiler/scaffold.ts +7 -2
- package/src/compiler/serveLanguage.ts +26 -20
- package/src/compiler/splitImports.ts +2 -2
- package/src/server/BRIDGE_VERSION.ts +1 -1
- 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/abideModules.ts +7 -7
- package/src/server/answerFailure.ts +3 -2
- package/src/server/answerRequest.ts +6 -70
- package/src/server/answerRpc.ts +48 -260
- package/src/server/answerSocket.ts +41 -463
- package/src/server/appKit.ts +1 -1
- package/src/server/argsContract.ts +1 -1
- package/src/server/createApp.ts +8 -2
- package/src/server/csp.ts +1 -1
- package/src/server/deferredBuilds.ts +3 -16
- package/src/server/faceOf.ts +1 -1
- package/src/server/generatedSurfaces.ts +2 -1
- package/src/server/handlerFileMount.ts +1 -1
- package/src/server/index.ts +1 -1
- 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/preload.ts +18 -0
- package/src/server/principal.ts +1 -1
- package/src/server/remote.ts +1 -1
- package/src/server/render.ts +31 -59
- package/src/server/requestScope.ts +5 -36
- package/src/server/runDesktop.ts +19 -447
- package/src/server/seedBuffer.ts +11 -11
- package/src/server/servedApp.ts +1 -1
- package/src/server/shell.ts +1 -1
- package/src/server/surfaceOf.ts +1 -1
- package/src/server/versionOf.ts +1 -1
- package/src/shared/RPC_SERVER.ts +27 -31
- package/src/shared/Schema.ts +1 -2
- 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/reactive/Keyed.ts +23 -15
- package/src/shared/reactive/Reactive.ts +46 -1002
- 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 -24
- package/src/shared/reactive/pairReactive.ts +5 -3
- package/src/shared/reactive/producer.ts +121 -0
- package/src/shared/reactive/scoped.ts +3 -2
- package/src/shared/reactive/selections.ts +209 -0
- package/src/shared/reactive/watch.ts +3 -4
- package/src/shared/rpc.ts +8 -5
- package/src/shared/validateJson.ts +1 -2
- package/src/shared/writePath.ts +29 -0
- package/src/ui/Router.ts +53 -69
- package/src/ui/outboxQueue.ts +2 -8
- package/src/ui/outcomeOf.ts +120 -0
- package/src/ui/socketWire.ts +3 -3
- package/src/ui/wire.ts +9 -10
- 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/ui/Router.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CURRENT_ROUTER, type NavigateOptions } from '#shared/CURRENT_ROUTER.ts'
|
|
2
2
|
import { decodedOrKept } from '#shared/decodedOrKept.ts'
|
|
3
3
|
import { ERROR_META } from '#shared/ERROR_META.ts'
|
|
4
|
-
import { firstMatch,
|
|
4
|
+
import { firstMatch, partsOf } from '#shared/matchRoute.ts'
|
|
5
5
|
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'
|
|
@@ -13,6 +13,12 @@ import { paintedCallsLanded } from '#shared/takeEarlyCall.ts'
|
|
|
13
13
|
import { LAYER_NAVIGATE } from './LAYER_NAMES.ts'
|
|
14
14
|
import { LevelChain, type PageModule } from './LevelChain.ts'
|
|
15
15
|
import { beginLayer, endLayer } from './measureLayer.ts'
|
|
16
|
+
import {
|
|
17
|
+
type Bearing,
|
|
18
|
+
type Found,
|
|
19
|
+
outcomeOf,
|
|
20
|
+
type Settled,
|
|
21
|
+
} from './outcomeOf.ts'
|
|
16
22
|
import { pageRegion, type Region } from './regions.ts'
|
|
17
23
|
|
|
18
24
|
export type Route = {
|
|
@@ -30,8 +36,6 @@ export type Route = {
|
|
|
30
36
|
|
|
31
37
|
type Compiled = Route & { segments: Segment[] }
|
|
32
38
|
|
|
33
|
-
type Found = { route: Compiled; params: Params }
|
|
34
|
-
|
|
35
39
|
/** A window's scroll, `[scrollX, scrollY]`. */
|
|
36
40
|
type Scroll = readonly [number, number]
|
|
37
41
|
|
|
@@ -151,7 +155,7 @@ export class Router {
|
|
|
151
155
|
* as the address below the app's root. Every way in — a click, an intent, back and forward, a
|
|
152
156
|
* hydration — is matched here.
|
|
153
157
|
*/
|
|
154
|
-
match(pathname: string): Found | undefined {
|
|
158
|
+
match(pathname: string): Found<Compiled> | undefined {
|
|
155
159
|
const below = unmounted(pathname)
|
|
156
160
|
const parts = below === undefined ? undefined : partsOf(below)
|
|
157
161
|
return parts === undefined ? undefined : firstMatch(this.routes, parts)
|
|
@@ -209,28 +213,17 @@ export class Router {
|
|
|
209
213
|
options: NavigateOptions,
|
|
210
214
|
step: Step,
|
|
211
215
|
): Promise<void> {
|
|
212
|
-
const
|
|
213
|
-
typeof url === 'string' ? new URL(url, location.href) : url
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
220
|
-
const found = this.match(target.pathname)
|
|
221
|
-
if (found === undefined) {
|
|
222
|
-
// NOT A ROUTE THIS BUILD KNOWS, so it is the server's to answer — which is a document
|
|
223
|
-
// load, and the one case where 30.7 does not apply because there is no route to run.
|
|
224
|
-
location.href = target.href
|
|
225
|
-
return
|
|
226
|
-
}
|
|
227
|
-
// 30.21, 30.23, 30.24 — THE ROUTE STANDING, AT ITS OWN PATH: another query, another fragment or
|
|
228
|
-
// the same address. The page reads its address through `route`, so it follows rather than
|
|
229
|
-
// being rebuilt, and there is no path for `middleware` to decide anything new about.
|
|
230
|
-
if (found.route === this.standing && this.mounted === target.pathname) {
|
|
231
|
-
this.follow(target, options, found, step)
|
|
232
|
-
return
|
|
216
|
+
const bearing: Bearing<Compiled> = {
|
|
217
|
+
target: typeof url === 'string' ? new URL(url, location.href) : url,
|
|
218
|
+
origin: location.origin,
|
|
219
|
+
errorPage: this.errorPage,
|
|
220
|
+
standing: this.standing,
|
|
221
|
+
mounted: this.mounted,
|
|
222
|
+
match: (pathname) => this.match(pathname),
|
|
233
223
|
}
|
|
224
|
+
const before = outcomeOf(bearing)
|
|
225
|
+
if (before.kind !== 'ask') return this.settle(before, options, step)
|
|
226
|
+
const found = before.found
|
|
234
227
|
const ticket = this.supersede()
|
|
235
228
|
route.navigating.set(true)
|
|
236
229
|
// EVERY WAY OUT CLEARS WHAT THE WAY IN SET: a load that fails, a setup that throws and a
|
|
@@ -239,70 +232,61 @@ export class Router {
|
|
|
239
232
|
try {
|
|
240
233
|
// 30.15 — THE SERVER IS ASKED ALONGSIDE THE LOAD, so its `middleware` decides the
|
|
241
234
|
// navigation for the price of the one round trip the chunks already cost.
|
|
235
|
+
// 30.27 — `loadOrMiss`, because a rejection here was dropped by the click's `void`,
|
|
236
|
+
// and the link did nothing.
|
|
242
237
|
const [answer, loaded] = await Promise.all([
|
|
243
|
-
ask(target, ticket.signal),
|
|
238
|
+
ask(bearing.target, ticket.signal),
|
|
244
239
|
loadOrMiss(found.route),
|
|
245
240
|
])
|
|
246
241
|
// SUPERSEDED, so this navigation is over. Nothing has been touched yet, which is the
|
|
247
242
|
// whole reason the ticket is checked here rather than after the mount — and
|
|
248
243
|
// `navigating` stays the newer navigation's to clear.
|
|
249
244
|
if (ticket.signal.aborted) return
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
) {
|
|
245
|
+
const after = outcomeOf(bearing, {
|
|
246
|
+
found,
|
|
247
|
+
answer,
|
|
248
|
+
loaded: loaded !== undefined,
|
|
249
|
+
online: navigator.onLine,
|
|
250
|
+
pushed: step.direction === 'push',
|
|
251
|
+
})
|
|
252
|
+
if (after.kind === 'offline') {
|
|
258
253
|
// 45.12 — UNLESS THE SERVICE WORKER SAVED THE PAGE, which the document load is
|
|
259
254
|
// then answered with.
|
|
260
|
-
if (
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
// document asks again and paints whatever the server says. A fetch that failed —
|
|
266
|
-
// offline, or a redirect to another origin that CORS will not let it follow — goes to
|
|
267
|
-
// the address it was sent to.
|
|
268
|
-
// 30.27 — AND CHUNKS THAT WOULD NOT LOAD, with the server down or a deploy that removed
|
|
269
|
-
// them: a rejection here was dropped by the click's `void`, and the link did nothing.
|
|
270
|
-
if (answer?.status !== 204 || loaded === undefined) {
|
|
271
|
-
location.href = answer?.url ?? target.href
|
|
272
|
-
return
|
|
273
|
-
}
|
|
274
|
-
// 30.17 — REDIRECTED, and the server already said yes to where it landed: the answer is
|
|
275
|
-
// the 204 at the end of the chain. That address is mounted in place of the one asked
|
|
276
|
-
// for, and a route the client has no row for is the document's (30.10).
|
|
277
|
-
const landed = answer.redirected ? new URL(answer.url) : target
|
|
278
|
-
const landing = answer.redirected
|
|
279
|
-
? this.match(landed.pathname)
|
|
280
|
-
: found
|
|
281
|
-
if (landing === undefined) {
|
|
282
|
-
location.href = landed.href
|
|
283
|
-
return
|
|
284
|
-
}
|
|
285
|
-
// 30.23 — THE ROUTE STANDING, AT ANOTHER PATH: `middleware` has said yes to the path, and
|
|
286
|
-
// the page it matched is the one already mounted, so it follows.
|
|
287
|
-
if (landing.route === this.standing) {
|
|
288
|
-
this.follow(landed, options, landing, step)
|
|
255
|
+
if (
|
|
256
|
+
(await savedOffline(bearing.target)) &&
|
|
257
|
+
!ticket.signal.aborted
|
|
258
|
+
)
|
|
259
|
+
location.href = after.href
|
|
289
260
|
return
|
|
290
261
|
}
|
|
262
|
+
if (after.kind !== 'mount') return this.settle(after, options, step)
|
|
291
263
|
const arriving =
|
|
292
|
-
|
|
264
|
+
after.found === found
|
|
265
|
+
? loaded
|
|
266
|
+
: await loadOrMiss(after.found.route)
|
|
293
267
|
if (ticket.signal.aborted) return
|
|
294
268
|
if (arriving === undefined) {
|
|
295
|
-
location.href =
|
|
269
|
+
location.href = after.at.href
|
|
296
270
|
return
|
|
297
271
|
}
|
|
298
272
|
await this.write(ticket, step.direction, () =>
|
|
299
|
-
this.commit(
|
|
273
|
+
this.commit(after.at, after.found, arriving, options, step),
|
|
300
274
|
)
|
|
301
275
|
} finally {
|
|
302
276
|
if (this.navigation === ticket) route.navigating.set(false)
|
|
303
277
|
}
|
|
304
278
|
}
|
|
305
279
|
|
|
280
|
+
/** Executes an outcome that loads nothing: a document load, or a step within the page. */
|
|
281
|
+
private settle(
|
|
282
|
+
outcome: Settled<Compiled>,
|
|
283
|
+
options: NavigateOptions,
|
|
284
|
+
step: Step,
|
|
285
|
+
): void {
|
|
286
|
+
if (outcome.kind === 'document') location.href = outcome.href
|
|
287
|
+
else this.follow(outcome.at, options, outcome.found, step)
|
|
288
|
+
}
|
|
289
|
+
|
|
306
290
|
/**
|
|
307
291
|
* Moves the address within the standing page, leaving the page mounted.
|
|
308
292
|
*
|
|
@@ -317,7 +301,7 @@ export class Router {
|
|
|
317
301
|
follow(
|
|
318
302
|
target: URL,
|
|
319
303
|
options: NavigateOptions,
|
|
320
|
-
found: Found
|
|
304
|
+
found: Found<Compiled>,
|
|
321
305
|
step: Step,
|
|
322
306
|
): void {
|
|
323
307
|
this.supersede()
|
|
@@ -447,7 +431,7 @@ export class Router {
|
|
|
447
431
|
*/
|
|
448
432
|
private commit(
|
|
449
433
|
target: URL,
|
|
450
|
-
found: Found
|
|
434
|
+
found: Found<Compiled>,
|
|
451
435
|
arriving: PageModule[],
|
|
452
436
|
options: NavigateOptions,
|
|
453
437
|
step: Step,
|
|
@@ -515,7 +499,7 @@ export class Router {
|
|
|
515
499
|
*
|
|
516
500
|
* 23.1 — WRITTEN BEFORE THE ARRIVING PAGE'S SETUP, which reads it.
|
|
517
501
|
*/
|
|
518
|
-
private arriveAt(target: URL, found: Found): void {
|
|
502
|
+
private arriveAt(target: URL, found: Found<Compiled>): void {
|
|
519
503
|
arrive(target.href, found.route.pattern, found.params)
|
|
520
504
|
this.standing = found.route
|
|
521
505
|
this.mounted = target.pathname
|
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'
|
|
@@ -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/socketWire.ts
CHANGED
|
@@ -8,15 +8,15 @@ 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
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 { patchEpoch, writtenSince } from '#shared/reactive/patchMarks.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'
|
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)
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The `offline` of the call that is starting a load, while it starts.
|
|
3
|
-
*
|
|
4
|
-
* 16.81 — SET AROUND THE REACH as `LOAD_SIGNAL` is, and read by the browser's wire as its body
|
|
5
|
-
* starts: what stands in for an answer the call could not get. A leaf, because `callLoad` sets it
|
|
6
|
-
* and the wire reads it.
|
|
7
|
-
*/
|
|
8
|
-
export const LOAD_OFFLINE: {
|
|
9
|
-
current: ((args: unknown) => unknown) | undefined
|
|
10
|
-
/**
|
|
11
|
-
* Where the body that queues the call asks for the value the call reached, which `callLoad`
|
|
12
|
-
* fills once the reach returns: what the delivered answer lands in (46.7).
|
|
13
|
-
*/
|
|
14
|
-
standing: { value: unknown } | undefined
|
|
15
|
-
} = { current: undefined, standing: undefined }
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The abort signal of the call that is starting a load, while it starts.
|
|
3
|
-
*
|
|
4
|
-
* 16.49 — THE SIGNAL OF THE CALL THAT IS STARTING A LOAD, set around the reach and read by a
|
|
5
|
-
* transport's body as it starts: the browser's wire hands it to `fetch`, so a load every caller
|
|
6
|
-
* aborted ends its request too. A leaf, because `callLoad` sets it and the wire reads it.
|
|
7
|
-
*/
|
|
8
|
-
export const LOAD_SIGNAL: { current: AbortSignal | undefined } = {
|
|
9
|
-
current: undefined,
|
|
10
|
-
}
|
package/src/shared/issuesOf.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Issues in the shape `Issues<T>` gives them for the value refused: by path for an object or an
|
|
3
|
-
* array, the bare list for anything else.
|
|
4
|
-
*
|
|
5
|
-
* DECIDED BY THE VALUE, because the value is all the runtime holds — `T` is erased. A scalar has
|
|
6
|
-
* no path to key by, so every issue about one stands on `''`, and handing back `{ '': [...] }`
|
|
7
|
-
* there is the wrapper `Issues<T>` says a reader never unwraps: `error().data[0]` read `undefined`.
|
|
8
|
-
*/
|
|
9
|
-
export function issuesOf(
|
|
10
|
-
found: Record<string, string[]>,
|
|
11
|
-
value: unknown,
|
|
12
|
-
): Record<string, string[]> | string[] {
|
|
13
|
-
if (typeof value === 'object' && value !== null) return found
|
|
14
|
-
const listed: string[] = []
|
|
15
|
-
for (const path in found)
|
|
16
|
-
for (const said of found[path] as string[]) listed.push(said)
|
|
17
|
-
return listed
|
|
18
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* When each entry of a `Keyed` declaring `ttl` last produced, and how long it lives, by entry cell.
|
|
3
|
-
*
|
|
4
|
-
* 11.45. A leaf because `Keyed.ts` writes it and `Reactive.ts` reads it (`deadlineOf`), and the
|
|
5
|
-
* second cannot import the first.
|
|
6
|
-
*/
|
|
7
|
-
export const ENTRY_LIVES = new WeakMap<object, { at: number; ms: number }>()
|