@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,7 +1,8 @@
|
|
|
1
1
|
import { decodeRefJson } from '../../shared/decodeRefJson.ts'
|
|
2
|
+
import { isThenable } from '../../shared/isThenable.ts'
|
|
2
3
|
import { effect } from '../effect.ts'
|
|
3
4
|
import { CURRENT_BOUNDARY } from '../runtime/CURRENT_BOUNDARY.ts'
|
|
4
|
-
import {
|
|
5
|
+
import { claimMarker } from '../runtime/claimMarker.ts'
|
|
5
6
|
import { generationGuard } from '../runtime/generationGuard.ts'
|
|
6
7
|
import { RANGE_CLOSE, RANGE_OPEN } from '../runtime/RANGE_MARKER.ts'
|
|
7
8
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
@@ -10,10 +11,9 @@ import { RESUME } from '../runtime/RESUME.ts'
|
|
|
10
11
|
import { SuspenseSignal } from '../runtime/SuspenseSignal.ts'
|
|
11
12
|
import { scopeGroup } from '../runtime/scopeGroup.ts'
|
|
12
13
|
import type { State } from '../runtime/types/State.ts'
|
|
13
|
-
import { withoutHydration } from '../runtime/withoutHydration.ts'
|
|
14
14
|
import { state } from '../state.ts'
|
|
15
15
|
import { anchoredBranch } from './anchoredBranch.ts'
|
|
16
|
-
import {
|
|
16
|
+
import { discardAndRebuild } from './discardAndRebuild.ts'
|
|
17
17
|
|
|
18
18
|
/*
|
|
19
19
|
Async binding — the runtime for `<template await>`. Renders the pending branch,
|
|
@@ -159,44 +159,42 @@ export function awaitBlock(
|
|
|
159
159
|
bracketing — it runs only after a clean build. */
|
|
160
160
|
const adopt = (open: Node | null, build: (parent: Node) => void): void => {
|
|
161
161
|
const cursor = hydration as NonNullable<typeof hydration>
|
|
162
|
+
/* The cursor already sits here (claiming `open` advanced past it); kept for the
|
|
163
|
+
bracketing below. */
|
|
162
164
|
const firstAdopted = open?.nextSibling ?? null
|
|
163
|
-
cursor.next.set(parent, firstAdopted)
|
|
164
165
|
branch.adoptStrand(build, () => {
|
|
165
|
-
/* A guaranteed control-flow marker —
|
|
166
|
+
/* A guaranteed control-flow marker — the claim throws on a desync (caught by
|
|
166
167
|
firstHydrate's adopt try/catch → rebuildCold) instead of silently claiming null
|
|
167
168
|
and over-clearing the parent. */
|
|
168
|
-
const close =
|
|
169
|
-
cursor.next.set(parent, close.nextSibling ?? null)
|
|
169
|
+
const close = claimMarker(cursor, parent, `/abide:await:${id} close marker`)
|
|
170
170
|
/* Bracket the adopted nodes: `[` before the first claimed node (or before `close`
|
|
171
171
|
for an empty branch), `]` then the anchor just before `close`. */
|
|
172
172
|
const start = document.createComment(RANGE_OPEN)
|
|
173
173
|
parent.insertBefore(start, firstAdopted ?? close)
|
|
174
174
|
const end = document.createComment(RANGE_CLOSE)
|
|
175
175
|
parent.insertBefore(end, close)
|
|
176
|
-
|
|
177
|
-
parent.insertBefore(anchorNode, close)
|
|
178
|
-
branch.anchor = anchorNode
|
|
176
|
+
branch.parkAnchor(close)
|
|
179
177
|
return { start, end }
|
|
180
178
|
})
|
|
181
179
|
}
|
|
182
180
|
|
|
183
|
-
/* Discard the SSR boundary and (re)build the block from
|
|
184
|
-
|
|
185
|
-
|
|
181
|
+
/* Discard the SSR boundary and (re)build the block from `result`, fresh (hydration
|
|
182
|
+
off) — the recovery path when adoption can't use the server markup. Insert the
|
|
183
|
+
anchor at the node AFTER the discarded boundary — NOT the captured `before`, which
|
|
184
|
+
for a skeleton-anchored block is the open boundary itself and is removed here, so
|
|
185
|
+
reusing it throws `NotFoundError` in a strict DOM. */
|
|
186
|
+
const rebuildCold = (open: Node | null, result: () => unknown): void => {
|
|
186
187
|
branch.detach()
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
removed here, so reusing it throws `NotFoundError` in a strict DOM. */
|
|
190
|
-
const after = discardBoundary(
|
|
188
|
+
discardAndRebuild(
|
|
189
|
+
hydration as NonNullable<typeof hydration>,
|
|
191
190
|
parent,
|
|
192
191
|
open,
|
|
193
192
|
`/abide:await:${id}`,
|
|
194
|
-
|
|
193
|
+
(after) => {
|
|
194
|
+
branch.parkAnchor(after)
|
|
195
|
+
render(result())
|
|
196
|
+
},
|
|
195
197
|
)
|
|
196
|
-
const anchorNode = document.createTextNode('')
|
|
197
|
-
branch.anchor = anchorNode
|
|
198
|
-
parent.insertBefore(anchorNode, after)
|
|
199
|
-
withoutHydration(() => render(promiseThunk()))
|
|
200
198
|
}
|
|
201
199
|
|
|
202
200
|
/* The first run when hydrating: adopt by precedence (resume / warm-sync), else
|
|
@@ -206,9 +204,9 @@ export function awaitBlock(
|
|
|
206
204
|
warm cache (or re-fetches) instead of crashing hydration. */
|
|
207
205
|
const firstHydrate = (): void => {
|
|
208
206
|
const cursor = hydration as NonNullable<typeof hydration>
|
|
209
|
-
/* The await block's open marker is compiler-guaranteed —
|
|
207
|
+
/* The await block's open marker is compiler-guaranteed — the claim throws a
|
|
210
208
|
legible desync here rather than propagating a null that over-clears the parent. */
|
|
211
|
-
const open =
|
|
209
|
+
const open = claimMarker(cursor, parent, `abide:await:${id} open marker`)
|
|
212
210
|
/* RESUME holds the ref-json-encoded entry STRING; decode here, where the codec
|
|
213
211
|
lives. A decode failure (malformed/absent payload) reads as "no resume" — fall
|
|
214
212
|
through to the live promise rather than crash hydration. */
|
|
@@ -233,7 +231,7 @@ export function awaitBlock(
|
|
|
233
231
|
would otherwise surface as an unhandledrejection at boot (process-fatal under
|
|
234
232
|
Bun) instead of being harmlessly dropped. */
|
|
235
233
|
if (isThenable(result)) {
|
|
236
|
-
void
|
|
234
|
+
void result.then(undefined, () => undefined)
|
|
237
235
|
}
|
|
238
236
|
/* Build the adopted branch around a value CELL (then) so a later re-run updates
|
|
239
237
|
it in place, exactly like a fresh mount. The `throw` for a catch-less rejection
|
|
@@ -260,7 +258,7 @@ export function awaitBlock(
|
|
|
260
258
|
valueCell = cell
|
|
261
259
|
activeKind = kind
|
|
262
260
|
} catch {
|
|
263
|
-
rebuildCold(open)
|
|
261
|
+
rebuildCold(open, promiseThunk)
|
|
264
262
|
}
|
|
265
263
|
return
|
|
266
264
|
}
|
|
@@ -271,19 +269,14 @@ export function awaitBlock(
|
|
|
271
269
|
valueCell = cell
|
|
272
270
|
activeKind = 'then'
|
|
273
271
|
} catch {
|
|
274
|
-
rebuildCold(open)
|
|
272
|
+
rebuildCold(open, promiseThunk)
|
|
275
273
|
}
|
|
276
274
|
return
|
|
277
275
|
}
|
|
278
|
-
/*
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
parent.insertBefore(anchorNode, after)
|
|
283
|
-
/* The boundary's server nodes are gone, so the pending branch builds FRESH — clear
|
|
284
|
-
the claim cursor (see withoutHydration) so its `cloneStatic`/text don't try to
|
|
285
|
-
claim discarded nodes and silently render nothing. */
|
|
286
|
-
withoutHydration(() => render(result))
|
|
276
|
+
/* Genuinely pending — no resume, not warm: the boundary's server nodes can't be
|
|
277
|
+
adopted, so discard them and build the pending branch fresh from the already-read
|
|
278
|
+
promise (nothing was placed yet; the detach inside is a no-op). */
|
|
279
|
+
rebuildCold(open, () => result)
|
|
287
280
|
}
|
|
288
281
|
|
|
289
282
|
effect(() => {
|
|
@@ -296,9 +289,7 @@ export function awaitBlock(
|
|
|
296
289
|
firstHydrate()
|
|
297
290
|
return
|
|
298
291
|
}
|
|
299
|
-
|
|
300
|
-
branch.anchor = anchorNode
|
|
301
|
-
parent.insertBefore(anchorNode, before)
|
|
292
|
+
branch.parkAnchor(before)
|
|
302
293
|
}
|
|
303
294
|
/* Read the promise every subsequent run so an invalidate re-runs the block. ONLY this
|
|
304
295
|
read is tracked (the branch builds untracked via `scope`), so the block re-runs only
|
|
@@ -321,8 +312,3 @@ export function awaitBlock(
|
|
|
321
312
|
render(result)
|
|
322
313
|
})
|
|
323
314
|
}
|
|
324
|
-
|
|
325
|
-
/* Whether a value is Promise-like (the cold path); a non-thenable is warm-sync. */
|
|
326
|
-
function isThenable(value: unknown): value is Promise<unknown> {
|
|
327
|
-
return value !== null && typeof (value as { then?: unknown })?.then === 'function'
|
|
328
|
-
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { isAsyncCell } from '../../shared/isAsyncCell.ts'
|
|
2
|
+
import type { AsyncComputed } from '../../shared/types/AsyncComputed.ts'
|
|
3
|
+
import { createEffectNode } from '../runtime/createEffectNode.ts'
|
|
4
|
+
import { SuspenseSignal } from '../runtime/SuspenseSignal.ts'
|
|
5
|
+
import { readCell } from './readCell.ts'
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
The subject of an `{#await X}` block, normalised so the block AWAITS an async cell's
|
|
9
|
+
resolution instead of peeking its `undefined`-while-pending value (ADR-0047). `{#await}`
|
|
10
|
+
means "wait", so when its subject is a `computed`/`linked` async cell the block should show
|
|
11
|
+
its pending branch until the cell settles, then `{:then}` with the resolved value — but a
|
|
12
|
+
cell reference otherwise lowers to `$$readCell(cell)`, a PEEK, which reads `undefined` while
|
|
13
|
+
pending and fires `{:then}` with `undefined` (the `{#if}`/`{#switch}` cell subjects avoid this
|
|
14
|
+
with a `cellPending` guard; `{#await}` had no equivalent).
|
|
15
|
+
|
|
16
|
+
The compiler wraps ONLY a bare cell-reference subject in this (a plain-promise subject like
|
|
17
|
+
`{#await getFoo()}` bypasses it), so `subject` is always some cell the `cellReadNames` set
|
|
18
|
+
named — of two runtime shapes:
|
|
19
|
+
|
|
20
|
+
- **an async cell** (`AsyncComputed`/`AsyncState` — a `computed`/`linked` whose seed unwraps a
|
|
21
|
+
promise/stream). Pending (in flight, no value) → a PROMISE resolving to the cell's value once
|
|
22
|
+
it settles (rejecting with its error); the `pending()` read subscribes the reading block, so a
|
|
23
|
+
client reseed/`cache.invalidate` re-runs it and the SSR `renderToStream` awaits the returned
|
|
24
|
+
promise. Settled (a value, or a held value being refreshed — SWR) → the value now, so `{:then}`
|
|
25
|
+
shows it with no pending flash; an error with no retained value throws (→ `{:catch}`/`{#try}`).
|
|
26
|
+
- **a lazy `Computed`/`State`/derive** holding a bare promise (`computed(getFoo())` with no
|
|
27
|
+
`await` stays an opaque promise-holder, not an async cell) → read through `readCell` to its
|
|
28
|
+
`.value`, i.e. the promise itself, which `awaitBlock`/`renderToStream` then awaits — exactly the
|
|
29
|
+
pre-existing `{#await computedPromise}` behaviour, preserved.
|
|
30
|
+
*/
|
|
31
|
+
// @documentation plumbing
|
|
32
|
+
export function awaitSubject(subject: unknown): unknown {
|
|
33
|
+
if (!isAsyncCell(subject)) {
|
|
34
|
+
/* A lazy `Computed`/`State`/derive reference: unwrap to its `.value` (the held promise or
|
|
35
|
+
value), which the await block awaits — never peeked to `undefined`. */
|
|
36
|
+
return readCell(subject)
|
|
37
|
+
}
|
|
38
|
+
const cell = subject as AsyncComputed<unknown> & {
|
|
39
|
+
settled?: () => Promise<unknown> | undefined
|
|
40
|
+
}
|
|
41
|
+
/* `pending()` — in flight AND no value — is the only state that WAITS; a settled or
|
|
42
|
+
refreshing cell reads its retained value now (SWR). The read subscribes the caller. */
|
|
43
|
+
if (cell.pending()) {
|
|
44
|
+
const inFlight = cell.settled?.()
|
|
45
|
+
if (inFlight !== undefined) {
|
|
46
|
+
/* Await the in-flight settle, then read the freshly-stored value (a `settled` cell
|
|
47
|
+
writes its value before this `.then` runs — it registered its handler first); an
|
|
48
|
+
error there rejects this promise → `{:catch}`. EXCEPT a `SuspenseSignal`
|
|
49
|
+
rejection, which is a PAUSE, not a failure (an async seed's sync prefix read a
|
|
50
|
+
pending blocking dep — the cell swallows it and stays pending, reseeding once the
|
|
51
|
+
dep settles); rejecting would render `{:catch SuspenseSignal}`, so keep waiting
|
|
52
|
+
for the real settle instead. */
|
|
53
|
+
return inFlight.then(
|
|
54
|
+
() => settledValue(cell),
|
|
55
|
+
(error) => {
|
|
56
|
+
if (error instanceof SuspenseSignal) {
|
|
57
|
+
return whenSettled(cell)
|
|
58
|
+
}
|
|
59
|
+
throw error
|
|
60
|
+
},
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
/* Pending with NO in-flight promise — a stream before its first frame, or a
|
|
64
|
+
`SuspenseSignal`-paused seed (both leave `pending()` true with `settled()` cleared).
|
|
65
|
+
Resolving now would read `undefined` into `{:then}` — the exact flash ADR-0047 kills;
|
|
66
|
+
wait for the cell to actually settle. */
|
|
67
|
+
return whenSettled(cell)
|
|
68
|
+
}
|
|
69
|
+
/* Settled: an error with no retained value becomes a REJECTED promise (never a synchronous
|
|
70
|
+
throw), so `awaitBlock`/`renderToStream` route it to `{:catch}` rather than letting it
|
|
71
|
+
escape the block; otherwise the retained value now. `settledValue` is the ONE statement
|
|
72
|
+
of the value-else-error rule for both the settled and the just-settled arms. */
|
|
73
|
+
try {
|
|
74
|
+
return settledValue(cell)
|
|
75
|
+
} catch (error) {
|
|
76
|
+
return Promise.reject(error)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* The value of an in-flight cell once it settles: its retained value, or a throw when it holds
|
|
81
|
+
only an error — the throw rejects the awaiting promise, routing to `{:catch}`/`{#try}`. */
|
|
82
|
+
function settledValue(cell: AsyncComputed<unknown>): unknown {
|
|
83
|
+
const value = cell.peek()
|
|
84
|
+
if (value === undefined) {
|
|
85
|
+
const error = cell.error()
|
|
86
|
+
if (error !== undefined) {
|
|
87
|
+
throw error
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return value
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* A promise for the cell's NEXT settle, driven by the graph: an effect watches `pending()` and
|
|
94
|
+
resolves with the settled value (or rejects with its error) the moment it flips false — the
|
|
95
|
+
waiter for the states that hold no in-flight promise to chain on. The write that settles the
|
|
96
|
+
cell flushes effects synchronously (client and SSR alike), so this needs no polling; the
|
|
97
|
+
effect disposes itself on a microtask once done. */
|
|
98
|
+
function whenSettled(cell: AsyncComputed<unknown>): Promise<unknown> {
|
|
99
|
+
return new Promise((resolve, reject) => {
|
|
100
|
+
let done = false
|
|
101
|
+
let dispose: (() => void) | undefined
|
|
102
|
+
const settle = (): void => {
|
|
103
|
+
if (done || cell.pending()) {
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
done = true
|
|
107
|
+
try {
|
|
108
|
+
resolve(settledValue(cell))
|
|
109
|
+
} catch (error) {
|
|
110
|
+
reject(error)
|
|
111
|
+
}
|
|
112
|
+
/* Deferred: disposing mid-run would unlink the node while `runNode` is still
|
|
113
|
+
tracking it. */
|
|
114
|
+
queueMicrotask(() => dispose?.())
|
|
115
|
+
}
|
|
116
|
+
dispose = createEffectNode(settle)
|
|
117
|
+
})
|
|
118
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { claimChild } from '../runtime/claimChild.ts'
|
|
2
|
+
import { parkCursor } from '../runtime/parkCursor.ts'
|
|
3
|
+
import type { HydrationCursor } from '../runtime/types/HydrationCursor.ts'
|
|
4
|
+
import { assertClaimedText } from './assertClaimedText.ts'
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
Claims one text consumer's portion of the merged SSR text node at the parent's cursor:
|
|
8
|
+
assert the node begins with `value` (or the split below misaligns and orphans the tail),
|
|
9
|
+
split the node at the value's length so the remainder stays claimable, and advance past
|
|
10
|
+
the claimed portion. `splitAlways` splits even on an exact-length consume — a non-final
|
|
11
|
+
consumer in a run must leave a node behind for its sibling (an interpolation rendering
|
|
12
|
+
empty otherwise has no node and the next claim grabs the wrong sibling); the final
|
|
13
|
+
consumer keeps the cheaper split-only-when-shorter path.
|
|
14
|
+
|
|
15
|
+
Returns undefined WITHOUT moving the cursor when the cursor doesn't hold a text node —
|
|
16
|
+
a value that rendered empty on the server emitted no text node, so the cursor sits on
|
|
17
|
+
the following element/comment or past the end. The caller owns that miss policy (bind a
|
|
18
|
+
synthesized node, or skip); a text node is detected by `splitText`, not `nodeType`, so
|
|
19
|
+
the test mini-dom is covered too.
|
|
20
|
+
*/
|
|
21
|
+
export function claimText(
|
|
22
|
+
hydration: HydrationCursor,
|
|
23
|
+
parent: Node,
|
|
24
|
+
value: string,
|
|
25
|
+
splitAlways: boolean,
|
|
26
|
+
): Text | undefined {
|
|
27
|
+
const claimed = claimChild(hydration, parent)
|
|
28
|
+
if (claimed === null || typeof (claimed as Text).splitText !== 'function') {
|
|
29
|
+
return undefined
|
|
30
|
+
}
|
|
31
|
+
const node = claimed as Text
|
|
32
|
+
assertClaimedText(node, value)
|
|
33
|
+
if (splitAlways ? value.length <= node.data.length : value.length < node.data.length) {
|
|
34
|
+
node.splitText(value.length)
|
|
35
|
+
}
|
|
36
|
+
parkCursor(hydration, parent, node.nextSibling)
|
|
37
|
+
return node
|
|
38
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { claimRun } from '../runtime/claimRun.ts'
|
|
2
2
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
3
3
|
import { foreignWrapperTag } from './foreignWrapperTag.ts'
|
|
4
4
|
import { templateFor } from './templateFor.ts'
|
|
@@ -29,13 +29,7 @@ export function cloneStatic(parent: Node, html: string): void {
|
|
|
29
29
|
const source = wrapper === undefined ? template.content : (template.content.firstChild as Node)
|
|
30
30
|
const hydration = RENDER.hydration
|
|
31
31
|
if (hydration !== undefined) {
|
|
32
|
-
|
|
33
|
-
let remaining = source.childNodes.length
|
|
34
|
-
while (remaining > 0 && node !== null) {
|
|
35
|
-
node = node.nextSibling
|
|
36
|
-
remaining -= 1
|
|
37
|
-
}
|
|
38
|
-
hydration.next.set(parent, node)
|
|
32
|
+
claimRun(hydration, parent, source.childNodes.length)
|
|
39
33
|
return
|
|
40
34
|
}
|
|
41
35
|
for (const child of source.childNodes) {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { HydrationCursor } from '../runtime/types/HydrationCursor.ts'
|
|
2
|
+
import { withoutHydration } from '../runtime/withoutHydration.ts'
|
|
3
|
+
import { discardBoundary } from './discardBoundary.ts'
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
The shared divergence-recovery mechanics of every named-boundary block: remove the SSR
|
|
7
|
+
boundary (open marker through `closeData`, parking the cursor after it) and run a FRESH
|
|
8
|
+
build at the freed position with the claim cursor cleared — so the build's helpers create
|
|
9
|
+
nodes instead of claiming the discarded ones. `build` receives the node after the removed
|
|
10
|
+
boundary (its insertion reference; null = parent's end). Each block keeps its own POLICY —
|
|
11
|
+
when to give up on adoption (a resume value that didn't round-trip, a rejected guarded
|
|
12
|
+
build, a boundary whose streamed fragment never arrived) — and delegates the dance here.
|
|
13
|
+
*/
|
|
14
|
+
export function discardAndRebuild<T>(
|
|
15
|
+
hydration: HydrationCursor,
|
|
16
|
+
parent: Node,
|
|
17
|
+
open: Node | null,
|
|
18
|
+
closeData: string,
|
|
19
|
+
build: (after: Node | null) => T,
|
|
20
|
+
): T {
|
|
21
|
+
const after = discardBoundary(parent, open, closeData, hydration)
|
|
22
|
+
return withoutHydration(() => build(after))
|
|
23
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { parkCursor } from '../runtime/parkCursor.ts'
|
|
2
|
+
import type { HydrationCursor } from '../runtime/types/HydrationCursor.ts'
|
|
2
3
|
|
|
3
4
|
/* Remove an SSR boundary — open marker through close marker (inclusive) — and park
|
|
4
5
|
the hydration cursor on the node after it, returning that node. A fresh run then
|
|
@@ -8,11 +9,11 @@ export function discardBoundary(
|
|
|
8
9
|
parent: Node,
|
|
9
10
|
open: Node | null,
|
|
10
11
|
closeData: string,
|
|
11
|
-
hydration:
|
|
12
|
+
hydration: HydrationCursor,
|
|
12
13
|
): Node | null {
|
|
13
14
|
/* Nothing to discard (no open marker) — park the cursor and return, as before. */
|
|
14
15
|
if (open === null) {
|
|
15
|
-
hydration
|
|
16
|
+
parkCursor(hydration, parent, null)
|
|
16
17
|
return null
|
|
17
18
|
}
|
|
18
19
|
/* Locate the close marker WITHOUT mutating first. Removing as we walk would, on a
|
|
@@ -40,6 +41,6 @@ export function discardBoundary(
|
|
|
40
41
|
}
|
|
41
42
|
node = next
|
|
42
43
|
}
|
|
43
|
-
hydration
|
|
44
|
+
parkCursor(hydration, parent, after)
|
|
44
45
|
return after
|
|
45
46
|
}
|
package/src/lib/ui/dom/each.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { effect } from '../effect.ts'
|
|
2
2
|
import { CURRENT_PATH } from '../runtime/CURRENT_PATH.ts'
|
|
3
3
|
import { claimChild } from '../runtime/claimChild.ts'
|
|
4
|
-
import {
|
|
4
|
+
import { claimMarker } from '../runtime/claimMarker.ts'
|
|
5
5
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
6
6
|
import { scope } from '../runtime/scope.ts'
|
|
7
7
|
import { scopeGroup } from '../runtime/scopeGroup.ts'
|
|
@@ -82,8 +82,7 @@ export function each<T>(
|
|
|
82
82
|
const segment = keyed ? keyOf(item) : position
|
|
83
83
|
const hydration = RENDER.hydration
|
|
84
84
|
if (hydration !== undefined) {
|
|
85
|
-
const start =
|
|
86
|
-
hydration.next.set(parent, start.nextSibling)
|
|
85
|
+
const start = claimMarker(hydration, parent, 'each row start marker')
|
|
87
86
|
const dispose = group.track(
|
|
88
87
|
scope(() =>
|
|
89
88
|
withPathFrom(basePath, segment, () =>
|
|
@@ -91,8 +90,7 @@ export function each<T>(
|
|
|
91
90
|
),
|
|
92
91
|
),
|
|
93
92
|
)
|
|
94
|
-
const end =
|
|
95
|
-
hydration.next.set(parent, end.nextSibling)
|
|
93
|
+
const end = claimMarker(hydration, parent, 'each row end marker')
|
|
96
94
|
return { start, end, dispose, cell, indexCell }
|
|
97
95
|
}
|
|
98
96
|
/* Shared detached-range create primitive: a `[ … ]` fragment built under `parent`'s
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parkCursor } from '../runtime/parkCursor.ts'
|
|
1
2
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
2
3
|
import { scope } from '../runtime/scope.ts'
|
|
3
4
|
import type { UiProps } from '../runtime/types/UiProps.ts'
|
|
@@ -31,7 +32,7 @@ export function fillBoundary(
|
|
|
31
32
|
}
|
|
32
33
|
/* Hydrate: adopt the server content between the markers in place. */
|
|
33
34
|
const parent = open.parentNode as Node
|
|
34
|
-
hydration
|
|
35
|
+
parkCursor(hydration, parent, open.nextSibling)
|
|
35
36
|
const scoped = withScope(() => scope(() => build(parent, props)))
|
|
36
37
|
return { dispose: disposeRange(scoped, open, close) }
|
|
37
38
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { RENDER } from '../runtime/RENDER.ts'
|
|
1
|
+
import { runHydrationPass } from '../runtime/runHydrationPass.ts'
|
|
3
2
|
import { scope } from '../runtime/scope.ts'
|
|
4
3
|
import { withScope } from './withScope.ts'
|
|
5
4
|
|
|
@@ -24,18 +23,12 @@ export function hydrate(
|
|
|
24
23
|
build: (host: Element, props: unknown) => void,
|
|
25
24
|
props?: unknown,
|
|
26
25
|
): () => void {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
try {
|
|
26
|
+
/* The pass lifecycle (claim cursor, withhold window, render pass, restore-on-throw)
|
|
27
|
+
is owned by `runHydrationPass` — the same bracket the router's hydrating mount runs. */
|
|
28
|
+
return runHydrationPass(() => {
|
|
31
29
|
/* Same shared mount core as `mount` (see `withScope`) — a hydrated component owns a
|
|
32
30
|
scope too, adopting the model its build adopts — run with the claim cursor active. */
|
|
33
31
|
const { lexical } = withScope(() => scope(() => build(host, props)))
|
|
34
32
|
return () => lexical.dispose()
|
|
35
|
-
}
|
|
36
|
-
RENDER.hydration = previous
|
|
37
|
-
/* Outermost exit clears the window and wakes the peeks this pass withheld; a nested
|
|
38
|
-
child hydrate raises/lowers the depth without firing it early. */
|
|
39
|
-
hydrationWindow.exit()
|
|
40
|
-
}
|
|
33
|
+
})
|
|
41
34
|
}
|
|
@@ -1,13 +1,37 @@
|
|
|
1
|
+
import { CURRENT_PATH } from '../runtime/CURRENT_PATH.ts'
|
|
2
|
+
import { claimChild } from '../runtime/claimChild.ts'
|
|
1
3
|
import { OWNER } from '../runtime/OWNER.ts'
|
|
4
|
+
import { RANGE_OPEN } from '../runtime/RANGE_MARKER.ts'
|
|
5
|
+
import { RENDER } from '../runtime/RENDER.ts'
|
|
2
6
|
import type { UiComponent } from '../runtime/types/UiComponent.ts'
|
|
3
7
|
import { withOptionalPath } from '../runtime/withOptionalPath.ts'
|
|
8
|
+
import { commentData } from './commentData.ts'
|
|
9
|
+
import { discardAndRebuild } from './discardAndRebuild.ts'
|
|
4
10
|
import { mountRange } from './mountRange.ts'
|
|
11
|
+
import { openMarker } from './openMarker.ts'
|
|
5
12
|
|
|
6
13
|
/*
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
14
|
+
The client mount for a child component — a DUAL-MODE adopter, and the ONLY child mount the
|
|
15
|
+
compiler emits. A child mounts as a marker-bounded range at `before` in `parent` (no wrapper
|
|
16
|
+
element, so the child's root is a true direct child — see `mountRange`), its dispose filed
|
|
17
|
+
with the mounting owner so scope and DOM leave together on teardown.
|
|
18
|
+
|
|
19
|
+
The server decides per render whether a HOISTABLE child (ADR-0039) inlined (fast/settled) or
|
|
20
|
+
streamed (still-pending), and the client build can't know which — nor whether this child was
|
|
21
|
+
hoistable at all — so hydrate mode probes the cursor:
|
|
22
|
+
|
|
23
|
+
- `<!--[-->` (RANGE_OPEN) next → the server INLINED the child (every non-hoistable child,
|
|
24
|
+
and a hoistable one whose flight settled); adopt its range in place.
|
|
25
|
+
- `<!--abide:await:CHILDPATH-->` next → the server STREAMED it; `__abideSwap` has already
|
|
26
|
+
spliced the child's `<!--[-->…<!--]-->` between the boundary markers before the bundle
|
|
27
|
+
ran, so claim the boundary open, adopt the inner range in place, claim the boundary close.
|
|
28
|
+
- no hydration (a client-side navigation) → a plain create-mode mount.
|
|
29
|
+
|
|
30
|
+
CHILDPATH is the child's render-path — `withPath(ordinal, () => CURRENT_PATH.current)` —
|
|
31
|
+
computed the same way the server's `renderPath(ordinal)` wrote the boundary id, so the two
|
|
32
|
+
agree with no counter. A streamed child re-mounts (no RESUME value of its own — the boundary
|
|
33
|
+
is html-only); nested `{#await}` blocks and async cells inside it adopt through their own
|
|
34
|
+
existing channels.
|
|
11
35
|
*/
|
|
12
36
|
// @documentation plumbing
|
|
13
37
|
export function mountChild(
|
|
@@ -21,6 +45,50 @@ export function mountChild(
|
|
|
21
45
|
row key the each block already pushed). Absent (a non-compiled caller) → no path segment. */
|
|
22
46
|
ordinal?: number,
|
|
23
47
|
): void {
|
|
24
|
-
const
|
|
48
|
+
const run = <T>(build: () => T): T => withOptionalPath(ordinal, build)
|
|
49
|
+
const mount = (): { dispose: () => void } =>
|
|
50
|
+
run(() => mountRange(parent, factory.build, props, before))
|
|
51
|
+
|
|
52
|
+
const hydration = RENDER.hydration
|
|
53
|
+
if (hydration === undefined) {
|
|
54
|
+
/* Call mount() first, THEN register its dispose — `OWNER.current?.push(mount())` would
|
|
55
|
+
short-circuit the whole expression (never mounting) when there is no owner. */
|
|
56
|
+
const handle = mount()
|
|
57
|
+
OWNER.current?.push(handle.dispose)
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* Probe the cursor without consuming it. Streamed iff the next node is this child's boundary. */
|
|
62
|
+
const childPath = run(() => CURRENT_PATH.current)
|
|
63
|
+
const cursor = claimChild(hydration, parent)
|
|
64
|
+
const streamed = cursor !== null && commentData(cursor) === `abide:await:${childPath}`
|
|
65
|
+
|
|
66
|
+
if (!streamed) {
|
|
67
|
+
/* INLINE — the server inlined the child's `[ … ]` range; adopt it in place. */
|
|
68
|
+
const handle = mount()
|
|
69
|
+
OWNER.current?.push(handle.dispose)
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* STREAMED — claim the boundary open (advances the cursor to the swapped-in `[`). */
|
|
74
|
+
const open = openMarker(parent, `abide:await:${childPath}`)
|
|
75
|
+
const inner = claimChild(hydration, parent)
|
|
76
|
+
const warm = inner !== null && commentData(inner) === RANGE_OPEN
|
|
77
|
+
if (warm) {
|
|
78
|
+
const handle = mount()
|
|
79
|
+
openMarker(parent, `/abide:await:${childPath}`)
|
|
80
|
+
OWNER.current?.push(handle.dispose)
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
/* COLD (defensive — renderToStream drains every await before closing, so a warm boundary is the
|
|
84
|
+
deterministic path). The fragment never arrived: discard the empty boundary and create-mount
|
|
85
|
+
the child fresh at that position; its streamed cells resolve post-mount via receiveStreamedCell. */
|
|
86
|
+
const handle = discardAndRebuild(
|
|
87
|
+
hydration,
|
|
88
|
+
parent,
|
|
89
|
+
open,
|
|
90
|
+
`/abide:await:${childPath}`,
|
|
91
|
+
(after) => run(() => mountRange(parent, factory.build, props, after)),
|
|
92
|
+
)
|
|
25
93
|
OWNER.current?.push(handle.dispose)
|
|
26
94
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { effect } from '../effect.ts'
|
|
2
2
|
import { CURRENT_PATH } from '../runtime/CURRENT_PATH.ts'
|
|
3
|
+
import { parkCursor } from '../runtime/parkCursor.ts'
|
|
3
4
|
import { RANGE_CLOSE, RANGE_OPEN } from '../runtime/RANGE_MARKER.ts'
|
|
4
5
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
5
6
|
import { scope } from '../runtime/scope.ts'
|
|
@@ -82,7 +83,7 @@ export function mountSwappableRange<Key>(
|
|
|
82
83
|
const chosen = chosenFor(activeKey)
|
|
83
84
|
const matchedEnd = matchingRangeClose(start)
|
|
84
85
|
clearBetween(start, matchedEnd)
|
|
85
|
-
hydration
|
|
86
|
+
parkCursor(hydration, parent, matchedEnd)
|
|
86
87
|
end = openMarker(parent, RANGE_CLOSE)
|
|
87
88
|
if (chosen !== undefined) {
|
|
88
89
|
dispose = group.track(fillBefore(end, chosen))
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { claimMarker } from '../runtime/claimMarker.ts'
|
|
2
2
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
3
3
|
|
|
4
4
|
/*
|
|
@@ -12,9 +12,7 @@ rather than a list of single nodes.
|
|
|
12
12
|
export function openMarker(parent: Node, data: string, before: Node | null = null): Comment {
|
|
13
13
|
const hydration = RENDER.hydration
|
|
14
14
|
if (hydration !== undefined) {
|
|
15
|
-
|
|
16
|
-
hydration.next.set(parent, node.nextSibling)
|
|
17
|
-
return node
|
|
15
|
+
return claimMarker(hydration, parent, `control-flow marker "${data}"`)
|
|
18
16
|
}
|
|
19
17
|
const node = document.createComment(data)
|
|
20
18
|
/* `before` (a node already in `parent`) places the block among static siblings —
|
package/src/lib/ui/dom/outlet.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { claimMarker } from '../runtime/claimMarker.ts'
|
|
2
2
|
import { OUTLET_CLOSE, OUTLET_OPEN } from '../runtime/OUTLET_MARKER.ts'
|
|
3
3
|
import { PENDING_OUTLET } from '../runtime/PENDING_OUTLET.ts'
|
|
4
|
+
import { parkCursor } from '../runtime/parkCursor.ts'
|
|
4
5
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
5
6
|
import { commentData } from './commentData.ts'
|
|
6
7
|
|
|
@@ -30,7 +31,7 @@ export function outlet(
|
|
|
30
31
|
PENDING_OUTLET.current = { open, close }
|
|
31
32
|
return { open, close }
|
|
32
33
|
}
|
|
33
|
-
const open =
|
|
34
|
+
const open = claimMarker(hydration, parent, 'outlet open marker')
|
|
34
35
|
/* Skip to the matching close: depth-count outlet markers (nested child-layer slots
|
|
35
36
|
are balanced), so a layout wrapping another layout skips its WHOLE subtree. */
|
|
36
37
|
let depth = 1
|
|
@@ -56,7 +57,7 @@ export function outlet(
|
|
|
56
57
|
)
|
|
57
58
|
}
|
|
58
59
|
const close = node as Comment
|
|
59
|
-
hydration
|
|
60
|
+
parkCursor(hydration, parent, close.nextSibling)
|
|
60
61
|
PENDING_OUTLET.current = { open, close }
|
|
61
62
|
return { open, close }
|
|
62
63
|
}
|