@abide/abide 0.54.0 → 0.56.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/AGENTS.md +330 -498
- package/CHANGELOG.md +63 -0
- package/README.md +166 -146
- package/package.json +2 -3
- package/src/lib/server/rpc/defineRpc.ts +2 -1
- package/src/lib/server/rpc/types/RpcHelper.ts +29 -22
- package/src/lib/server/runtime/SSR_SWAP_SCRIPT.ts +3 -2
- package/src/lib/server/runtime/createUiPageRenderer.ts +34 -26
- package/src/lib/shared/SEEDS.ts +33 -0
- package/src/lib/shared/SOCKET_SEED.ts +4 -5
- package/src/lib/shared/cache.ts +169 -75
- package/src/lib/shared/createRemoteFunction.ts +4 -1
- package/src/lib/shared/encodeSeedValue.ts +24 -0
- package/src/lib/shared/jsonSchemaForType.ts +3 -16
- package/src/lib/shared/peek.ts +3 -6
- package/src/lib/ui/amendReaderHook.ts +1 -1
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +1 -6
- package/src/lib/ui/compile/analyzeComponent.ts +0 -2
- package/src/lib/ui/compile/awaitSubjectExpr.ts +23 -0
- package/src/lib/ui/compile/cachedShadowProgram.ts +2 -1
- package/src/lib/ui/compile/compileComponent.ts +1 -10
- package/src/lib/ui/compile/compileShadow.ts +113 -8
- package/src/lib/ui/compile/componentOrdinals.ts +39 -0
- package/src/lib/ui/compile/desugarSignals.ts +195 -44
- package/src/lib/ui/compile/generateBuild.ts +25 -24
- package/src/lib/ui/compile/generateSSR.ts +37 -24
- package/src/lib/ui/compile/hoistableAwaits.ts +1 -11
- package/src/lib/ui/compile/isSpuriousAsyncReadDiagnostic.ts +46 -5
- package/src/lib/ui/compile/liftScriptSeed.ts +47 -0
- package/src/lib/ui/compile/lowerContext.ts +26 -6
- package/src/lib/ui/compile/lowerScript.ts +1 -11
- package/src/lib/ui/compile/renameSignalRefs.ts +7 -21
- package/src/lib/ui/compile/seedTypeClassifierForRoot.ts +1 -1
- package/src/lib/ui/compile/shadowInterpolationClassifier.ts +1 -1
- package/src/lib/ui/compile/types/AnalyzedComponent.ts +3 -5
- package/src/lib/ui/compile/wrapReactiveSeeds.ts +44 -0
- package/src/lib/ui/compile/wrapSeed.ts +28 -0
- package/src/lib/ui/computed.ts +16 -0
- package/src/lib/ui/createScope.ts +4 -3
- package/src/lib/ui/dom/anchorCursor.ts +2 -1
- package/src/lib/ui/dom/anchoredBranch.ts +14 -3
- package/src/lib/ui/dom/appendStatic.ts +11 -20
- package/src/lib/ui/dom/appendText.ts +41 -43
- package/src/lib/ui/dom/appendTextAt.ts +10 -57
- package/src/lib/ui/dom/awaitBlock.ts +30 -44
- package/src/lib/ui/dom/awaitSubject.ts +118 -0
- package/src/lib/ui/dom/claimText.ts +38 -0
- package/src/lib/ui/dom/cloneStatic.ts +2 -8
- package/src/lib/ui/dom/discardAndRebuild.ts +23 -0
- package/src/lib/ui/dom/discardBoundary.ts +5 -4
- package/src/lib/ui/dom/each.ts +3 -5
- package/src/lib/ui/dom/fillBoundary.ts +2 -1
- package/src/lib/ui/dom/hydrate.ts +5 -12
- package/src/lib/ui/dom/mountChild.ts +73 -5
- package/src/lib/ui/dom/mountSwappableRange.ts +2 -1
- package/src/lib/ui/dom/openMarker.ts +2 -4
- package/src/lib/ui/dom/outlet.ts +4 -3
- package/src/lib/ui/dom/readCell.ts +26 -11
- package/src/lib/ui/dom/skeleton.ts +2 -7
- package/src/lib/ui/dom/switchBlock.ts +2 -1
- package/src/lib/ui/dom/tryBlock.ts +26 -38
- package/src/lib/ui/dom/when.ts +6 -2
- package/src/lib/ui/linked.ts +32 -1
- package/src/lib/ui/resumeSeedScript.ts +1 -1
- package/src/lib/ui/router.ts +12 -24
- package/src/lib/ui/runtime/AsyncCellError.ts +2 -1
- package/src/lib/ui/runtime/BRANCH_SEGMENT.ts +24 -0
- package/src/lib/ui/runtime/CELL_SEED.ts +4 -6
- package/src/lib/ui/runtime/DOC_SEED.ts +5 -6
- package/src/lib/ui/runtime/RENDER.ts +7 -5
- package/src/lib/ui/runtime/RESUME.ts +5 -7
- package/src/lib/ui/runtime/SEED_MARKS.ts +14 -0
- package/src/lib/ui/runtime/SuspenseSignal.ts +2 -2
- package/src/lib/ui/runtime/claimChild.ts +5 -7
- package/src/lib/ui/runtime/claimExpected.ts +2 -6
- package/src/lib/ui/runtime/claimMarker.ts +13 -0
- package/src/lib/ui/runtime/claimRun.ts +22 -0
- package/src/lib/ui/runtime/consumeSeed.ts +38 -0
- package/src/lib/ui/runtime/createAsyncCell.ts +66 -8
- package/src/lib/ui/runtime/createComputedNode.ts +1 -0
- package/src/lib/ui/runtime/createEffectNode.ts +1 -0
- package/src/lib/ui/runtime/createSignalNode.ts +1 -0
- package/src/lib/ui/runtime/parkCursor.ts +10 -0
- package/src/lib/ui/runtime/readNode.ts +21 -1
- package/src/lib/ui/runtime/renderPath.ts +2 -2
- package/src/lib/ui/runtime/routeBareSeed.ts +28 -0
- package/src/lib/ui/runtime/runHydrationPass.ts +69 -0
- package/src/lib/ui/runtime/runNode.ts +14 -0
- package/src/lib/ui/runtime/types/HydrationCursor.ts +8 -0
- package/src/lib/ui/runtime/types/ReactiveNode.ts +5 -0
- package/src/lib/ui/settleAsyncCells.ts +30 -3
- package/src/lib/ui/startClient.ts +0 -7
- package/src/lib/ui/tryEncodeResume.ts +6 -15
- package/src/lib/ui/watch.ts +3 -11
- package/src/lib/ui/dom/mountStreamedChild.ts +0 -84
- package/src/lib/ui/dom/readCellBlocking.ts +0 -29
- package/src/lib/ui/runtime/restoreWarmSeeds.ts +0 -20
- package/src/lib/ui/runtime/warmSeedBackup.ts +0 -10
|
@@ -1,35 +1,50 @@
|
|
|
1
1
|
import { isAsyncCell } from '../../shared/isAsyncCell.ts'
|
|
2
2
|
import type { AsyncComputed } from '../../shared/types/AsyncComputed.ts'
|
|
3
3
|
import { AsyncCellError } from '../runtime/AsyncCellError.ts'
|
|
4
|
+
import { SuspenseSignal } from '../runtime/SuspenseSignal.ts'
|
|
4
5
|
|
|
5
6
|
/*
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
reader (a function) is called, and a sync
|
|
9
|
-
shape lets a `linked`/`computed` binding
|
|
10
|
-
settling promise, a stream, or a plain value —
|
|
7
|
+
The one read for a `computed`/`linked` reference the compiler lowers to `$$readCell(NAME)`,
|
|
8
|
+
everywhere — script and template, both sides. An async cell (`AsyncComputed`/`AsyncState`)
|
|
9
|
+
yields its latest retained value, a `derive` reader (a function) is called, and a sync
|
|
10
|
+
`Computed`/`State` yields its `.value`. One read shape lets a `linked`/`computed` binding
|
|
11
|
+
auto-track whichever source it resolved to — a settling promise, a stream, or a plain value —
|
|
12
|
+
with no read-site branching in codegen.
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
Whether a read PAUSES is a property of the NODE, not the read site (ADR-0042). A pending
|
|
15
|
+
blocking cell (`cell.blocking` — author `await`, the one that joins the SSR barrier) throws
|
|
16
|
+
a `SuspenseSignal`: the reader's branch of the graph pauses until the value resolves, and
|
|
17
|
+
the throwing read subscribed the reader to the cell so it re-runs on settle. A pending
|
|
18
|
+
STREAMING cell peeks `undefined` (composes with `?.`/`??`). So a rendering region withholds,
|
|
19
|
+
a derive over it pauses too, and the SSR barrier awaits it — all off the same node bit,
|
|
20
|
+
with no per-site knowledge threaded through the compiler.
|
|
21
|
+
|
|
22
|
+
For a settled async cell this stays the value-aware **throwing peek** (ADR-0019 D3.2):
|
|
14
23
|
- error AND no retained value → throw `AsyncCellError` (carries the cell for keep-the-watch);
|
|
15
24
|
- error WITH a retained value → the value (a failed background refresh stays visible — SWR);
|
|
16
|
-
- pending (no value) → `undefined`;
|
|
17
25
|
- resolved → the value.
|
|
18
|
-
The
|
|
26
|
+
The throws are codegen behaviour only — `cell.peek()`/`cell.error()` in JS never throw; the
|
|
19
27
|
author handles an error locally via the probes, or lets it reach a `{#try}`.
|
|
20
28
|
*/
|
|
21
29
|
// @documentation plumbing
|
|
22
30
|
export function readCell(cell: unknown): unknown {
|
|
23
31
|
if (isAsyncCell(cell)) {
|
|
24
|
-
const asyncCell = cell as AsyncComputed<unknown>
|
|
32
|
+
const asyncCell = cell as AsyncComputed<unknown> & { blocking?: boolean }
|
|
25
33
|
const value = asyncCell.peek()
|
|
26
34
|
/* A retained value wins over an error (SWR) and over pending — return it. Only when
|
|
27
|
-
nothing is retained does an error
|
|
35
|
+
nothing is retained does the cell's state decide: an error is "nothing to render"
|
|
36
|
+
(throw to `{#try}`); a pending blocking cell PAUSES (throw suspense); a pending
|
|
37
|
+
streaming cell — or a blocking cell that legitimately resolved to `undefined` — is
|
|
38
|
+
the bare `undefined`. `pending()` (in flight, no value) gates the suspend so a
|
|
39
|
+
resolved-`undefined` blocking cell never suspends forever. */
|
|
28
40
|
if (value === undefined) {
|
|
29
41
|
const error = asyncCell.error()
|
|
30
42
|
if (error !== undefined) {
|
|
31
43
|
throw new AsyncCellError(asyncCell, error)
|
|
32
44
|
}
|
|
45
|
+
if (asyncCell.blocking === true && asyncCell.pending()) {
|
|
46
|
+
throw new SuspenseSignal(asyncCell)
|
|
47
|
+
}
|
|
33
48
|
}
|
|
34
49
|
return value
|
|
35
50
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { walkAnchorOrder } from '../compile/walkAnchorOrder.ts'
|
|
2
2
|
import { walkElementOrder } from '../compile/walkElementOrder.ts'
|
|
3
|
-
import {
|
|
3
|
+
import { claimRun } from '../runtime/claimRun.ts'
|
|
4
4
|
import { HOLE_ATTRIBUTE } from '../runtime/HOLE_ATTRIBUTE.ts'
|
|
5
5
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
6
6
|
import { commentData } from './commentData.ts'
|
|
@@ -121,12 +121,7 @@ export function skeleton(parent: Node, html: string): SkeletonHoles {
|
|
|
121
121
|
const hydration = RENDER.hydration
|
|
122
122
|
const topLevel: Node[] = []
|
|
123
123
|
if (hydration !== undefined) {
|
|
124
|
-
|
|
125
|
-
for (let count = 0; count < topLevelCount && node !== null; count += 1) {
|
|
126
|
-
topLevel.push(node)
|
|
127
|
-
node = node.nextSibling
|
|
128
|
-
}
|
|
129
|
-
hydration.next.set(parent, node)
|
|
124
|
+
claimRun(hydration, parent, topLevelCount, topLevel)
|
|
130
125
|
} else {
|
|
131
126
|
const children = source.childNodes
|
|
132
127
|
/* Stage clones in a fragment ONLY for a live (connected) parent, where one
|
|
@@ -11,7 +11,8 @@ to the default (`match` undefined); the chosen case's content lives in a RANGE
|
|
|
11
11
|
bounded by two comment markers, so a case holds any content. Staying on the same
|
|
12
12
|
case across a subject change leaves it mounted; switching clears the range and
|
|
13
13
|
builds the new case fresh. See `mountSwappableRange` for the shared
|
|
14
|
-
hydrate/swap/teardown semantics — the case index is the swap key
|
|
14
|
+
hydrate/swap/teardown semantics — the case index is the swap key, which doubles as
|
|
15
|
+
the case's render-path segment (`caseSegment` — the alphabet SSR also emits).
|
|
15
16
|
*/
|
|
16
17
|
// @documentation plumbing
|
|
17
18
|
export function switchBlock(
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import { effect } from '../effect.ts'
|
|
2
2
|
import { AsyncCellError } from '../runtime/AsyncCellError.ts'
|
|
3
3
|
import { CURRENT_BOUNDARY } from '../runtime/CURRENT_BOUNDARY.ts'
|
|
4
|
-
import {
|
|
4
|
+
import { claimMarker } from '../runtime/claimMarker.ts'
|
|
5
5
|
import { OWNER } from '../runtime/OWNER.ts'
|
|
6
6
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
7
7
|
import { SuspenseSignal } from '../runtime/SuspenseSignal.ts'
|
|
8
8
|
import { scopeGroup } from '../runtime/scopeGroup.ts'
|
|
9
9
|
import type { Boundary } from '../runtime/types/Boundary.ts'
|
|
10
10
|
import type { State } from '../runtime/types/State.ts'
|
|
11
|
-
import { withoutHydration } from '../runtime/withoutHydration.ts'
|
|
12
11
|
import { state } from '../state.ts'
|
|
13
12
|
import { anchoredBranch } from './anchoredBranch.ts'
|
|
14
|
-
import {
|
|
13
|
+
import { discardAndRebuild } from './discardAndRebuild.ts'
|
|
15
14
|
|
|
16
15
|
/*
|
|
17
16
|
Reactive error boundary — the runtime for `<template try>` (ADR-0019 D3). Unlike the old
|
|
@@ -191,9 +190,7 @@ export function tryBlock(
|
|
|
191
190
|
firstHydrate()
|
|
192
191
|
return
|
|
193
192
|
}
|
|
194
|
-
|
|
195
|
-
branch.anchor = anchorNode
|
|
196
|
-
parent.insertBefore(anchorNode, before)
|
|
193
|
+
branch.parkAnchor(before)
|
|
197
194
|
showTry()
|
|
198
195
|
|
|
199
196
|
/* The first run when hydrating: claim the boundary's open marker, adopt the guarded
|
|
@@ -202,26 +199,25 @@ export function tryBlock(
|
|
|
202
199
|
throws too) discards the boundary and builds the catch fresh. */
|
|
203
200
|
function firstHydrate(): void {
|
|
204
201
|
const cursor = hydration as NonNullable<typeof hydration>
|
|
205
|
-
const open =
|
|
202
|
+
const open = claimMarker(cursor, parent, `abide:try:${id} open marker`)
|
|
206
203
|
try {
|
|
207
|
-
adopt(
|
|
204
|
+
adopt()
|
|
208
205
|
} catch (error) {
|
|
209
206
|
rebuildCold(open, error)
|
|
210
207
|
}
|
|
211
208
|
}
|
|
212
209
|
|
|
213
|
-
/* Adopt the guarded branch in place
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
`firstHydrate`
|
|
218
|
-
|
|
210
|
+
/* Adopt the guarded branch in place — the cursor already sits past the boundary open
|
|
211
|
+
(claiming it advanced): claim the server's `[` range-open marker, build the guarded
|
|
212
|
+
content (which claims its nodes) under the boundary, then claim the `]` range-close
|
|
213
|
+
and the boundary close, parking an anchor so a later swap detaches this range like
|
|
214
|
+
any other. A build throw disposes the partial scope and rethrows so `firstHydrate`
|
|
215
|
+
rebuilds cold. */
|
|
216
|
+
function adopt(): void {
|
|
219
217
|
const cursor = hydration as NonNullable<typeof hydration>
|
|
220
|
-
cursor.next.set(parent, open.nextSibling ?? null)
|
|
221
218
|
/* The server emits the `[ … ]` range inside the boundary — claim `[` as the live
|
|
222
|
-
range start,
|
|
223
|
-
const start =
|
|
224
|
-
cursor.next.set(parent, start.nextSibling ?? null)
|
|
219
|
+
range start, advancing the cursor to the first content node. */
|
|
220
|
+
const start = claimMarker(cursor, parent, `abide:try:${id} range-open marker`)
|
|
225
221
|
/* Install the boundary around the whole adopt (build + marker claims); the shared
|
|
226
222
|
strand-dispose guard in `adoptStrand` disposes the partial scope and rethrows on any
|
|
227
223
|
failure, and this `finally` restores the boundary before the rethrow reaches
|
|
@@ -234,17 +230,9 @@ export function tryBlock(
|
|
|
234
230
|
branch.adoptStrand(
|
|
235
231
|
(host) => renderTry(host),
|
|
236
232
|
() => {
|
|
237
|
-
const end =
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
`abide:try:${id} range-close marker`,
|
|
241
|
-
) as Comment
|
|
242
|
-
cursor.next.set(parent, end.nextSibling ?? null)
|
|
243
|
-
const close = claimExpected(cursor, parent, `/abide:try:${id} close marker`)
|
|
244
|
-
cursor.next.set(parent, close.nextSibling ?? null)
|
|
245
|
-
const anchorNode = document.createTextNode('')
|
|
246
|
-
parent.insertBefore(anchorNode, close)
|
|
247
|
-
branch.anchor = anchorNode
|
|
233
|
+
const end = claimMarker(cursor, parent, `abide:try:${id} range-close marker`)
|
|
234
|
+
const close = claimMarker(cursor, parent, `/abide:try:${id} close marker`)
|
|
235
|
+
branch.parkAnchor(close)
|
|
248
236
|
return { start, end }
|
|
249
237
|
},
|
|
250
238
|
)
|
|
@@ -258,18 +246,18 @@ export function tryBlock(
|
|
|
258
246
|
No catch → rethrow (the throw surfaces past the boundary, as the sync version did). */
|
|
259
247
|
function rebuildCold(open: Node, error: unknown): void {
|
|
260
248
|
branch.detach()
|
|
261
|
-
|
|
249
|
+
discardAndRebuild(
|
|
250
|
+
hydration as NonNullable<typeof hydration>,
|
|
262
251
|
parent,
|
|
263
252
|
open,
|
|
264
253
|
`/abide:try:${id}`,
|
|
265
|
-
|
|
254
|
+
(after) => {
|
|
255
|
+
if (!hasCatch) {
|
|
256
|
+
throw error
|
|
257
|
+
}
|
|
258
|
+
branch.parkAnchor(after)
|
|
259
|
+
showCatch(error)
|
|
260
|
+
},
|
|
266
261
|
)
|
|
267
|
-
if (!hasCatch) {
|
|
268
|
-
throw error
|
|
269
|
-
}
|
|
270
|
-
const anchorNode = document.createTextNode('')
|
|
271
|
-
branch.anchor = anchorNode
|
|
272
|
-
parent.insertBefore(anchorNode, after)
|
|
273
|
-
withoutHydration(() => showCatch(error))
|
|
274
262
|
}
|
|
275
263
|
}
|
package/src/lib/ui/dom/when.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ELSE_SEGMENT, THEN_SEGMENT } from '../runtime/BRANCH_SEGMENT.ts'
|
|
1
2
|
import { SuspenseSignal } from '../runtime/SuspenseSignal.ts'
|
|
2
3
|
import { mountSwappableRange } from './mountSwappableRange.ts'
|
|
3
4
|
|
|
@@ -37,7 +38,9 @@ export function when(
|
|
|
37
38
|
block (render neither branch) until it settles, exactly as a bare async subject holds;
|
|
38
39
|
the read tracked its cell, so the swap effect re-runs on resolve. */
|
|
39
40
|
try {
|
|
40
|
-
|
|
41
|
+
/* The branch key doubles as the branch's render-path segment (see
|
|
42
|
+
BRANCH_SEGMENT), so it is sourced from the shared alphabet SSR also emits. */
|
|
43
|
+
return condition() ? THEN_SEGMENT : ELSE_SEGMENT
|
|
41
44
|
} catch (signal) {
|
|
42
45
|
if (!(signal instanceof SuspenseSignal)) {
|
|
43
46
|
throw signal
|
|
@@ -45,7 +48,8 @@ export function when(
|
|
|
45
48
|
return 'pending'
|
|
46
49
|
}
|
|
47
50
|
},
|
|
48
|
-
(branch) =>
|
|
51
|
+
(branch) =>
|
|
52
|
+
branch === THEN_SEGMENT ? render : branch === ELSE_SEGMENT ? renderElse : undefined,
|
|
49
53
|
before,
|
|
50
54
|
/* An async subject (compiler supplies `isPending`) can render a different branch on the
|
|
51
55
|
server than the pending client, so hydration must not adopt the SSR range in place. */
|
package/src/lib/ui/linked.ts
CHANGED
|
@@ -4,6 +4,8 @@ import type { NamedAsyncIterable } from '../shared/types/NamedAsyncIterable.ts'
|
|
|
4
4
|
import { createAsyncCell } from './runtime/createAsyncCell.ts'
|
|
5
5
|
import { createEffectNode } from './runtime/createEffectNode.ts'
|
|
6
6
|
import { isAsyncFunction } from './runtime/isAsyncFunction.ts'
|
|
7
|
+
import { routeBareSeed } from './runtime/routeBareSeed.ts'
|
|
8
|
+
import { SuspenseSignal } from './runtime/SuspenseSignal.ts'
|
|
7
9
|
import type { State } from './runtime/types/State.ts'
|
|
8
10
|
import { state } from './state.ts'
|
|
9
11
|
|
|
@@ -41,6 +43,19 @@ export function linked<T>(
|
|
|
41
43
|
transform?: (next: T, previous: T) => T,
|
|
42
44
|
): State<T> | AsyncState<T> {
|
|
43
45
|
const coerce = transform as ((next: unknown, previous: unknown) => unknown) | undefined
|
|
46
|
+
/* A BARE seed that reached the runtime literally (a branch-nested `<script>`, a direct
|
|
47
|
+
JS caller) — the compile-time `wrapSeed` normalization only covers the leading script,
|
|
48
|
+
so honor the authored contract (ADR-0045) by VALUE via the shared `routeBareSeed`,
|
|
49
|
+
exactly as `computed` does; a non-async value becomes a constant seed thunk (reseeds
|
|
50
|
+
never fire — nothing tracked — matching the wrapped `() => value` form). */
|
|
51
|
+
if (typeof seed !== 'function') {
|
|
52
|
+
const value: unknown = seed
|
|
53
|
+
const cell = routeBareSeed(value, true, coerce)
|
|
54
|
+
if (cell !== undefined) {
|
|
55
|
+
return cell as AsyncState<T>
|
|
56
|
+
}
|
|
57
|
+
seed = () => value as T
|
|
58
|
+
}
|
|
44
59
|
/* `await` marker: an async-function seed unwraps its promise into a writable async cell. */
|
|
45
60
|
if (isAsyncFunction(seed)) {
|
|
46
61
|
return createAsyncCell(seed as () => unknown, {
|
|
@@ -53,10 +68,26 @@ export function linked<T>(
|
|
|
53
68
|
non-stream value falls through to the plain synchronous cell. */
|
|
54
69
|
let probe: unknown
|
|
55
70
|
let threw = false
|
|
71
|
+
let suspended = false
|
|
56
72
|
try {
|
|
57
73
|
probe = seed()
|
|
58
|
-
} catch {
|
|
74
|
+
} catch (error) {
|
|
59
75
|
threw = true
|
|
76
|
+
suspended = error instanceof SuspenseSignal
|
|
77
|
+
}
|
|
78
|
+
/* The seed SUSPENDED — it read a pending BLOCKING cell (`await`-marked). A sync `state`
|
|
79
|
+
here would hold `undefined` while the reseed effect re-threw the suspend, letting it
|
|
80
|
+
escape at construction. Route to the eager async cell instead: `createAsyncCell` already
|
|
81
|
+
treats a synchronously-suspending seed as "pending" — it stays in flight, the throwing
|
|
82
|
+
read subscribed its effect to the blocking dependency, and it re-runs on settle — and it
|
|
83
|
+
marks the cell BLOCKING so its own reads pause too, matching a lazy `computed(() =>
|
|
84
|
+
blockingCell)`. This makes a blocking read behave the same in a `linked` seed as in a
|
|
85
|
+
`computed` seed or a template. A real (non-suspense) throw keeps the sync fall-through. */
|
|
86
|
+
if (suspended) {
|
|
87
|
+
return createAsyncCell(seed as () => unknown, {
|
|
88
|
+
writable: true,
|
|
89
|
+
transform: coerce,
|
|
90
|
+
}) as AsyncState<T>
|
|
60
91
|
}
|
|
61
92
|
if (!threw && isAsyncIterable(probe)) {
|
|
62
93
|
return createAsyncCell(seed as () => unknown, {
|
|
@@ -23,5 +23,5 @@ export function resumeSeedScript(resume: Record<string, ResumeEntry>): string {
|
|
|
23
23
|
if (encoded.length === 0) {
|
|
24
24
|
return ''
|
|
25
25
|
}
|
|
26
|
-
return `<script>Object.assign(window.
|
|
26
|
+
return `<script>Object.assign((window.__abideSeeds=window.__abideSeeds||{}).resume=window.__abideSeeds.resume||{},${safeJsonForScript(Object.fromEntries(encoded))})</script>`
|
|
27
27
|
}
|
package/src/lib/ui/router.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { hydrationWindow } from '../shared/hydrationWindow.ts'
|
|
2
1
|
import { layoutChainForRoute } from '../shared/layoutChainForRoute.ts'
|
|
3
2
|
import { matchRoute } from '../shared/matchRoute.ts'
|
|
4
3
|
import { fillBoundary } from './dom/fillBoundary.ts'
|
|
@@ -7,12 +6,10 @@ import { effect } from './effect.ts'
|
|
|
7
6
|
import { navigatePath } from './navigate.ts'
|
|
8
7
|
import { CHILD_PRESENT } from './runtime/CHILD_PRESENT.ts'
|
|
9
8
|
import { clientPage } from './runtime/clientPage.ts'
|
|
10
|
-
import { enterRenderPass } from './runtime/enterRenderPass.ts'
|
|
11
|
-
import { exitRenderPass } from './runtime/exitRenderPass.ts'
|
|
12
9
|
import { historyEntries } from './runtime/historyEntries.ts'
|
|
13
10
|
import { PENDING_OUTLET } from './runtime/PENDING_OUTLET.ts'
|
|
14
11
|
import { RENDER } from './runtime/RENDER.ts'
|
|
15
|
-
import {
|
|
12
|
+
import { runHydrationPass } from './runtime/runHydrationPass.ts'
|
|
16
13
|
import { runtimePath } from './runtime/runtimePath.ts'
|
|
17
14
|
import type { AbideHistoryState } from './runtime/types/AbideHistoryState.ts'
|
|
18
15
|
import type { NavVerdict } from './runtime/types/NavVerdict.ts'
|
|
@@ -258,19 +255,10 @@ export function router(
|
|
|
258
255
|
mountedPageKey = pageKey
|
|
259
256
|
}
|
|
260
257
|
if (hydrating) {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
try {
|
|
266
|
-
run()
|
|
267
|
-
} finally {
|
|
268
|
-
exitRenderPass()
|
|
269
|
-
RENDER.hydration = previous
|
|
270
|
-
/* Outermost exit clears the window and wakes the peeks this pass withheld
|
|
271
|
-
for SSR congruence, now that the warm value is congruent to show. */
|
|
272
|
-
hydrationWindow.exit()
|
|
273
|
-
}
|
|
258
|
+
/* The pass lifecycle (claim cursor, withhold window, render pass, and the
|
|
259
|
+
warm-seed restore a throwing pass owes its cold rebuild) is owned by
|
|
260
|
+
`runHydrationPass` — the same bracket `hydrate` runs. */
|
|
261
|
+
runHydrationPass(run)
|
|
274
262
|
return
|
|
275
263
|
}
|
|
276
264
|
run()
|
|
@@ -567,13 +555,13 @@ export function router(
|
|
|
567
555
|
disposeFrom(0)
|
|
568
556
|
rootBoundary = undefined
|
|
569
557
|
mountedPageKey = undefined
|
|
570
|
-
/*
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
558
|
+
/* The cell/doc warm seeds the failed pass adopted are still in place —
|
|
559
|
+
a pass only MARKS them, deleting on a clean exit (`consumeSeed`,
|
|
560
|
+
ADR-0048) — so this cold rebuild re-adopts the SSR-resolved values
|
|
561
|
+
instead of refetching (a cold refetch would leave blocking `await`
|
|
562
|
+
cells pending, and a top-level `$$readCell` of one sits in no suspense
|
|
563
|
+
region — its SuspenseSignal would escape this rebuild and kill the
|
|
564
|
+
mount). */
|
|
577
565
|
buildFrom(0, chainKeys, layoutViews, pageView, key, params, false)
|
|
578
566
|
}
|
|
579
567
|
/* Reapply the destination entry's scroll once its DOM exists — a
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { messageFromError } from '../../shared/messageFromError.ts'
|
|
1
2
|
import type { AsyncComputed } from '../../shared/types/AsyncComputed.ts'
|
|
2
3
|
|
|
3
4
|
/*
|
|
@@ -13,7 +14,7 @@ export class AsyncCellError extends Error {
|
|
|
13
14
|
readonly cell: AsyncComputed<unknown>
|
|
14
15
|
|
|
15
16
|
constructor(cell: AsyncComputed<unknown>, cause: unknown) {
|
|
16
|
-
super(
|
|
17
|
+
super(messageFromError(cause), { cause })
|
|
17
18
|
this.name = 'AsyncCellError'
|
|
18
19
|
this.cell = cell
|
|
19
20
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/*
|
|
2
|
+
The render-path segment alphabet for control-flow branches — the one place the segment a
|
|
3
|
+
branch's content composes under is decided, read by BOTH sides (the same pattern
|
|
4
|
+
`RANGE_MARKER` uses for the wire markers, so a literal can never drift):
|
|
5
|
+
|
|
6
|
+
- the client runtimes (`when` returns THEN/ELSE as its swap key, `switchBlock` its case
|
|
7
|
+
index) hand the key to `mountSwappableRange`, which pushes `String(key)` as the branch's
|
|
8
|
+
path segment;
|
|
9
|
+
- the SSR back-end (`generateSSR`) emits the same segment around a branch's content
|
|
10
|
+
(`withPathBranch`), so a component/cell inside composes a byte-identical
|
|
11
|
+
serialization-stable id — the warm-seed key (ADR-0033).
|
|
12
|
+
|
|
13
|
+
A `when` keys its branches by name; a `switchBlock` (a `{#switch}`, or an `{#if}` chain with
|
|
14
|
+
any `{:elseif}`) keys each case by its index in the `[then, ...branches]` source order —
|
|
15
|
+
the default at its own position.
|
|
16
|
+
*/
|
|
17
|
+
export const THEN_SEGMENT = 'then'
|
|
18
|
+
export const ELSE_SEGMENT = 'else'
|
|
19
|
+
|
|
20
|
+
/* A switchBlock case's segment: its source-order index, stringified exactly as
|
|
21
|
+
`mountSwappableRange` stringifies the numeric swap key. */
|
|
22
|
+
export function caseSegment(index: number): string {
|
|
23
|
+
return String(index)
|
|
24
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { SEEDS } from '../../shared/SEEDS.ts'
|
|
2
|
+
|
|
1
3
|
/* The async-cell warm-seed manifest: an SSR-resolved cell value, keyed by the cell's
|
|
2
4
|
serialization-stable render-path id (`${scope.id}:${index}`), as a ref-json-encoded STRING
|
|
3
5
|
(decoded lazily at the read site in `createAsyncCell`, so an in-process graph with cycles /
|
|
@@ -5,10 +7,6 @@
|
|
|
5
7
|
`startClient` drains `__SSR__.cells` into here before mount; a hydrating cell reads its key to
|
|
6
8
|
seed its value warm instead of re-running the seed (no refetch, no pending flash).
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
store — whoever runs first creates it, the other adopts the same reference (mirrors RESUME). */
|
|
10
|
-
const globalScope = globalThis as { __abideCells?: Record<string, string> }
|
|
11
|
-
globalScope.__abideCells ??= {}
|
|
12
|
-
|
|
10
|
+
The `cells` partition of the one `__abideSeeds` manifest (ADR-0048, see SEEDS). */
|
|
13
11
|
// @documentation plumbing
|
|
14
|
-
export const CELL_SEED: Record<string, string> =
|
|
12
|
+
export const CELL_SEED: Record<string, string> = SEEDS.cells
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
+
import { SEEDS } from '../../shared/SEEDS.ts'
|
|
2
|
+
|
|
1
3
|
/* The doc-state warm-seed manifest: an SSR-captured reactive-document snapshot, keyed by its
|
|
2
4
|
scope's serialization-stable render-path id, as a ref-json-encoded STRING (decoded at the read
|
|
3
5
|
site in `createScope`). `startClient` drains `__SSR__.docs` into here before mount; a hydrating
|
|
4
6
|
scope reads its key to seed a plain `state(initial)` to the SERVER value instead of the fresh
|
|
5
|
-
init — a uuid/timestamp/random otherwise diverges from the SSR HTML.
|
|
6
|
-
`globalThis.__abideDocs` so an inline pre-bundle script and the framework share one store —
|
|
7
|
-
whoever runs first creates it, the other adopts the same reference (mirrors CELL_SEED). */
|
|
8
|
-
const globalScope = globalThis as { __abideDocs?: Record<string, string> }
|
|
9
|
-
globalScope.__abideDocs ??= {}
|
|
7
|
+
init — a uuid/timestamp/random otherwise diverges from the SSR HTML.
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+
The `docs` partition of the one `__abideSeeds` manifest (ADR-0048, see SEEDS). */
|
|
10
|
+
export const DOC_SEED: Record<string, string> = SEEDS.docs
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import type { HydrationCursor } from './types/HydrationCursor.ts'
|
|
2
|
+
|
|
1
3
|
/*
|
|
2
4
|
Render mode shared by the dom helpers. In the default (create) mode `hydration`
|
|
3
|
-
is undefined and helpers build fresh nodes. During `hydrate` it holds
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
is undefined and helpers build fresh nodes. During `hydrate` it holds the pass's
|
|
6
|
+
claim cursor (see `HydrationCursor`): helpers claim in build order, which matches
|
|
7
|
+
the SSR order, advancing through the claim verbs (`claimMarker`/`claimRun`/
|
|
8
|
+
`claimText`) or parking explicitly (`parkCursor`).
|
|
7
9
|
|
|
8
10
|
`blockCounters`/`depth` drive the render-pass block-id counters: every `await`/`try`
|
|
9
11
|
block draws a path-namespaced id via `nextBlockId` (ADR-0037) — `blockCounters` maps
|
|
@@ -21,7 +23,7 @@ elements built there get the right namespace — the fragment itself carries non
|
|
|
21
23
|
undefined outside foreign content. See `enterNamespace`/`effectiveChildNamespace`.
|
|
22
24
|
*/
|
|
23
25
|
export const RENDER: {
|
|
24
|
-
hydration:
|
|
26
|
+
hydration: HydrationCursor | undefined
|
|
25
27
|
blockCounters: Map<string, number>
|
|
26
28
|
depth: number
|
|
27
29
|
namespace: string | undefined
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { SEEDS } from '../../shared/SEEDS.ts'
|
|
2
|
+
|
|
1
3
|
/* The await-resume manifest: the resolved value (or error) of each streamed
|
|
2
4
|
`await` block, keyed by its boundary id. The SSR stream serializes each entry
|
|
3
5
|
alongside its fragment; the client registers it (via the inline swap script),
|
|
@@ -11,13 +13,9 @@
|
|
|
11
13
|
stream-swap script — vanilla, running before the bundle and the codec load — able
|
|
12
14
|
to register an entry without the decoder.
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
same reference. */
|
|
16
|
+
The `resume` partition of the one `__abideSeeds` manifest (ADR-0048, see SEEDS) —
|
|
17
|
+
the inline scripts and the framework share the same reference. */
|
|
17
18
|
export type ResumeEntry = { ok: true; value: unknown } | { ok: false; error: unknown }
|
|
18
19
|
|
|
19
|
-
const globalScope = globalThis as { __abideResume?: Record<string, string> }
|
|
20
|
-
globalScope.__abideResume ??= {}
|
|
21
|
-
|
|
22
20
|
// @documentation plumbing
|
|
23
|
-
export const RESUME: Record<string, string> =
|
|
21
|
+
export const RESUME: Record<string, string> = SEEDS.resume
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/* The active hydration pass's consumed-seed marks (ADR-0048 two-phase consume): each seed a
|
|
2
|
+
pass adopts is MARKED here rather than deleted, so a THROWING pass leaves every value in
|
|
3
|
+
place for the cold rebuild to re-adopt (no shadow backup), while the OUTERMOST clean pass
|
|
4
|
+
exit deletes the marked keys — preserving the one-shot contract (an SPA re-nav to the same
|
|
5
|
+
render-path re-inits fresh). Keyed per store so a mark is an O(1) Set add on the hydrate
|
|
6
|
+
hot path. Owned by `runHydrationPass`: save/restore for nesting, and a nested clean exit
|
|
7
|
+
hands its marks to the enclosing pass instead of committing — only the outermost owner
|
|
8
|
+
spends seeds, so an outer-pass throw can still hand every value to the recovery rebuild.
|
|
9
|
+
undefined outside a pass, where `consumeSeed` falls back to delete-on-read. Deliberately
|
|
10
|
+
NOT cleared by `withoutHydration` — a fresh build inside a live pass (a block's cold
|
|
11
|
+
rebuild) still marks and re-adopts. */
|
|
12
|
+
export const SEED_MARKS: {
|
|
13
|
+
current: Map<Record<string, string>, Set<string>> | undefined
|
|
14
|
+
} = { current: undefined }
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { AsyncComputed } from '../../shared/types/AsyncComputed.ts'
|
|
2
2
|
|
|
3
3
|
/*
|
|
4
|
-
The sentinel a
|
|
5
|
-
|
|
4
|
+
The sentinel `readCell` raises when a BLOCKING cell (`cell.blocking` — an `await`-marked cell)
|
|
5
|
+
has no value *yet* — "not resolved," not an error (ADR-0042 D3). It is a sibling
|
|
6
6
|
of `AsyncCellError` but a DISTINCT class so the two are told apart by `instanceof`: a suspend is
|
|
7
7
|
caught LOCALLY by the reading region — each DOM primitive (`appendText`, `attr`, `each`,
|
|
8
8
|
`spreadAttrs`, `watch`, and the `when`/`switchBlock` condition) swallows it and withholds to an
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { HydrationCursor } from './types/HydrationCursor.ts'
|
|
2
2
|
|
|
3
|
-
/* The next server-rendered node to claim under `parent` during hydration
|
|
4
|
-
defaulting to
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
parent: Node,
|
|
8
|
-
): Node | null {
|
|
3
|
+
/* The next server-rendered node to claim under `parent` during hydration — a PROBE
|
|
4
|
+
(the cursor doesn't move), defaulting to the first child when the pointer hasn't
|
|
5
|
+
been set yet. The claim verbs read through this and advance via `parkCursor`. */
|
|
6
|
+
export function claimChild(hydration: HydrationCursor, parent: Node): Node | null {
|
|
9
7
|
return hydration.next.has(parent) ? (hydration.next.get(parent) ?? null) : parent.firstChild
|
|
10
8
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CURRENT_PATH } from './CURRENT_PATH.ts'
|
|
2
2
|
import { claimChild } from './claimChild.ts'
|
|
3
|
-
import type { RENDER } from './RENDER.ts'
|
|
4
3
|
import { reportHydrationDivergence } from './reportHydrationDivergence.ts'
|
|
4
|
+
import type { HydrationCursor } from './types/HydrationCursor.ts'
|
|
5
5
|
|
|
6
6
|
/*
|
|
7
7
|
A hydration claim that MUST succeed. Like `claimChild`, but throws a structural error
|
|
@@ -13,11 +13,7 @@ invariant from a compile-time-only parity test into a runtime guard. Use only wh
|
|
|
13
13
|
compiled skeleton GUARANTEES a node — control-flow range markers, keyed-row boundaries —
|
|
14
14
|
never where an end-of-list null is legitimate (e.g. a trailing reactive text binding).
|
|
15
15
|
*/
|
|
16
|
-
export function claimExpected(
|
|
17
|
-
hydration: NonNullable<(typeof RENDER)['hydration']>,
|
|
18
|
-
parent: Node,
|
|
19
|
-
expected: string,
|
|
20
|
-
): Node {
|
|
16
|
+
export function claimExpected(hydration: HydrationCursor, parent: Node, expected: string): Node {
|
|
21
17
|
const node = claimChild(hydration, parent)
|
|
22
18
|
if (node === null) {
|
|
23
19
|
/* Name where first on the `hydrate` channel (return ignored — a missing structural node
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { claimExpected } from './claimExpected.ts'
|
|
2
|
+
import { parkCursor } from './parkCursor.ts'
|
|
3
|
+
import type { HydrationCursor } from './types/HydrationCursor.ts'
|
|
4
|
+
|
|
5
|
+
/* Claims a compiler-guaranteed marker node (a range bracket, a block/boundary comment,
|
|
6
|
+
a keyed-row bracket) at the parent's cursor and advances past it. Throws a legible
|
|
7
|
+
structural desync when no node is there (`claimExpected`) — a missing marker means
|
|
8
|
+
SSR markup and the client build disagree on structure at this position. */
|
|
9
|
+
export function claimMarker(hydration: HydrationCursor, parent: Node, expected: string): Comment {
|
|
10
|
+
const node = claimExpected(hydration, parent, expected)
|
|
11
|
+
parkCursor(hydration, parent, node.nextSibling)
|
|
12
|
+
return node as Comment
|
|
13
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { claimChild } from './claimChild.ts'
|
|
2
|
+
import { parkCursor } from './parkCursor.ts'
|
|
3
|
+
import type { HydrationCursor } from './types/HydrationCursor.ts'
|
|
4
|
+
|
|
5
|
+
/* Claims a run of `count` top-level server-rendered siblings at the parent's cursor and
|
|
6
|
+
parks it past them (a short run — the server DOM ending early — parks on null). The
|
|
7
|
+
static-clone paths advance through this: the markup is byte-identical to the server's,
|
|
8
|
+
so nothing inside needs claiming individually. `into` collects the claimed nodes when
|
|
9
|
+
the caller resolves holes against them (`skeleton`); omitted, the run is only skipped. */
|
|
10
|
+
export function claimRun(
|
|
11
|
+
hydration: HydrationCursor,
|
|
12
|
+
parent: Node,
|
|
13
|
+
count: number,
|
|
14
|
+
into?: Node[],
|
|
15
|
+
): void {
|
|
16
|
+
let node = claimChild(hydration, parent)
|
|
17
|
+
for (let index = 0; index < count && node !== null; index += 1) {
|
|
18
|
+
into?.push(node)
|
|
19
|
+
node = node.nextSibling
|
|
20
|
+
}
|
|
21
|
+
parkCursor(hydration, parent, node)
|
|
22
|
+
}
|