@abide/abide 0.53.0 → 0.55.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 -494
- package/CHANGELOG.md +106 -0
- package/README.md +166 -146
- package/package.json +3 -4
- package/src/checkAbide.ts +1 -1
- 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/amendBroadcaster.ts +53 -0
- package/src/lib/server/runtime/createUiPageRenderer.ts +54 -20
- package/src/lib/server/runtime/runWithRequestScope.ts +1 -0
- package/src/lib/server/runtime/types/RequestStore.ts +9 -0
- package/src/lib/server/sockets/amendFamilyEntry.ts +39 -0
- package/src/lib/server/sockets/createSocketDispatcher.ts +9 -0
- package/src/lib/server/sockets/defineSocket.ts +11 -1
- package/src/lib/shared/AMEND_TOPIC_PREFIX.ts +9 -0
- package/src/lib/shared/SEEDS.ts +33 -0
- package/src/lib/shared/SOCKET_SEED.ts +12 -0
- package/src/lib/shared/amend.ts +48 -0
- package/src/lib/shared/amendBroadcastSlot.ts +19 -0
- package/src/lib/shared/applyAmendLocally.ts +20 -0
- package/src/lib/shared/attachRpcSelectorMethods.ts +5 -5
- package/src/lib/shared/buildSocketOverChannel.ts +9 -2
- package/src/lib/shared/cache.ts +203 -84
- package/src/lib/shared/cacheReaderSocketSlot.ts +11 -0
- package/src/lib/shared/createCacheStore.ts +8 -0
- 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 +4 -7
- package/src/lib/shared/socketTailsSlot.ts +13 -0
- package/src/lib/shared/types/AmendApply.ts +15 -0
- package/src/lib/shared/types/CacheReaderHook.ts +11 -0
- package/src/lib/shared/types/RemoteFunction.ts +30 -19
- package/src/lib/shared/types/SocketTails.ts +13 -0
- package/src/lib/shared/types/SsrPayload.ts +6 -0
- package/src/lib/test/createTestApp.ts +6 -0
- package/src/lib/ui/amendReaderHook.ts +125 -0
- 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 +39 -0
- package/src/lib/ui/compile/compileComponent.ts +1 -3
- package/src/lib/ui/compile/componentOrdinals.ts +39 -0
- package/src/lib/ui/compile/desugarSignals.ts +65 -36
- 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/interpolationClassifierForRoot.ts +8 -13
- package/src/lib/ui/compile/isSpuriousAsyncReadDiagnostic.ts +46 -5
- package/src/lib/ui/compile/lowerCompoundAssignment.ts +49 -0
- package/src/lib/ui/compile/lowerContext.ts +26 -6
- package/src/lib/ui/compile/lowerDocAccess.ts +19 -31
- package/src/lib/ui/compile/lowerScript.ts +1 -11
- package/src/lib/ui/compile/lowerUpdateExpression.ts +32 -0
- package/src/lib/ui/compile/renameSignalRefs.ts +25 -45
- package/src/lib/ui/compile/seedTypeClassifierForRoot.ts +10 -15
- package/src/lib/ui/compile/shadowInterpolationClassifier.ts +1 -1
- package/src/lib/ui/compile/types/AnalyzedComponent.ts +3 -5
- package/src/lib/ui/compile/wrapReactionCellSources.ts +10 -6
- 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 +31 -45
- 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 +27 -39
- package/src/lib/ui/dom/when.ts +6 -2
- package/src/lib/ui/linked.ts +14 -0
- package/src/lib/ui/resumeSeedScript.ts +1 -1
- package/src/lib/ui/router.ts +12 -16
- 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 +56 -8
- package/src/lib/ui/runtime/createComputedNode.ts +1 -0
- package/src/lib/ui/runtime/createDoc.ts +5 -1
- 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/Cell.ts +4 -2
- package/src/lib/ui/runtime/types/Doc.ts +3 -1
- 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/socketProxy.ts +17 -1
- package/src/lib/ui/startClient.ts +23 -0
- package/src/lib/ui/tryEncodeResume.ts +6 -15
- package/src/lib/ui/types/Scope.ts +1 -1
- package/src/lib/ui/watch.ts +21 -16
- package/src/serverEntry.ts +15 -0
- package/src/lib/shared/patch.ts +0 -41
- package/src/lib/ui/compile/COMPOUND_ASSIGNMENT_OPERATORS.ts +0 -19
- package/src/lib/ui/dom/mountStreamedChild.ts +0 -84
- package/src/lib/ui/dom/readCellBlocking.ts +0 -29
|
@@ -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
|
}
|
|
@@ -1,19 +1,19 @@
|
|
|
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'
|
|
8
9
|
import type { ResumeEntry } from '../runtime/RESUME.ts'
|
|
9
10
|
import { RESUME } from '../runtime/RESUME.ts'
|
|
10
|
-
import { scopeGroup } from '../runtime/scopeGroup.ts'
|
|
11
11
|
import { SuspenseSignal } from '../runtime/SuspenseSignal.ts'
|
|
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
|
}
|