@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
|
@@ -28,9 +28,9 @@ export function shadowInterpolationClassifier(
|
|
|
28
28
|
return undefined
|
|
29
29
|
}
|
|
30
30
|
const checker = program.getTypeChecker()
|
|
31
|
+
const mappings = shadows.get(abidePath)?.mappings ?? []
|
|
31
32
|
return (loc, code) => {
|
|
32
33
|
try {
|
|
33
|
-
const mappings = shadows.get(abidePath)?.mappings ?? []
|
|
34
34
|
const offset = sourceToShadowOffset(mappings, loc)
|
|
35
35
|
if (offset === undefined) {
|
|
36
36
|
return 'sync'
|
|
@@ -26,12 +26,10 @@ export type AnalyzedComponent = {
|
|
|
26
26
|
computedNames: Set<string>
|
|
27
27
|
/* Component signals read through `$$readCell(name)`: every `linked` and every async
|
|
28
28
|
`computed`. Threaded to both back-ends so a template reference (`{draft}`) lowers to
|
|
29
|
-
the unified cell read consistently on client and server.
|
|
29
|
+
the unified cell read consistently on client and server. Whether a pending read pauses
|
|
30
|
+
(blocking `await`) or peeks (streaming) is decided at runtime by the cell's own
|
|
31
|
+
`blocking` bit (ADR-0042), so no blocking subset is threaded through codegen. */
|
|
30
32
|
cellReadNames: Set<string>
|
|
31
|
-
/* The subset of `cellReadNames` that are BLOCKING `await` cells (ADR-0042): a template-injected
|
|
32
|
-
`{await X}` cell or a script `computed(await …)`. The CLIENT back-end reads these via
|
|
33
|
-
`$$readCellBlocking` (suspend-on-pending) rather than `$$readCell`. */
|
|
34
|
-
blockingCellNames: Set<string>
|
|
35
33
|
nodes: TemplateNode[]
|
|
36
34
|
/* One entry per non-empty `<style>` in the template (in source order): the scope
|
|
37
35
|
attribute its covered elements carry (annotated onto `nodes`) and the scoped
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import ts from 'typescript'
|
|
2
|
+
import { NESTED_REACTIVE_BINDINGS, resolveReactiveExport } from './resolveReactiveExport.ts'
|
|
3
|
+
import { wrapSeed } from './wrapSeed.ts'
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
Normalises every `state.computed(seed)` / `state.linked(seed)` in a nested branch
|
|
7
|
+
`<script>` so a BARE-VALUE seed becomes a thunk (`state.computed(a * 2)` →
|
|
8
|
+
`state.computed(() => a * 2)`), exactly as the top-level desugar (`desugarSignals` via
|
|
9
|
+
`wrapSeed`) does. A nested script keeps its reactive calls literal — it is not desugared to
|
|
10
|
+
the doc — so absent this the bare value reaches the runtime primitive, which stores it as
|
|
11
|
+
the compute function and calls it on read (`a * 2 is not a function`, a runtime crash). A
|
|
12
|
+
literal `() => …` / `function` thunk passes through unchanged, so the wrap is a no-op on the
|
|
13
|
+
already-thunked form the author usually writes.
|
|
14
|
+
|
|
15
|
+
Recognition is name-based (`NESTED_REACTIVE_BINDINGS`) because a nested script carries no
|
|
16
|
+
import of its own — it inherits the surface by the canonical `state` name, the same way
|
|
17
|
+
`nestedBindingNames` resolves it. Only the seed argument (`arguments[0]`) is wrapped; a
|
|
18
|
+
`state.linked(seed, ...)`'s trailing args pass through. Exposed as a `ts.TransformerFactory`
|
|
19
|
+
so the nested-script lowering can chain it ahead of the reference rename over ONE parsed tree.
|
|
20
|
+
*/
|
|
21
|
+
export function wrapReactiveSeedsTransformer(): ts.TransformerFactory<ts.SourceFile> {
|
|
22
|
+
return (context) => (root) => {
|
|
23
|
+
const visit = (node: ts.Node): ts.Node => {
|
|
24
|
+
if (ts.isCallExpression(node) && node.arguments.length > 0) {
|
|
25
|
+
const primitive = resolveReactiveExport(node.expression, NESTED_REACTIVE_BINDINGS)
|
|
26
|
+
if (primitive === 'computed' || primitive === 'linked') {
|
|
27
|
+
const seed = node.arguments[0] as ts.Expression
|
|
28
|
+
const wrapped = wrapSeed(ts.visitNode(seed, visit) as ts.Expression)
|
|
29
|
+
const rest = node.arguments
|
|
30
|
+
.slice(1)
|
|
31
|
+
.map((argument) => ts.visitNode(argument, visit) as ts.Expression)
|
|
32
|
+
return ts.factory.updateCallExpression(
|
|
33
|
+
node,
|
|
34
|
+
node.expression,
|
|
35
|
+
node.typeArguments,
|
|
36
|
+
[wrapped, ...rest],
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return ts.visitEachChild(node, visit, context)
|
|
41
|
+
}
|
|
42
|
+
return ts.visitNode(root, visit) as ts.SourceFile
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import ts from 'typescript'
|
|
2
|
+
import { hasTopLevelAwait } from './hasTopLevelAwait.ts'
|
|
3
|
+
|
|
4
|
+
const factory = ts.factory
|
|
5
|
+
|
|
6
|
+
/* Normalises a `computed`/`linked` argument into a seed THUNK: a literal `() => …` /
|
|
7
|
+
`function` argument passes through unchanged (the author already wrote the thunk), any
|
|
8
|
+
other expression is wrapped as `() => arg`. The wrapper arrow is made ASYNC when the
|
|
9
|
+
expression contains a top-level `await` (`computed(await load())` → `async () => await
|
|
10
|
+
load()`), which is exactly the marker the runtime primitive uses to route to an async cell.
|
|
11
|
+
Shared by the top-level desugar (`desugarSignals`) and the nested-`<script>` lowering
|
|
12
|
+
(`wrapReactiveSeeds`) so a bare-value seed is normalised identically on both surfaces. */
|
|
13
|
+
export function wrapSeed(argument: ts.Expression): ts.Expression {
|
|
14
|
+
if (ts.isArrowFunction(argument) || ts.isFunctionExpression(argument)) {
|
|
15
|
+
return argument
|
|
16
|
+
}
|
|
17
|
+
const modifiers = hasTopLevelAwait(argument)
|
|
18
|
+
? [factory.createModifier(ts.SyntaxKind.AsyncKeyword)]
|
|
19
|
+
: undefined
|
|
20
|
+
return factory.createArrowFunction(
|
|
21
|
+
modifiers,
|
|
22
|
+
undefined,
|
|
23
|
+
[],
|
|
24
|
+
undefined,
|
|
25
|
+
factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
|
|
26
|
+
argument,
|
|
27
|
+
)
|
|
28
|
+
}
|
package/src/lib/ui/computed.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { createComputedNode } from './runtime/createComputedNode.ts'
|
|
|
5
5
|
import { isAsyncFunction } from './runtime/isAsyncFunction.ts'
|
|
6
6
|
import { OWNER } from './runtime/OWNER.ts'
|
|
7
7
|
import { readNode } from './runtime/readNode.ts'
|
|
8
|
+
import { routeBareSeed } from './runtime/routeBareSeed.ts'
|
|
8
9
|
import type { Computed } from './runtime/types/Computed.ts'
|
|
9
10
|
import { unlinkDeps } from './runtime/unlinkDeps.ts'
|
|
10
11
|
|
|
@@ -32,6 +33,21 @@ export function computed<T>(compute: () => T): Computed<T>
|
|
|
32
33
|
export function computed<T>(
|
|
33
34
|
compute: () => T | Promise<T> | NamedAsyncIterable<T>,
|
|
34
35
|
): Computed<T> | AsyncComputed<T> {
|
|
36
|
+
/* A BARE seed that reached the runtime literally — a branch-nested `<script>` (which
|
|
37
|
+
keeps its `state.computed(...)` calls literal; see lowerScript) or a direct JS caller.
|
|
38
|
+
The compile-time `wrapSeed` normalization only covers the leading script, so honor the
|
|
39
|
+
same authored contract (ADR-0045: a bare seed behaves like its thunk form) by VALUE via
|
|
40
|
+
the shared `routeBareSeed`; a non-async value becomes a constant computed over it.
|
|
41
|
+
Without this a raw value lands as `node.compute` and the first read calls it
|
|
42
|
+
(`node.compute is 3` — a dead render mid-stream). */
|
|
43
|
+
if (typeof compute !== 'function') {
|
|
44
|
+
const seed: unknown = compute
|
|
45
|
+
const cell = routeBareSeed(seed, false)
|
|
46
|
+
if (cell !== undefined) {
|
|
47
|
+
return cell as AsyncComputed<T>
|
|
48
|
+
}
|
|
49
|
+
compute = () => seed as T
|
|
50
|
+
}
|
|
35
51
|
/* `await` marker: an async-function thunk unwraps its promise into a tracked async cell.
|
|
36
52
|
This is the only classification the read-only primitive can make without running the
|
|
37
53
|
seed — a sync `computed` MUST stay lazy (it does not compute until first read). A stream
|
|
@@ -4,6 +4,7 @@ import { computed } from './computed.ts'
|
|
|
4
4
|
import { effect } from './effect.ts'
|
|
5
5
|
import { linked } from './linked.ts'
|
|
6
6
|
import { CURRENT_PATH } from './runtime/CURRENT_PATH.ts'
|
|
7
|
+
import { consumeSeed } from './runtime/consumeSeed.ts'
|
|
7
8
|
import { createDoc } from './runtime/createDoc.ts'
|
|
8
9
|
import { DOC_SEED } from './runtime/DOC_SEED.ts'
|
|
9
10
|
import type { Cell } from './runtime/types/Cell.ts'
|
|
@@ -50,13 +51,13 @@ export function createScope(
|
|
|
50
51
|
on the client, so a uuid/timestamp/random would diverge from the SSR HTML. The server snapshot
|
|
51
52
|
for this scope's render-path id is decoded here and consumed by `replace` on each slot's FIRST
|
|
52
53
|
write (the eager init), keeping the server value while the throwaway init is discarded. One-shot
|
|
53
|
-
|
|
54
|
+
via `consumeSeed` (two-phase, ADR-0048) — an SPA re-nav to the same path re-inits fresh, and a
|
|
55
|
+
hydration-desync throw hands the seed to the cold rebuild; empty on the server. */
|
|
54
56
|
let pendingSeed: Map<string, unknown> | undefined
|
|
55
57
|
/* `DOC_SEED` is populated only on the client by `startClient` (empty on the server), so the read
|
|
56
58
|
needs no window guard — it mirrors the async-cell warm read. */
|
|
57
|
-
const encodedSeed = DOC_SEED
|
|
59
|
+
const encodedSeed = consumeSeed(DOC_SEED, id)
|
|
58
60
|
if (encodedSeed !== undefined) {
|
|
59
|
-
delete DOC_SEED[id]
|
|
60
61
|
try {
|
|
61
62
|
pendingSeed = new Map(
|
|
62
63
|
Object.entries(decodeRefJson(encodedSeed) as Record<string, unknown>),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parkCursor } from '../runtime/parkCursor.ts'
|
|
1
2
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
2
3
|
|
|
3
4
|
/*
|
|
@@ -18,7 +19,7 @@ export function anchorCursor(anchor: Node): Node | null {
|
|
|
18
19
|
const reference = anchor.nextSibling
|
|
19
20
|
const hydration = RENDER.hydration
|
|
20
21
|
if (hydration !== undefined) {
|
|
21
|
-
hydration
|
|
22
|
+
parkCursor(hydration, anchor.parentNode as Node, reference)
|
|
22
23
|
}
|
|
23
24
|
return reference
|
|
24
25
|
}
|
|
@@ -19,9 +19,10 @@ the accidental copy-paste, notably its two subtle guards:
|
|
|
19
19
|
rethrowing, so the caller's cold rebuild starts clean).
|
|
20
20
|
|
|
21
21
|
`anchor` is a mutable field: each block parks its own anchor text node at create /
|
|
22
|
-
hydrate / rebuild time
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
hydrate / rebuild time via `parkAnchor` (WHERE it parks is bespoke — before the boundary
|
|
23
|
+
close, after a discarded boundary, at the create-mode `before`), and `place` reads it. The
|
|
24
|
+
block owns its `scopeGroup`; each built branch's disposer is tracked in it so it disposes
|
|
25
|
+
on owner teardown, not only on a swap.
|
|
25
26
|
*/
|
|
26
27
|
export function anchoredBranch(
|
|
27
28
|
parent: Node,
|
|
@@ -29,6 +30,7 @@ export function anchoredBranch(
|
|
|
29
30
|
): {
|
|
30
31
|
anchor: Node | undefined
|
|
31
32
|
detach: () => void
|
|
33
|
+
parkAnchor: (before: Node | null) => void
|
|
32
34
|
place: (build: (host: Node) => void, wrapBuild?: (run: () => void) => void) => void
|
|
33
35
|
adoptStrand: (
|
|
34
36
|
build: (host: Node) => void,
|
|
@@ -128,6 +130,14 @@ export function anchoredBranch(
|
|
|
128
130
|
}
|
|
129
131
|
}
|
|
130
132
|
|
|
133
|
+
/* Park a fresh empty text node as the branch's swap anchor at `before` (null appends) —
|
|
134
|
+
the create/hydrate/rebuild triple every block repeated by hand. */
|
|
135
|
+
const parkAnchor = (before: Node | null): void => {
|
|
136
|
+
const node = document.createTextNode('')
|
|
137
|
+
anchor = node
|
|
138
|
+
parent.insertBefore(node, before)
|
|
139
|
+
}
|
|
140
|
+
|
|
131
141
|
return {
|
|
132
142
|
get anchor(): Node | undefined {
|
|
133
143
|
return anchor
|
|
@@ -136,6 +146,7 @@ export function anchoredBranch(
|
|
|
136
146
|
anchor = node
|
|
137
147
|
},
|
|
138
148
|
detach,
|
|
149
|
+
parkAnchor,
|
|
139
150
|
place,
|
|
140
151
|
adoptStrand,
|
|
141
152
|
}
|
|
@@ -1,34 +1,25 @@
|
|
|
1
1
|
import { claimChild } from '../runtime/claimChild.ts'
|
|
2
|
+
import { parkCursor } from '../runtime/parkCursor.ts'
|
|
2
3
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
3
|
-
import {
|
|
4
|
+
import { claimText } from './claimText.ts'
|
|
4
5
|
|
|
5
6
|
/*
|
|
6
7
|
A static (non-reactive) text node under `parent`: created and appended (create
|
|
7
|
-
mode), or claimed from the server-rendered text (hydrate mode)
|
|
8
|
-
|
|
9
|
-
lines up
|
|
8
|
+
mode), or claimed from the server-rendered text (hydrate mode) — the shared
|
|
9
|
+
`claimText` asserts, splits the merged SSR node at this literal's length so the
|
|
10
|
+
next claim lines up, and advances. Nothing is bound since the text never changes.
|
|
11
|
+
A static always renders on the server, so a non-text node at the cursor is a
|
|
12
|
+
tolerated desync: advance past it (assertClaimedText's domain is a text mismatch;
|
|
13
|
+
a structural one is claimExpected's).
|
|
10
14
|
*/
|
|
11
15
|
// @documentation plumbing
|
|
12
16
|
export function appendStatic(parent: Node, value: string, splitAlways = false): void {
|
|
13
17
|
const hydration = RENDER.hydration
|
|
14
18
|
if (hydration !== undefined) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
legibly at the divergence (only when a text node is actually here; a structural
|
|
19
|
-
mismatch is claimExpected's domain). */
|
|
20
|
-
if (isText) {
|
|
21
|
-
assertClaimedText(node, value)
|
|
19
|
+
if (claimText(hydration, parent, value, splitAlways) === undefined) {
|
|
20
|
+
const node = claimChild(hydration, parent)
|
|
21
|
+
parkCursor(hydration, parent, node === null ? null : node.nextSibling)
|
|
22
22
|
}
|
|
23
|
-
/* Split even on an exact-length consume when a sibling text binding follows
|
|
24
|
-
(`splitAlways`), so it gets its own node; see appendText for the rationale. */
|
|
25
|
-
if (
|
|
26
|
-
isText &&
|
|
27
|
-
(splitAlways ? value.length <= node.data.length : value.length < node.data.length)
|
|
28
|
-
) {
|
|
29
|
-
node.splitText(value.length)
|
|
30
|
-
}
|
|
31
|
-
hydration.next.set(parent, node === null ? null : node.nextSibling)
|
|
32
23
|
return
|
|
33
24
|
}
|
|
34
25
|
parent.appendChild(document.createTextNode(value))
|
|
@@ -2,10 +2,11 @@ import { rawHtmlString } from '../../shared/html.ts'
|
|
|
2
2
|
import { snippetPayload } from '../../shared/snippet.ts'
|
|
3
3
|
import { effect } from '../effect.ts'
|
|
4
4
|
import { claimChild } from '../runtime/claimChild.ts'
|
|
5
|
+
import { parkCursor } from '../runtime/parkCursor.ts'
|
|
5
6
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
6
7
|
import { SuspenseSignal } from '../runtime/SuspenseSignal.ts'
|
|
7
8
|
import { appendSnippet } from './appendSnippet.ts'
|
|
8
|
-
import {
|
|
9
|
+
import { claimText } from './claimText.ts'
|
|
9
10
|
import { isComment } from './isComment.ts'
|
|
10
11
|
import { parseRawNodes } from './parseRawNodes.ts'
|
|
11
12
|
import { readTextOrSuspend } from './readTextOrSuspend.ts'
|
|
@@ -34,7 +35,15 @@ cold client render (on hydrate the warm-seed makes the cell `refreshing()`, not
|
|
|
34
35
|
decided from the first resolved value.
|
|
35
36
|
*/
|
|
36
37
|
// @documentation plumbing
|
|
37
|
-
export function appendText(
|
|
38
|
+
export function appendText(
|
|
39
|
+
parent: Node,
|
|
40
|
+
read: () => unknown,
|
|
41
|
+
splitAlways = false,
|
|
42
|
+
/* Create-mode insertion reference: content lands before this node instead of at the end of
|
|
43
|
+
`parent` (`null` appends). Lets `appendTextAt` mount at a skeleton anchor through this one
|
|
44
|
+
create path. Ignored on hydrate, where the cursor decides position. */
|
|
45
|
+
before: Node | null = null,
|
|
46
|
+
): void {
|
|
38
47
|
/* Probe the first value once, tolerating a suspend (a pending blocking read) — a suspended
|
|
39
48
|
interpolation skips snippet/html detection and takes the text path, starting empty. */
|
|
40
49
|
let probe: unknown
|
|
@@ -50,62 +59,51 @@ export function appendText(parent: Node, read: () => unknown, splitAlways = fals
|
|
|
50
59
|
/* A snippet call (`{row(args)}`) mounts its builder; a `html\`\`` value inserts
|
|
51
60
|
raw markup; everything else is escaped text — decided by the first value. */
|
|
52
61
|
if (!suspended && typeof snippetPayload(probe) === 'function') {
|
|
53
|
-
|
|
62
|
+
if (before === null) {
|
|
63
|
+
appendSnippet(parent, read)
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
/* Positioned mount: buffer the snippet in a fragment, then splice it in at `before` —
|
|
67
|
+
`appendSnippet` itself only appends, and its markers track position from then on. */
|
|
68
|
+
const fragment = document.createDocumentFragment()
|
|
69
|
+
appendSnippet(fragment, read)
|
|
70
|
+
parent.insertBefore(fragment, before)
|
|
54
71
|
return
|
|
55
72
|
}
|
|
56
73
|
if (!suspended && rawHtmlString(probe) !== undefined) {
|
|
57
|
-
appendRawHtml(parent, read)
|
|
74
|
+
appendRawHtml(parent, read, before)
|
|
58
75
|
return
|
|
59
76
|
}
|
|
60
77
|
const hydration = RENDER.hydration
|
|
61
78
|
if (hydration !== undefined) {
|
|
62
|
-
const claimed = claimChild(hydration, parent)
|
|
63
79
|
/* Nullish reads render as empty text, never the literal `"undefined"` — so a
|
|
64
80
|
pending async read (undefined-while-pending, ADR-0032 D3) shows nothing. A blocking
|
|
65
81
|
read never suspends here (warm-seeded → `refreshing()`), so `probe` holds its value. */
|
|
66
82
|
const firstValue = probe
|
|
67
83
|
const value = firstValue == null ? '' : String(firstValue)
|
|
68
|
-
/*
|
|
69
|
-
|
|
70
|
-
text node
|
|
71
|
-
|
|
72
|
-
for the next consumer (a following element hole, a sibling binding,
|
|
73
|
-
Without
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (isText) {
|
|
83
|
-
assertClaimedText(node, value)
|
|
84
|
-
}
|
|
85
|
-
/* Peel this binding's text off the merged SSR node. A non-final binding in a
|
|
86
|
-
run (`splitAlways`) splits even when it consumes the whole node, leaving an
|
|
87
|
-
empty node for the next binding — otherwise an interpolation that renders to
|
|
88
|
-
empty string (or whose followers do) has no node and the next claim grabs the
|
|
89
|
-
wrong sibling. The final binding keeps `<` so it doesn't leave a stray node a
|
|
90
|
-
following element would claim. A synthesized node is already this binding's own,
|
|
91
|
-
so it never splits. */
|
|
92
|
-
if (
|
|
93
|
-
isText &&
|
|
94
|
-
(splitAlways ? value.length <= node.data.length : value.length < node.data.length)
|
|
95
|
-
) {
|
|
96
|
-
node.splitText(value.length)
|
|
84
|
+
/* Claim this binding's portion of the merged SSR text node (assert + split +
|
|
85
|
+
advance — see `claimText`). A value that first rendered empty produced NO server
|
|
86
|
+
text node, so the cursor points at the following node (an element/comment) or past
|
|
87
|
+
the end — the miss arm: bind a synthesized empty Text at the cursor and leave the
|
|
88
|
+
unclaimed node for the next consumer (a following element hole, a sibling binding,
|
|
89
|
+
or nothing). Without it the bind effect below derefs a null/element node. */
|
|
90
|
+
let node = claimText(hydration, parent, value, splitAlways)
|
|
91
|
+
if (node === undefined) {
|
|
92
|
+
const unclaimed = claimChild(hydration, parent)
|
|
93
|
+
node = parent.insertBefore(document.createTextNode(''), unclaimed)
|
|
94
|
+
/* Pin the cursor on the still-unclaimed node — the synthesized node is this
|
|
95
|
+
binding's own, and an unset cursor would default back to the (now synthesized)
|
|
96
|
+
first child. */
|
|
97
|
+
parkCursor(hydration, parent, unclaimed)
|
|
97
98
|
}
|
|
98
|
-
|
|
99
|
-
still-unclaimed `claimed` node it was inserted before (an element/comment, or null
|
|
100
|
-
at the end). */
|
|
101
|
-
hydration.next.set(parent, isText ? node.nextSibling : claimed)
|
|
99
|
+
const bound = node
|
|
102
100
|
effect(() => {
|
|
103
|
-
|
|
101
|
+
bound.data = readTextOrSuspend(read)
|
|
104
102
|
})
|
|
105
103
|
return
|
|
106
104
|
}
|
|
107
105
|
const node = document.createTextNode('')
|
|
108
|
-
parent.
|
|
106
|
+
parent.insertBefore(node, before)
|
|
109
107
|
effect(() => {
|
|
110
108
|
node.data = readTextOrSuspend(read)
|
|
111
109
|
})
|
|
@@ -113,7 +111,7 @@ export function appendText(parent: Node, read: () => unknown, splitAlways = fals
|
|
|
113
111
|
|
|
114
112
|
/* Raw-markup interpolation: parse the branded string into nodes behind an anchor,
|
|
115
113
|
re-parsing on change; on hydrate adopt the server markup between its markers. */
|
|
116
|
-
function appendRawHtml(parent: Node, read: () => unknown): void {
|
|
114
|
+
function appendRawHtml(parent: Node, read: () => unknown, before: Node | null): void {
|
|
117
115
|
const hydration = RENDER.hydration
|
|
118
116
|
const markup = (): string => rawHtmlString(read()) ?? ''
|
|
119
117
|
const anchor = document.createTextNode('')
|
|
@@ -141,7 +139,7 @@ function appendRawHtml(parent: Node, read: () => unknown): void {
|
|
|
141
139
|
nodes.push(node)
|
|
142
140
|
node = node.nextSibling
|
|
143
141
|
}
|
|
144
|
-
hydration
|
|
142
|
+
parkCursor(hydration, parent, node === null ? null : node.nextSibling)
|
|
145
143
|
parent.insertBefore(anchor, node)
|
|
146
144
|
let first = true
|
|
147
145
|
effect(() => {
|
|
@@ -155,7 +153,7 @@ function appendRawHtml(parent: Node, read: () => unknown): void {
|
|
|
155
153
|
return
|
|
156
154
|
}
|
|
157
155
|
|
|
158
|
-
parent.
|
|
156
|
+
parent.insertBefore(anchor, before)
|
|
159
157
|
effect(() => {
|
|
160
158
|
set(markup())
|
|
161
159
|
})
|
|
@@ -1,12 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { snippetPayload } from '../../shared/snippet.ts'
|
|
3
|
-
import { effect } from '../effect.ts'
|
|
1
|
+
import { parkCursor } from '../runtime/parkCursor.ts'
|
|
4
2
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
5
|
-
import { SuspenseSignal } from '../runtime/SuspenseSignal.ts'
|
|
6
|
-
import { appendSnippet } from './appendSnippet.ts'
|
|
7
3
|
import { appendText } from './appendText.ts'
|
|
8
|
-
import { parseRawNodes } from './parseRawNodes.ts'
|
|
9
|
-
import { readTextOrSuspend } from './readTextOrSuspend.ts'
|
|
10
4
|
|
|
11
5
|
/*
|
|
12
6
|
A reactive `{expr}` interpolation mounted at a skeleton anchor comment (`<!--a-->`), used
|
|
@@ -20,67 +14,26 @@ The anchor is KEPT (in both the SSR markup and the client DOM, like control flow
|
|
|
20
14
|
`<!--[-->` range markers), so server and client render identical markup — `appendTextAt`
|
|
21
15
|
never strips it.
|
|
22
16
|
|
|
23
|
-
|
|
24
|
-
content (the server rendered `<!--a-->value`, so the value is the anchor's next
|
|
25
|
-
|
|
17
|
+
Both paths delegate to `appendText`: hydrate with the cursor temporarily pointed at the
|
|
18
|
+
anchor's content (the server rendered `<!--a-->value`, so the value is the anchor's next
|
|
19
|
+
sibling), create with the anchor's next sibling as the insertion reference — one
|
|
20
|
+
text-split / snippet / raw-html implementation, positioned two ways.
|
|
26
21
|
*/
|
|
27
22
|
// @documentation plumbing
|
|
28
23
|
export function appendTextAt(anchor: Node, read: () => unknown): void {
|
|
29
24
|
const parent = anchor.parentNode as Node
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
const hydration = RENDER.hydration
|
|
26
|
+
if (hydration !== undefined) {
|
|
32
27
|
const had = hydration.next.has(parent)
|
|
33
28
|
const saved = hydration.next.get(parent)
|
|
34
|
-
hydration
|
|
29
|
+
parkCursor(hydration, parent, anchor.nextSibling)
|
|
35
30
|
appendText(parent, read)
|
|
36
31
|
if (had) {
|
|
37
|
-
hydration
|
|
32
|
+
parkCursor(hydration, parent, saved ?? null)
|
|
38
33
|
} else {
|
|
39
34
|
hydration.next.delete(parent)
|
|
40
35
|
}
|
|
41
36
|
return
|
|
42
37
|
}
|
|
43
|
-
|
|
44
|
-
/* Probe once, tolerating a suspend (a pending blocking read): a suspended interpolation skips
|
|
45
|
-
snippet/html detection and takes the text path, starting empty until the value resolves. */
|
|
46
|
-
let first: unknown
|
|
47
|
-
let suspended = false
|
|
48
|
-
try {
|
|
49
|
-
first = read()
|
|
50
|
-
} catch (signal) {
|
|
51
|
-
if (!(signal instanceof SuspenseSignal)) {
|
|
52
|
-
throw signal
|
|
53
|
-
}
|
|
54
|
-
suspended = true
|
|
55
|
-
}
|
|
56
|
-
if (!suspended && typeof snippetPayload(first) === 'function') {
|
|
57
|
-
const fragment = document.createDocumentFragment()
|
|
58
|
-
appendSnippet(fragment, read)
|
|
59
|
-
parent.insertBefore(fragment, anchor.nextSibling)
|
|
60
|
-
return
|
|
61
|
-
}
|
|
62
|
-
if (!suspended && rawHtmlString(first) !== undefined) {
|
|
63
|
-
let nodes: Node[] = []
|
|
64
|
-
effect(() => {
|
|
65
|
-
for (const node of nodes) {
|
|
66
|
-
parent.removeChild(node)
|
|
67
|
-
}
|
|
68
|
-
nodes = parseRawNodes(parent, rawHtmlString(read()) ?? '')
|
|
69
|
-
/* Insert the fresh markup just after the anchor (its live re-insertion point). */
|
|
70
|
-
const after = anchor.nextSibling
|
|
71
|
-
for (const node of nodes) {
|
|
72
|
-
parent.insertBefore(node, after)
|
|
73
|
-
}
|
|
74
|
-
})
|
|
75
|
-
return
|
|
76
|
-
}
|
|
77
|
-
const node = document.createTextNode('')
|
|
78
|
-
parent.insertBefore(node, anchor.nextSibling)
|
|
79
|
-
/* Text/suspend handling shared with `appendText`'s bind via `readTextOrSuspend`: stringify the
|
|
80
|
-
value, show '' while a blocking read is pending (re-running on settle, since the read tracked
|
|
81
|
-
its cell), and coerce nullish to '' (never the literal "undefined") — keeping this create path
|
|
82
|
-
congruent with the hydrate path above, which delegates to `appendText`. */
|
|
83
|
-
effect(() => {
|
|
84
|
-
node.data = readTextOrSuspend(read)
|
|
85
|
-
})
|
|
38
|
+
appendText(parent, read, false, anchor.nextSibling)
|
|
86
39
|
}
|