@abide/abide 0.44.0 → 0.45.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 +8 -6
- package/CHANGELOG.md +64 -0
- package/README.md +171 -181
- package/package.json +2 -1
- package/src/abideResolverPlugin.ts +6 -1
- package/src/devEntry.ts +43 -9
- package/src/lib/cli/parseArgvForRpc.ts +21 -24
- package/src/lib/cli/runCli.ts +9 -24
- package/src/lib/cli/tokenizeArgvFlags.ts +84 -0
- package/src/lib/server/error.ts +48 -53
- package/src/lib/server/json.ts +4 -3
- package/src/lib/server/jsonl.ts +1 -1
- package/src/lib/server/rpc/defineRpc.ts +11 -9
- package/src/lib/server/rpc/types/RemoteHandler.ts +5 -7
- package/src/lib/server/rpc/types/RpcHelper.ts +125 -35
- package/src/lib/server/rpc/types/TypedError.ts +18 -0
- package/src/lib/server/rpc/validationError.ts +3 -3
- package/src/lib/server/runtime/STATUS_TEXT.ts +22 -0
- package/src/lib/server/runtime/createServer.ts +36 -12
- package/src/lib/server/runtime/devHotModuleResponse.ts +1 -1
- package/src/lib/server/runtime/gzipResponse.ts +14 -16
- package/src/lib/server/runtime/installAmbientScopeStore.ts +33 -0
- package/src/lib/server/runtime/runWithRequestScope.ts +4 -1
- package/src/lib/server/runtime/snapshotEntryFromCache.ts +2 -0
- package/src/lib/server/runtime/typedErrorResponse.ts +26 -0
- package/src/lib/server/runtime/types/RequestStore.ts +25 -0
- package/src/lib/server/runtime/withResponseDefaults.ts +8 -3
- package/src/lib/server/sockets/defineSocket.ts +5 -1
- package/src/lib/server/sse.ts +1 -1
- package/src/lib/shared/HttpError.ts +1 -1
- package/src/lib/shared/RPC_ARGS_TYPE.ts +7 -0
- package/src/lib/shared/activeCacheStore.ts +5 -14
- package/src/lib/shared/activePage.ts +5 -19
- package/src/lib/shared/augmentModule.ts +18 -0
- package/src/lib/shared/basePath.ts +6 -5
- package/src/lib/shared/baseResolver.ts +10 -0
- package/src/lib/shared/baseSlot.ts +6 -12
- package/src/lib/shared/cache.ts +46 -4
- package/src/lib/shared/cacheEntryFromSnapshot.ts +21 -1
- package/src/lib/shared/cacheKeyOf.ts +7 -0
- package/src/lib/shared/cacheKeyStore.ts +8 -0
- package/src/lib/shared/cacheStoreResolver.ts +12 -0
- package/src/lib/shared/cacheStoreSlot.ts +5 -13
- package/src/lib/shared/changeAffectsClient.ts +35 -0
- package/src/lib/shared/createResolverSlot.ts +37 -0
- package/src/lib/shared/debugGate.ts +90 -0
- package/src/lib/shared/emitLogRecord.ts +18 -4
- package/src/lib/shared/globalCacheStoreResolver.ts +12 -0
- package/src/lib/shared/globalCacheStoreSlot.ts +6 -11
- package/src/lib/shared/httpErrorFor.ts +1 -1
- package/src/lib/shared/isDebugEnabled.ts +3 -10
- package/src/lib/shared/isDebugNegated.ts +4 -6
- package/src/lib/shared/pageResolver.ts +16 -0
- package/src/lib/shared/pageSlot.ts +5 -14
- package/src/lib/shared/recordCacheKey.ts +6 -0
- package/src/lib/shared/requestScopeResolver.ts +12 -0
- package/src/lib/shared/requestScopeSlot.ts +7 -12
- package/src/lib/shared/responseBodyKind.ts +35 -0
- package/src/lib/shared/setBaseResolver.ts +2 -4
- package/src/lib/shared/setCacheStoreResolver.ts +3 -5
- package/src/lib/shared/setGlobalCacheStoreResolver.ts +3 -5
- package/src/lib/shared/setPageResolver.ts +2 -5
- package/src/lib/shared/setRequestScopeResolver.ts +3 -5
- package/src/lib/shared/toTagSet.ts +3 -3
- package/src/lib/shared/types/CacheEntry.ts +15 -0
- package/src/lib/shared/types/CacheOptions.ts +5 -5
- package/src/lib/shared/types/CacheSnapshotEntry.ts +5 -0
- package/src/lib/shared/types/DebugGate.ts +10 -0
- package/src/lib/shared/types/ErrorSpec.ts +6 -6
- package/src/lib/shared/types/RemoteFunction.ts +8 -4
- package/src/lib/shared/types/ResolverSlot.ts +10 -0
- package/src/lib/shared/writeDts.ts +8 -1
- package/src/lib/shared/writePublicAssetsDts.ts +5 -9
- package/src/lib/shared/writeRoutesDts.ts +12 -16
- package/src/lib/shared/writeRpcDts.ts +13 -12
- package/src/lib/shared/writeTestRpcDts.ts +11 -11
- package/src/lib/shared/writeTestSocketsDts.ts +9 -9
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +1 -0
- package/src/lib/ui/compile/abideUiPlugin.ts +9 -7
- package/src/lib/ui/compile/assertRuntimeHelpersBound.ts +24 -12
- package/src/lib/ui/compile/compileModule.ts +37 -16
- package/src/lib/ui/compile/compileShadow.ts +75 -1
- package/src/lib/ui/compile/createShadowLanguageService.ts +29 -3
- package/src/lib/ui/compile/generateBuild.ts +7 -1
- package/src/lib/ui/compile/generateSSR.ts +10 -3
- package/src/lib/ui/compile/parseTemplate.ts +4 -1
- package/src/lib/ui/compile/types/TemplateNode.ts +1 -1
- package/src/lib/ui/createScope.ts +11 -4
- package/src/lib/ui/deferResume.ts +29 -0
- package/src/lib/ui/dom/appendText.ts +19 -4
- package/src/lib/ui/dom/awaitBlock.ts +74 -26
- package/src/lib/ui/dom/eachAsync.ts +11 -18
- package/src/lib/ui/dom/mountSwappableRange.ts +79 -0
- package/src/lib/ui/dom/skeleton.ts +10 -1
- package/src/lib/ui/dom/switchBlock.ts +15 -63
- package/src/lib/ui/dom/when.ts +11 -64
- package/src/lib/ui/installHotBridge.ts +2 -0
- package/src/lib/ui/matchRoute.ts +18 -1
- package/src/lib/ui/remoteProxy.ts +11 -1
- package/src/lib/ui/renderToStream.ts +13 -8
- package/src/lib/ui/resumeSeedScript.ts +2 -2
- package/src/lib/ui/runtime/CURRENT_SCOPE.ts +14 -1
- package/src/lib/ui/runtime/RESUME.ts +6 -0
- package/src/lib/ui/runtime/ambientScopeBacking.ts +26 -0
- package/src/lib/ui/runtime/createDoc.ts +102 -12
- package/src/lib/ui/runtime/createEffectNode.ts +7 -1
- package/src/lib/ui/runtime/flushEffects.ts +18 -0
- package/src/lib/ui/runtime/generationGuard.ts +40 -0
- package/src/lib/ui/runtime/runNode.ts +9 -1
- package/src/lib/ui/runtime/types/SsrRender.ts +4 -3
- package/src/lib/ui/scope.ts +6 -8
- package/src/lib/ui/tryEncodeResume.ts +5 -2
- package/src/lib/ui/types/Scope.ts +3 -4
- package/src/zodCjsPlugin.ts +16 -1
- package/src/lib/server/rpc/buildErrorConstructors.ts +0 -19
- package/src/lib/shared/matchesDebugPattern.ts +0 -16
- package/src/lib/shared/parseDebugPatterns.ts +0 -21
- package/src/lib/shared/types/ErrorConstructors.ts +0 -17
- package/src/lib/shared/types/ErrorDescriptor.ts +0 -10
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { effect } from '../effect.ts'
|
|
2
2
|
import { claimChild } from '../runtime/claimChild.ts'
|
|
3
|
-
import {
|
|
3
|
+
import { generationGuard } from '../runtime/generationGuard.ts'
|
|
4
4
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
5
5
|
import { scopeGroup } from '../runtime/scopeGroup.ts'
|
|
6
6
|
import type { State } from '../runtime/types/State.ts'
|
|
@@ -74,12 +74,16 @@ export function eachAsync<T>(
|
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
/* Bumped each run so a superseded drain stops appending and pruning. */
|
|
78
|
-
let generation = 0
|
|
79
77
|
let iterator: AsyncIterator<T> | undefined
|
|
78
|
+
/* Bumped each run so a superseded drain stops appending and pruning, and on owner teardown
|
|
79
|
+
— which also `return()`s the live iterator to release the source (rows and the error
|
|
80
|
+
branch are disposed by the group, whose scopes were tracked). */
|
|
81
|
+
const guard = generationGuard(() => {
|
|
82
|
+
iterator?.return?.(undefined)?.catch(() => undefined)
|
|
83
|
+
iterator = undefined
|
|
84
|
+
})
|
|
80
85
|
effect(() => {
|
|
81
|
-
|
|
82
|
-
const generationAtStart = generation
|
|
86
|
+
const generationAtStart = guard.renew()
|
|
83
87
|
iterator?.return?.(undefined)?.catch(() => undefined) // close the superseded run's iterator before re-streaming
|
|
84
88
|
iterator = undefined
|
|
85
89
|
clearError() // a fresh run drops a prior error branch
|
|
@@ -92,7 +96,7 @@ export function eachAsync<T>(
|
|
|
92
96
|
while (true) {
|
|
93
97
|
const result = await active.next()
|
|
94
98
|
/* A re-run or teardown bumped the generation while we awaited. */
|
|
95
|
-
if (generationAtStart
|
|
99
|
+
if (!guard.live(generationAtStart)) {
|
|
96
100
|
return
|
|
97
101
|
}
|
|
98
102
|
if (result.done === true) {
|
|
@@ -126,7 +130,7 @@ export function eachAsync<T>(
|
|
|
126
130
|
}
|
|
127
131
|
}
|
|
128
132
|
drain().catch((error: unknown) => {
|
|
129
|
-
if (generationAtStart
|
|
133
|
+
if (!guard.live(generationAtStart)) {
|
|
130
134
|
return
|
|
131
135
|
}
|
|
132
136
|
/* No catch branch → surface the rejection (mirrors `<template await>`). */
|
|
@@ -137,15 +141,4 @@ export function eachAsync<T>(
|
|
|
137
141
|
errorRange = insertRange((host) => renderCatch(host, error))
|
|
138
142
|
})
|
|
139
143
|
})
|
|
140
|
-
|
|
141
|
-
/* Stop the live stream when the enclosing scope tears down: bump the generation so
|
|
142
|
-
the drain abandons its loop and `return()` the iterator to release the source. The
|
|
143
|
-
rows and error branch are disposed by the group (their scopes were tracked). */
|
|
144
|
-
if (OWNER.current !== undefined) {
|
|
145
|
-
OWNER.current.push(() => {
|
|
146
|
-
generation += 1
|
|
147
|
-
iterator?.return?.(undefined)?.catch(() => undefined)
|
|
148
|
-
iterator = undefined
|
|
149
|
-
})
|
|
150
|
-
}
|
|
151
144
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { effect } from '../effect.ts'
|
|
2
|
+
import { RANGE_CLOSE, RANGE_OPEN } from '../runtime/RANGE_MARKER.ts'
|
|
3
|
+
import { RENDER } from '../runtime/RENDER.ts'
|
|
4
|
+
import { scope } from '../runtime/scope.ts'
|
|
5
|
+
import { scopeGroup } from '../runtime/scopeGroup.ts'
|
|
6
|
+
import { fillBefore } from './fillBefore.ts'
|
|
7
|
+
import { openMarker } from './openMarker.ts'
|
|
8
|
+
import { replaceRange } from './replaceRange.ts'
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
The shared lifecycle of every single-slot control-flow block (`when`, `switchBlock`):
|
|
12
|
+
a marker-bounded RANGE that holds at most one piece of content at a time, swapped as a
|
|
13
|
+
unit when a `key` changes. The block supplies a `key()` thunk (the identity of the
|
|
14
|
+
chosen branch — `'then'|'else'` for `when`, the case index for `switchBlock`) and a
|
|
15
|
+
`contentFor(key)` resolver returning that branch's builder, or `undefined` for an
|
|
16
|
+
empty branch. This module owns the marker setup, the hydrate-vs-create fork, and the
|
|
17
|
+
swap effect with its reentrancy/teardown dance — the structure both blocks copy-pasted.
|
|
18
|
+
|
|
19
|
+
The chosen branch builds through `scope` (directly on hydrate, via `fillBefore` on a
|
|
20
|
+
swap), which builds untracked — so a raw reactive read in the branch content doesn't
|
|
21
|
+
subscribe the swap effect; only `key()` (which reads the block's source) drives the
|
|
22
|
+
toggle. The branch's own interpolations still track, each through its own effect.
|
|
23
|
+
|
|
24
|
+
On hydrate it adopts the server-rendered range: claim the start marker, run the chosen
|
|
25
|
+
branch in place (its content claims the existing nodes), then claim the end marker. The
|
|
26
|
+
effect's first run sees the same key and is a no-op; later changes clear the range and
|
|
27
|
+
build fresh into a fragment.
|
|
28
|
+
|
|
29
|
+
`before` (a static node located by the skeleton) places the range among siblings on
|
|
30
|
+
create, so the block sits before a static suffix rather than at the parent's end.
|
|
31
|
+
Hydrate ignores it — the claim cursor (positioned past the prefix) drives placement.
|
|
32
|
+
*/
|
|
33
|
+
export function mountSwappableRange<Key>(
|
|
34
|
+
parent: Node,
|
|
35
|
+
key: () => Key,
|
|
36
|
+
contentFor: (key: Key) => ((parent: Node) => void) | undefined,
|
|
37
|
+
before: Node | null = null,
|
|
38
|
+
): void {
|
|
39
|
+
const hydration = RENDER.hydration
|
|
40
|
+
/* The live branch's scope, registered with the owner so it disposes on owner
|
|
41
|
+
teardown — not only on a branch swap via replaceRange. */
|
|
42
|
+
const group = scopeGroup()
|
|
43
|
+
let dispose: (() => void) | undefined
|
|
44
|
+
let activeKey: Key
|
|
45
|
+
let end: Comment
|
|
46
|
+
|
|
47
|
+
const start = openMarker(parent, RANGE_OPEN, before)
|
|
48
|
+
if (hydration !== undefined) {
|
|
49
|
+
activeKey = key()
|
|
50
|
+
const chosen = contentFor(activeKey)
|
|
51
|
+
if (chosen !== undefined) {
|
|
52
|
+
dispose = group.track(scope(() => chosen(parent))) // content claims the SSR nodes in place
|
|
53
|
+
}
|
|
54
|
+
end = openMarker(parent, RANGE_CLOSE)
|
|
55
|
+
} else {
|
|
56
|
+
end = openMarker(parent, RANGE_CLOSE, before)
|
|
57
|
+
activeKey = key()
|
|
58
|
+
const chosen = contentFor(activeKey)
|
|
59
|
+
if (chosen !== undefined) {
|
|
60
|
+
dispose = group.track(fillBefore(end, chosen))
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
effect(() => {
|
|
65
|
+
const next = key()
|
|
66
|
+
if (next === activeKey) {
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
activeKey = next
|
|
70
|
+
const chosen = contentFor(next)
|
|
71
|
+
/* Null `dispose` before `replaceRange` builds the new branch: a reentrant swap
|
|
72
|
+
during that build (an effect in the new content writing the source) would
|
|
73
|
+
otherwise re-enter with the already-disposed disposer and clear it twice. */
|
|
74
|
+
const prior = dispose
|
|
75
|
+
dispose = undefined
|
|
76
|
+
const built = replaceRange(start, end, prior, chosen)
|
|
77
|
+
dispose = built !== undefined ? group.track(built) : undefined
|
|
78
|
+
})
|
|
79
|
+
}
|
|
@@ -129,10 +129,19 @@ export function skeleton(parent: Node, html: string): SkeletonHoles {
|
|
|
129
129
|
hydration.next.set(parent, node)
|
|
130
130
|
} else {
|
|
131
131
|
const children = source.childNodes
|
|
132
|
+
/* Stage clones in a fragment ONLY for a live (connected) parent, where one
|
|
133
|
+
append reflows once instead of per clone. A detached parent triggers no
|
|
134
|
+
reflow on append, so the fragment is pure overhead there — skip it and
|
|
135
|
+
append direct. `topLevel` collects each clone either way for anchor/
|
|
136
|
+
element-hole resolution. */
|
|
137
|
+
const target: Node = parent.isConnected ? document.createDocumentFragment() : parent
|
|
132
138
|
for (let index = 0; index < children.length; index += 1) {
|
|
133
139
|
const clone = (children[index] as Node).cloneNode(true)
|
|
134
140
|
topLevel.push(clone)
|
|
135
|
-
|
|
141
|
+
target.appendChild(clone)
|
|
142
|
+
}
|
|
143
|
+
if (target !== parent) {
|
|
144
|
+
parent.appendChild(target)
|
|
136
145
|
}
|
|
137
146
|
}
|
|
138
147
|
/* Anchor holes via the ONE shared ordering rule (`walkAnchorOrder`) — the same traversal
|
|
@@ -1,26 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { RANGE_CLOSE, RANGE_OPEN } from '../runtime/RANGE_MARKER.ts'
|
|
3
|
-
import { RENDER } from '../runtime/RENDER.ts'
|
|
4
|
-
import { scope } from '../runtime/scope.ts'
|
|
5
|
-
import { scopeGroup } from '../runtime/scopeGroup.ts'
|
|
6
|
-
import { fillBefore } from './fillBefore.ts'
|
|
7
|
-
import { openMarker } from './openMarker.ts'
|
|
8
|
-
import { replaceRange } from './replaceRange.ts'
|
|
1
|
+
import { mountSwappableRange } from './mountSwappableRange.ts'
|
|
9
2
|
import type { SwitchCase } from './types/SwitchCase.ts'
|
|
10
3
|
|
|
11
4
|
/*
|
|
12
5
|
Multi-branch binding — the runtime for `{#switch}` blocks and for `{#if}` chains that
|
|
13
6
|
include `{:elseif}` branches (compiled as a switch over `true` with Boolean-coerced
|
|
14
|
-
match thunks so the first truthy branch wins).
|
|
7
|
+
match thunks so the first truthy branch wins). A swappable range evaluates the
|
|
15
8
|
subject, picks the first case whose `match` equals it (strict `===`), falling back
|
|
16
9
|
to the default (`match` undefined); the chosen case's content lives in a RANGE
|
|
17
10
|
bounded by two comment markers, so a case holds any content. Staying on the same
|
|
18
11
|
case across a subject change leaves it mounted; switching clears the range and
|
|
19
|
-
builds the new case fresh.
|
|
20
|
-
|
|
21
|
-
On hydrate it adopts the case the server rendered: claim the start marker, run the
|
|
22
|
-
matching case in place, claim the end marker. The effect's first run picks the same
|
|
23
|
-
case and is a no-op; later changes swap the range.
|
|
12
|
+
builds the new case fresh. See `mountSwappableRange` for the shared
|
|
13
|
+
hydrate/swap/teardown semantics — the case index is the swap key.
|
|
24
14
|
*/
|
|
25
15
|
// @documentation plumbing
|
|
26
16
|
export function switchBlock(
|
|
@@ -29,59 +19,21 @@ export function switchBlock(
|
|
|
29
19
|
cases: SwitchCase[],
|
|
30
20
|
before: Node | null = null,
|
|
31
21
|
): void {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
teardown — not only when the subject switches cases via replaceRange. */
|
|
35
|
-
const group = scopeGroup()
|
|
36
|
-
let dispose: (() => void) | undefined
|
|
37
|
-
let activeIndex: number
|
|
38
|
-
let end: Comment
|
|
39
|
-
|
|
22
|
+
/* Pick the first case matching the subject (`===`), else the default (`match`
|
|
23
|
+
undefined), else -1 for no match. */
|
|
40
24
|
const select = (value: unknown): number => {
|
|
41
25
|
const matched = cases.findIndex(
|
|
42
26
|
(entry) => entry.match !== undefined && entry.match() === value,
|
|
43
27
|
)
|
|
44
28
|
return matched === -1 ? cases.findIndex((entry) => entry.match === undefined) : matched
|
|
45
29
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const start = openMarker(parent, RANGE_OPEN, before)
|
|
56
|
-
if (hydration !== undefined) {
|
|
57
|
-
activeIndex = select(subject())
|
|
58
|
-
const chosen = caseAt(activeIndex)
|
|
59
|
-
if (chosen !== undefined) {
|
|
60
|
-
dispose = group.track(scope(() => chosen.render(parent))) // claim the SSR nodes in place
|
|
61
|
-
}
|
|
62
|
-
end = openMarker(parent, RANGE_CLOSE)
|
|
63
|
-
} else {
|
|
64
|
-
end = openMarker(parent, RANGE_CLOSE, before)
|
|
65
|
-
activeIndex = select(subject())
|
|
66
|
-
const chosen = caseAt(activeIndex)
|
|
67
|
-
if (chosen !== undefined) {
|
|
68
|
-
dispose = group.track(fillBefore(end, (p) => chosen.render(p)))
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
effect(() => {
|
|
73
|
-
const index = select(subject())
|
|
74
|
-
if (index === activeIndex) {
|
|
75
|
-
return
|
|
76
|
-
}
|
|
77
|
-
activeIndex = index
|
|
78
|
-
const chosen = caseAt(index)
|
|
79
|
-
/* Null `dispose` before `replaceRange` builds the new case: a reentrant switch
|
|
80
|
-
during that build (an effect in the new content writing the subject) would
|
|
81
|
-
otherwise re-enter with the already-disposed disposer and clear it twice. */
|
|
82
|
-
const prior = dispose
|
|
83
|
-
dispose = undefined
|
|
84
|
-
const next = replaceRange(start, end, prior, chosen && ((p) => chosen.render(p)))
|
|
85
|
-
dispose = next !== undefined ? group.track(next) : undefined
|
|
86
|
-
})
|
|
30
|
+
mountSwappableRange(
|
|
31
|
+
parent,
|
|
32
|
+
() => select(subject()),
|
|
33
|
+
(index) => {
|
|
34
|
+
const chosen = index === -1 ? undefined : cases[index]
|
|
35
|
+
return chosen && ((p) => chosen.render(p))
|
|
36
|
+
},
|
|
37
|
+
before,
|
|
38
|
+
)
|
|
87
39
|
}
|
package/src/lib/ui/dom/when.ts
CHANGED
|
@@ -1,24 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { RANGE_CLOSE, RANGE_OPEN } from '../runtime/RANGE_MARKER.ts'
|
|
3
|
-
import { RENDER } from '../runtime/RENDER.ts'
|
|
4
|
-
import { scope } from '../runtime/scope.ts'
|
|
5
|
-
import { scopeGroup } from '../runtime/scopeGroup.ts'
|
|
6
|
-
import { fillBefore } from './fillBefore.ts'
|
|
7
|
-
import { openMarker } from './openMarker.ts'
|
|
8
|
-
import { replaceRange } from './replaceRange.ts'
|
|
1
|
+
import { mountSwappableRange } from './mountSwappableRange.ts'
|
|
9
2
|
|
|
10
3
|
/*
|
|
11
4
|
Conditional binding — the runtime for `<template if>` (with optional `else`). The
|
|
12
5
|
branch's content lives in a RANGE bounded by two comment markers, so a branch may
|
|
13
6
|
hold anything — elements, components, text, nested control-flow, snippets — not
|
|
14
|
-
just element roots.
|
|
15
|
-
on a truthy↔falsy flip (`render` truthy, `renderElse` falsy); an
|
|
16
|
-
condition is a no-op.
|
|
17
|
-
|
|
18
|
-
On hydrate it adopts the server-rendered range: claim the start marker, run the
|
|
19
|
-
matching render in place (its content claims the existing nodes), then claim the
|
|
20
|
-
end marker. The effect's first run sees the same branch and is a no-op; later
|
|
21
|
-
toggles clear the range and build fresh into a fragment.
|
|
7
|
+
just element roots. A 2-case swappable range tracks `condition()` and swaps the
|
|
8
|
+
range's content on a truthy↔falsy flip (`render` truthy, `renderElse` falsy); an
|
|
9
|
+
unchanged condition is a no-op. See `mountSwappableRange` for the shared
|
|
10
|
+
hydrate/swap/teardown semantics.
|
|
22
11
|
*/
|
|
23
12
|
// @documentation plumbing
|
|
24
13
|
export function when(
|
|
@@ -28,52 +17,10 @@ export function when(
|
|
|
28
17
|
renderElse?: (parent: Node) => void,
|
|
29
18
|
before: Node | null = null,
|
|
30
19
|
): void {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
/* The live branch's scope, registered with the owner so it disposes on owner
|
|
38
|
-
teardown — not only on a branch flip via replaceRange. */
|
|
39
|
-
const group = scopeGroup()
|
|
40
|
-
let dispose: (() => void) | undefined
|
|
41
|
-
let activeBranch: 'then' | 'else'
|
|
42
|
-
let end: Comment
|
|
43
|
-
|
|
44
|
-
/* `before` (a static node located by the skeleton) places the range among siblings on
|
|
45
|
-
create, so the block sits before a static suffix rather than at the parent's end.
|
|
46
|
-
Hydrate ignores it — the claim cursor (positioned past the prefix) drives placement. */
|
|
47
|
-
const start = openMarker(parent, RANGE_OPEN, before)
|
|
48
|
-
if (hydration !== undefined) {
|
|
49
|
-
activeBranch = condition() ? 'then' : 'else'
|
|
50
|
-
const chosen = chosenFor(activeBranch)
|
|
51
|
-
if (chosen !== undefined) {
|
|
52
|
-
dispose = group.track(scope(() => chosen(parent))) // content claims the SSR nodes in place
|
|
53
|
-
}
|
|
54
|
-
end = openMarker(parent, RANGE_CLOSE)
|
|
55
|
-
} else {
|
|
56
|
-
end = openMarker(parent, RANGE_CLOSE, before)
|
|
57
|
-
activeBranch = condition() ? 'then' : 'else'
|
|
58
|
-
const chosen = chosenFor(activeBranch)
|
|
59
|
-
if (chosen !== undefined) {
|
|
60
|
-
dispose = group.track(fillBefore(end, chosen))
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
effect(() => {
|
|
65
|
-
const branch = condition() ? 'then' : 'else'
|
|
66
|
-
if (branch === activeBranch) {
|
|
67
|
-
return
|
|
68
|
-
}
|
|
69
|
-
activeBranch = branch
|
|
70
|
-
const chosen = chosenFor(branch)
|
|
71
|
-
/* Null `dispose` before `replaceRange` builds the new branch: a reentrant flip
|
|
72
|
-
during that build (an effect in the new content writing the condition) would
|
|
73
|
-
otherwise re-enter with the already-disposed disposer and clear it twice. */
|
|
74
|
-
const prior = dispose
|
|
75
|
-
dispose = undefined
|
|
76
|
-
const next = replaceRange(start, end, prior, chosen)
|
|
77
|
-
dispose = next !== undefined ? group.track(next) : undefined
|
|
78
|
-
})
|
|
20
|
+
mountSwappableRange(
|
|
21
|
+
parent,
|
|
22
|
+
() => (condition() ? 'then' : 'else'),
|
|
23
|
+
(branch) => (branch === 'then' ? render : renderElse),
|
|
24
|
+
before,
|
|
25
|
+
)
|
|
79
26
|
}
|
|
@@ -25,6 +25,7 @@ import { spreadProps } from './dom/spreadProps.ts'
|
|
|
25
25
|
import { switchBlock } from './dom/switchBlock.ts'
|
|
26
26
|
import { tryBlock } from './dom/tryBlock.ts'
|
|
27
27
|
import { when } from './dom/when.ts'
|
|
28
|
+
import { deferResume } from './deferResume.ts'
|
|
28
29
|
import { effect } from './effect.ts'
|
|
29
30
|
import { enterScope } from './enterScope.ts'
|
|
30
31
|
import { exitScope } from './exitScope.ts'
|
|
@@ -68,6 +69,7 @@ export function installHotBridge(): void {
|
|
|
68
69
|
eachAsync,
|
|
69
70
|
when,
|
|
70
71
|
awaitBlock,
|
|
72
|
+
deferResume,
|
|
71
73
|
tryBlock,
|
|
72
74
|
switchBlock,
|
|
73
75
|
mountSlot,
|
package/src/lib/ui/matchRoute.ts
CHANGED
|
@@ -52,6 +52,18 @@ export function matchRoute(
|
|
|
52
52
|
return best === undefined ? undefined : { route: best.route, params: best.params }
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/* Percent-decodes a captured `[name]` value. Bun's `req.params` decoding is
|
|
56
|
+
lenient (malformed sequences pass through), so mirror that by falling back to
|
|
57
|
+
the raw value rather than throwing on a malformed `%` a page navigation would
|
|
58
|
+
otherwise crash on. */
|
|
59
|
+
function decodeParam(value: string): string {
|
|
60
|
+
try {
|
|
61
|
+
return decodeURIComponent(value)
|
|
62
|
+
} catch {
|
|
63
|
+
return value
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
55
67
|
/* Matches one parsed pattern against the path's segments, capturing params;
|
|
56
68
|
undefined on mismatch. A catch-all consumes every remaining segment. */
|
|
57
69
|
function matchSegments(
|
|
@@ -81,7 +93,12 @@ function matchSegments(
|
|
|
81
93
|
if (value === '') {
|
|
82
94
|
return undefined
|
|
83
95
|
}
|
|
84
|
-
|
|
96
|
+
/* `url()` encodes a `[name]` value whole, and Bun decodes `req.params`
|
|
97
|
+
server-side, so decode here to hand the page the same value SSR does
|
|
98
|
+
(e.g. `The%20Daily%20Show` → `The Daily Show`). The catch-all above
|
|
99
|
+
stays raw to match the server, which reconstructs it from the raw
|
|
100
|
+
pathname. */
|
|
101
|
+
params[segment.name] = decodeParam(value)
|
|
85
102
|
}
|
|
86
103
|
}
|
|
87
104
|
/* No catch-all consumed the tail, so the path must have no extra segments. */
|
|
@@ -53,11 +53,21 @@ HttpError; and once a backlog exists, a fresh call parks straight to the TAIL
|
|
|
53
53
|
the app owns when to replay. `rpc.outbox()` exposes the queue.
|
|
54
54
|
*/
|
|
55
55
|
// @documentation plumbing
|
|
56
|
+
export function remoteProxy<Args, Return>(
|
|
57
|
+
method: HttpMethod,
|
|
58
|
+
url: string,
|
|
59
|
+
durable: DurableOptions & { outbox: true },
|
|
60
|
+
): RemoteFunction<Args, Return, Record<never, never>, true>
|
|
61
|
+
export function remoteProxy<Args, Return>(
|
|
62
|
+
method: HttpMethod,
|
|
63
|
+
url: string,
|
|
64
|
+
durable?: DurableOptions,
|
|
65
|
+
): RemoteFunction<Args, Return>
|
|
56
66
|
export function remoteProxy<Args, Return>(
|
|
57
67
|
method: HttpMethod,
|
|
58
68
|
url: string,
|
|
59
69
|
durable?: DurableOptions,
|
|
60
|
-
): RemoteFunction<Args, Return> {
|
|
70
|
+
): RemoteFunction<Args, Return, Record<never, never>, boolean> {
|
|
61
71
|
/* Assigned after `createRemoteFunction` so the invoke closure (which runs later, per
|
|
62
72
|
call) parks through the shared queue; undefined leaves the plain fetch path. */
|
|
63
73
|
let queue: OutboxQueue<Args> | undefined
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { deferResume } from './deferResume.ts'
|
|
1
2
|
import { resumeSeedScript } from './resumeSeedScript.ts'
|
|
2
|
-
import type { ResumeEntry } from './runtime/RESUME.ts'
|
|
3
|
+
import type { DeferMarker, ResumeEntry } from './runtime/RESUME.ts'
|
|
3
4
|
import type { SsrAwait, SsrRender } from './runtime/types/SsrRender.ts'
|
|
4
5
|
import { tryEncodeResume } from './tryEncodeResume.ts'
|
|
5
6
|
|
|
@@ -39,8 +40,8 @@ export async function* renderToStream(
|
|
|
39
40
|
instead of refetching. (`resume` is the render body's live object, so late writes
|
|
40
41
|
appear here.) */
|
|
41
42
|
const seededResume = new Set<number>(Object.keys(resume).map(Number))
|
|
42
|
-
const resumeDelta = (): Record<number, ResumeEntry> => {
|
|
43
|
-
const delta: Record<number, ResumeEntry> = {}
|
|
43
|
+
const resumeDelta = (): Record<number, ResumeEntry | DeferMarker> => {
|
|
44
|
+
const delta: Record<number, ResumeEntry | DeferMarker> = {}
|
|
44
45
|
for (const [key, entry] of Object.entries(resume)) {
|
|
45
46
|
const id = Number(key)
|
|
46
47
|
if (!seededResume.has(id)) {
|
|
@@ -83,18 +84,22 @@ export async function* renderToStream(
|
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
type Settled = { id: number; html: string; resume: ResumeEntry }
|
|
87
|
+
type Settled = { id: number; html: string; resume: ResumeEntry | DeferMarker }
|
|
87
88
|
|
|
88
89
|
/* Awaits one streaming block's promise and renders the resolved or error branch to
|
|
89
90
|
HTML (the renderers are async so a nested `await` block composes), capturing the
|
|
90
91
|
value (serializable) for the resume manifest. Errors serialize as their message —
|
|
91
|
-
enough for the catch branch, without leaking a stack.
|
|
92
|
+
enough for the catch branch, without leaking a stack. A cache-backed value defers via
|
|
93
|
+
`deferResume` (a `{defer,key}` marker + lazy body seed) — the client adopts the streamed
|
|
94
|
+
branch inert instead of decoding the value; a non-cache value ships inline as before. */
|
|
92
95
|
function settle(block: SsrAwait): Promise<Settled> {
|
|
93
|
-
|
|
96
|
+
/* Keep the promise: cache() tagged it with its key, which `deferResume` reads to defer. */
|
|
97
|
+
const pending = block.promise()
|
|
98
|
+
return Promise.resolve(pending).then(
|
|
94
99
|
async (value) => ({
|
|
95
100
|
id: block.id,
|
|
96
101
|
html: await block.then(value),
|
|
97
|
-
resume:
|
|
102
|
+
resume: deferResume(pending, value),
|
|
98
103
|
}),
|
|
99
104
|
async (error) => {
|
|
100
105
|
/* No catch branch → surface the rejection (500 before the first flush,
|
|
@@ -118,6 +123,6 @@ function settle(block: SsrAwait): Promise<Settled> {
|
|
|
118
123
|
strings. `tryEncodeResume` handles the serialize-or-refetch policy (undefined → no
|
|
119
124
|
script → the swap consumers skip registration → hydration re-runs that one promise).
|
|
120
125
|
`applyResolved`/the inline swap script store it via `.textContent`; `awaitBlock` decodes it. */
|
|
121
|
-
function encodeStreamResume(resume: ResumeEntry, id: number): string | undefined {
|
|
126
|
+
function encodeStreamResume(resume: ResumeEntry | DeferMarker, id: number): string | undefined {
|
|
122
127
|
return tryEncodeResume(resume, id)?.replace(/</g, '\\u003c')
|
|
123
128
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { safeJsonForScript } from '../shared/safeJsonForScript.ts'
|
|
2
|
-
import type { ResumeEntry } from './runtime/RESUME.ts'
|
|
2
|
+
import type { DeferMarker, ResumeEntry } from './runtime/RESUME.ts'
|
|
3
3
|
import { tryEncodeResume } from './tryEncodeResume.ts'
|
|
4
4
|
|
|
5
5
|
/* A self-contained `<script>` seeding the await-resume manifest with the blocking
|
|
@@ -11,7 +11,7 @@ import { tryEncodeResume } from './tryEncodeResume.ts'
|
|
|
11
11
|
early or parse as a line terminator. Shared by the buffered (`createUiPageRenderer`)
|
|
12
12
|
and streaming (`renderToStream`) paths. */
|
|
13
13
|
// @documentation plumbing
|
|
14
|
-
export function resumeSeedScript(resume: Record<number, ResumeEntry>): string {
|
|
14
|
+
export function resumeSeedScript(resume: Record<number, ResumeEntry | DeferMarker>): string {
|
|
15
15
|
/* ref-json (not JSON) so a value carrying cycles or shared back-references — a
|
|
16
16
|
media tree with parent↔child links — seeds instead of being dropped. `tryEncodeResume`
|
|
17
17
|
drops just an unserializable entry (the client re-runs that one branch's promise),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Scope } from '../types/Scope.ts'
|
|
2
|
+
import { ambientScopeBacking } from './ambientScopeBacking.ts'
|
|
2
3
|
|
|
3
4
|
/*
|
|
4
5
|
The ambient lexical scope. The compiler establishes one per lexical level (a
|
|
@@ -6,5 +7,17 @@ component, a control-flow branch) by setting `current` around the build, so the
|
|
|
6
7
|
bare `scope()` accessor and the scope-bound primitives resolve "where they are"
|
|
7
8
|
with no handle threaded. Undefined outside any scope, where `scope()` mints a
|
|
8
9
|
detached root on first use.
|
|
10
|
+
|
|
11
|
+
`current` reads/writes through a SWAPPABLE backing (`ambientScopeBacking`) rather
|
|
12
|
+
than a raw field: the default is a module variable, but the server installs an
|
|
13
|
+
AsyncLocalStorage-backed holder so concurrent async SSR renders don't clobber one
|
|
14
|
+
shared global across the inline `await`s they suspend on. See `ambientScopeBacking`.
|
|
9
15
|
*/
|
|
10
|
-
export const CURRENT_SCOPE: { current: Scope | undefined } = {
|
|
16
|
+
export const CURRENT_SCOPE: { current: Scope | undefined } = {
|
|
17
|
+
get current(): Scope | undefined {
|
|
18
|
+
return ambientScopeBacking.active.get()
|
|
19
|
+
},
|
|
20
|
+
set current(value: Scope | undefined) {
|
|
21
|
+
ambientScopeBacking.active.set(value)
|
|
22
|
+
},
|
|
23
|
+
}
|
|
@@ -16,6 +16,12 @@
|
|
|
16
16
|
same reference. */
|
|
17
17
|
export type ResumeEntry = { ok: true; value: unknown } | { ok: false; error: unknown }
|
|
18
18
|
|
|
19
|
+
/* Deferred-resume marker (Tier 2): a large cache-backed `{#await cache()}` ships this in
|
|
20
|
+
place of its value — just the cache key, so hydration adopts the server branch inert and
|
|
21
|
+
pays no value decode. The client seeds that key lazily and materializes it only on a later
|
|
22
|
+
re-read. Discriminated from a ResumeEntry by the `defer` field. */
|
|
23
|
+
export type DeferMarker = { defer: true; key: string }
|
|
24
|
+
|
|
19
25
|
const globalScope = globalThis as { __abideResume?: Record<number, string> }
|
|
20
26
|
globalScope.__abideResume ??= {}
|
|
21
27
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Scope } from '../types/Scope.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
The swappable backing behind `CURRENT_SCOPE.current`. The default is a module
|
|
5
|
+
variable — correct on the client (one render tree) and on the server outside any
|
|
6
|
+
request. The server swaps in an AsyncLocalStorage-backed holder at boot
|
|
7
|
+
(`installAmbientScopeStore`), because SSR is partly async: a render brackets itself
|
|
8
|
+
with `enterScope`/`exitScope` and `await`s inline between them (blocking `{#await}`,
|
|
9
|
+
child renders, slots, a top-level `await`). A module global held across those awaits
|
|
10
|
+
interleaves across concurrent requests — one render resumes to read another's scope.
|
|
11
|
+
Keying the ambient off the per-request store (which the async context propagates
|
|
12
|
+
correctly) isolates it. Mirrors `requestScopeResolver`'s server-installed slot; the
|
|
13
|
+
indirection cost lands at build/effect-creation time, never on the signal hot path.
|
|
14
|
+
*/
|
|
15
|
+
type ScopeBacking = { get(): Scope | undefined; set(value: Scope | undefined): void }
|
|
16
|
+
|
|
17
|
+
let moduleCurrent: Scope | undefined
|
|
18
|
+
|
|
19
|
+
export const ambientScopeBacking: { active: ScopeBacking } = {
|
|
20
|
+
active: {
|
|
21
|
+
get: () => moduleCurrent,
|
|
22
|
+
set: (value) => {
|
|
23
|
+
moduleCurrent = value
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
}
|