@abide/abide 0.43.1 → 0.44.1
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 +293 -219
- package/CHANGELOG.md +63 -0
- package/README.md +191 -67
- package/package.json +2 -2
- package/src/abideLsp.ts +8 -4
- package/src/abideResolverPlugin.ts +17 -2
- package/src/build.ts +9 -0
- package/src/buildDisconnected.ts +2 -0
- package/src/devEntry.ts +44 -9
- package/src/lib/bundle/probeAbideServer.ts +1 -1
- package/src/lib/cli/parseArgvForRpc.ts +25 -26
- package/src/lib/cli/runCli.ts +9 -24
- package/src/lib/cli/tokenizeArgvFlags.ts +84 -0
- package/src/lib/cli/types/CliManifest.ts +6 -4
- package/src/lib/mcp/annotationsForMethod.ts +1 -1
- package/src/lib/mcp/toolResultFromResponse.ts +1 -1
- package/src/lib/mcp/types/JsonRpcRequest.ts +3 -3
- package/src/lib/server/AppModule.ts +1 -1
- package/src/lib/server/cli/handleCliDownload.ts +7 -5
- package/src/lib/server/cli/installScript.ts +3 -3
- package/src/lib/server/prompts/promptRegistry.ts +1 -1
- package/src/lib/server/prompts/types/Prompt.ts +2 -1
- package/src/lib/server/rpc/defineRpc.ts +9 -1
- package/src/lib/server/rpc/dispatchRpcInProcess.ts +2 -2
- package/src/lib/server/rpc/types/RpcHelper.ts +1 -1
- package/src/lib/server/runtime/createPublicAssetServer.ts +2 -2
- package/src/lib/server/runtime/createRouteDispatcher.ts +1 -1
- package/src/lib/server/runtime/createServer.ts +50 -12
- package/src/lib/server/runtime/createUiPageRenderer.ts +7 -11
- package/src/lib/server/runtime/crossOriginForbidden.ts +2 -2
- package/src/lib/server/runtime/crossOriginGate.ts +1 -1
- package/src/lib/server/runtime/devHotModuleResponse.ts +3 -3
- package/src/lib/server/runtime/disableIdleTimeoutForStream.ts +1 -1
- package/src/lib/server/runtime/gzipResponse.ts +14 -16
- package/src/lib/server/runtime/logExposedSurfaces.ts +3 -4
- package/src/lib/server/runtime/runWithRequestScope.ts +4 -1
- package/src/lib/server/runtime/serializeCacheSnapshot.ts +3 -8
- package/src/lib/server/runtime/snapshotEntryFromCache.ts +10 -7
- package/src/lib/server/runtime/streamFromIterator.ts +2 -2
- package/src/lib/server/runtime/types/InspectorRpc.ts +2 -2
- package/src/lib/server/runtime/types/RequestStore.ts +16 -0
- package/src/lib/server/runtime/withResponseDefaults.ts +8 -3
- package/src/lib/server/socket.ts +1 -1
- package/src/lib/server/sockets/createSocketDispatcher.ts +2 -2
- package/src/lib/server/sockets/defineSocket.ts +5 -1
- package/src/lib/server/sockets/socketRegistry.ts +2 -2
- package/src/lib/server/sockets/types/SocketClientFrame.ts +3 -3
- package/src/lib/shared/CACHE_WRAPPED.ts +3 -2
- package/src/lib/shared/REMOTE_FUNCTION.ts +1 -1
- package/src/lib/shared/RPC_ARGS_TYPE.ts +7 -0
- package/src/lib/shared/STREAMING_CONTENT_TYPES.ts +3 -2
- 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/bodyValueForKind.ts +35 -0
- package/src/lib/shared/cache.ts +22 -2
- package/src/lib/shared/cacheEntryFromSnapshot.ts +23 -18
- 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/createPushIterator.ts +1 -1
- package/src/lib/shared/createResolverSlot.ts +37 -0
- package/src/lib/shared/debugGate.ts +90 -0
- package/src/lib/shared/decodeResponse.ts +11 -8
- 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/hasReplayableRequest.ts +17 -0
- package/src/lib/shared/html.ts +7 -26
- package/src/lib/shared/isDebugEnabled.ts +3 -10
- package/src/lib/shared/isDebugNegated.ts +4 -6
- package/src/lib/shared/manifestModule.ts +2 -2
- package/src/lib/shared/online.ts +1 -1
- package/src/lib/shared/outboxProbeSlot.ts +1 -1
- package/src/lib/shared/pageResolver.ts +16 -0
- package/src/lib/shared/pageSlot.ts +5 -14
- package/src/lib/shared/pageUrlForFile.ts +1 -1
- 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/routeParamsShape.ts +20 -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/snapshotShippable.ts +22 -0
- package/src/lib/shared/types/CacheEntry.ts +1 -1
- package/src/lib/shared/types/CacheInvalidation.ts +3 -3
- package/src/lib/shared/types/ClientFlags.ts +5 -3
- package/src/lib/shared/types/DebugGate.ts +10 -0
- package/src/lib/shared/types/LastConnection.ts +1 -1
- package/src/lib/shared/types/ResolverSlot.ts +10 -0
- package/src/lib/shared/url.ts +4 -1
- package/src/lib/shared/withBase.ts +1 -2
- package/src/lib/shared/writeDts.ts +10 -3
- package/src/lib/shared/writePublicAssetsDts.ts +5 -9
- package/src/lib/shared/writeRoutesDts.ts +14 -46
- 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/test/createTestSocketChannel.ts +3 -3
- package/src/lib/ui/COMPONENT_WRAPPER_PREFIX.ts +1 -1
- package/src/lib/ui/compile/ABIDE_SEMANTIC_TOKENS_LEGEND.ts +40 -6
- package/src/lib/ui/compile/REACTIVE_CALLEES.ts +1 -1
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +49 -37
- package/src/lib/ui/compile/abideUiPlugin.ts +10 -8
- package/src/lib/ui/compile/assertRuntimeHelpersBound.ts +27 -13
- package/src/lib/ui/compile/awaitPlan.ts +14 -2
- package/src/lib/ui/compile/catchBinding.ts +8 -0
- package/src/lib/ui/compile/classStyleMergePlan.ts +108 -0
- package/src/lib/ui/compile/collectAbideDiagnostics.ts +11 -0
- package/src/lib/ui/compile/compileComponent.ts +4 -4
- package/src/lib/ui/compile/compileModule.ts +56 -21
- package/src/lib/ui/compile/compileSSR.ts +12 -10
- package/src/lib/ui/compile/compileShadow.ts +163 -12
- package/src/lib/ui/compile/composeProps.ts +3 -3
- package/src/lib/ui/compile/createShadowLanguageService.ts +58 -4
- package/src/lib/ui/compile/createShadowProgram.ts +2 -1
- package/src/lib/ui/compile/createShadowScope.ts +42 -0
- package/src/lib/ui/compile/decodeHtmlEntities.ts +4 -0
- package/src/lib/ui/compile/desugarSignals.ts +12 -7
- package/src/lib/ui/compile/eachPlan.ts +61 -0
- package/src/lib/ui/compile/elementPlan.ts +55 -0
- package/src/lib/ui/compile/encodeSemanticTokens.ts +24 -1
- package/src/lib/ui/compile/generateBuild.ts +156 -127
- package/src/lib/ui/compile/generateSSR.ts +112 -73
- package/src/lib/ui/compile/ifPlan.ts +7 -1
- package/src/lib/ui/compile/interpolatedTemplateLiteral.ts +25 -0
- package/src/lib/ui/compile/isPlainIdentifier.ts +7 -0
- package/src/lib/ui/compile/isTextLeaf.ts +8 -7
- package/src/lib/ui/compile/lowerContext.ts +36 -46
- package/src/lib/ui/compile/lowerDocAccess.ts +2 -2
- package/src/lib/ui/compile/lowerScript.ts +1 -1
- package/src/lib/ui/compile/markupTokens.ts +313 -0
- package/src/lib/ui/compile/pagePropsType.ts +23 -0
- package/src/lib/ui/compile/parseTemplate.ts +135 -62
- package/src/lib/ui/compile/reactiveBinding.ts +47 -0
- package/src/lib/ui/compile/renameSignalRefs.ts +44 -10
- package/src/lib/ui/compile/skeletonable.ts +5 -5
- package/src/lib/ui/compile/snippetPlan.ts +28 -0
- package/src/lib/ui/compile/spreadExcludedNames.ts +4 -4
- package/src/lib/ui/compile/structuralBlockTokens.ts +3 -4
- package/src/lib/ui/compile/switchPlan.ts +7 -1
- package/src/lib/ui/compile/tryPlan.ts +8 -1
- package/src/lib/ui/compile/types/AnalyzedComponent.ts +2 -2
- package/src/lib/ui/compile/types/Binding.ts +24 -0
- package/src/lib/ui/compile/types/CompiledShadow.ts +13 -1
- package/src/lib/ui/compile/types/ElementPlan.ts +43 -0
- package/src/lib/ui/compile/types/ShadowKind.ts +9 -0
- package/src/lib/ui/compile/types/ShadowScope.ts +19 -0
- package/src/lib/ui/compile/types/TemplateAttr.ts +7 -0
- package/src/lib/ui/compile/types/TemplateNode.ts +9 -9
- package/src/lib/ui/compile/withBindings.ts +45 -0
- package/src/lib/ui/createScope.ts +2 -2
- package/src/lib/ui/dom/MATHML_NAMESPACE.ts +2 -2
- package/src/lib/ui/dom/SVG_NAMESPACE.ts +3 -2
- package/src/lib/ui/dom/appendText.ts +1 -1
- package/src/lib/ui/dom/applyResolved.ts +1 -1
- package/src/lib/ui/dom/buildDetachedRange.ts +4 -3
- package/src/lib/ui/dom/cloneStatic.ts +3 -2
- package/src/lib/ui/dom/each.ts +26 -7
- package/src/lib/ui/dom/foreignWrapperTag.ts +2 -2
- package/src/lib/ui/dom/mountChild.ts +8 -1
- package/src/lib/ui/dom/mountSwappableRange.ts +79 -0
- package/src/lib/ui/dom/readCall.ts +6 -4
- package/src/lib/ui/dom/scopeLabel.ts +3 -3
- package/src/lib/ui/dom/skeleton.ts +10 -1
- package/src/lib/ui/dom/switchBlock.ts +17 -63
- package/src/lib/ui/dom/types/EachRow.ts +4 -0
- package/src/lib/ui/dom/types/SkeletonHoles.ts +4 -2
- package/src/lib/ui/dom/when.ts +11 -64
- package/src/lib/ui/history.ts +1 -2
- package/src/lib/ui/html.ts +28 -0
- package/src/lib/ui/installHotBridge.ts +0 -2
- package/src/lib/ui/renderChain.ts +16 -3
- package/src/lib/ui/router.ts +24 -3
- package/src/lib/ui/rpcOutbox/createOutboxQueue.ts +1 -1
- package/src/lib/ui/runtime/CHILD_PRESENT.ts +8 -0
- package/src/lib/ui/runtime/OUTLET_MARKER.ts +2 -2
- package/src/lib/ui/runtime/RANGE_MARKER.ts +1 -1
- package/src/lib/ui/runtime/REACTIVE_CONTEXT.ts +9 -3
- package/src/lib/ui/runtime/captureModelDoc.ts +1 -1
- package/src/lib/ui/runtime/createDoc.ts +107 -13
- package/src/lib/ui/runtime/createEffectNode.ts +5 -5
- package/src/lib/ui/runtime/flushEffects.ts +22 -9
- package/src/lib/ui/runtime/historyEntries.ts +1 -1
- package/src/lib/ui/runtime/track.ts +4 -2
- package/src/lib/ui/runtime/trigger.ts +34 -20
- package/src/lib/ui/runtime/types/Route.ts +2 -2
- package/src/lib/ui/runtime/types/UiComponent.ts +3 -2
- package/src/lib/ui/runtime/types/UiProps.ts +9 -12
- package/src/lib/ui/runtime/walkPath.ts +15 -3
- package/src/lib/ui/startClient.ts +41 -14
- package/src/lib/ui/state.ts +3 -3
- package/src/lib/ui/types/Scope.ts +3 -3
- package/src/serverBuildPlugins.ts +11 -5
- package/src/zodCjsPlugin.ts +51 -0
- package/template/.zed/settings.json +7 -1
- package/template/package.json +3 -0
- package/template/src/ui/pages/layout.abide +3 -5
- package/src/lib/shared/matchesDebugPattern.ts +0 -16
- package/src/lib/shared/parseDebugPatterns.ts +0 -21
|
@@ -1,24 +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
|
-
Multi-branch binding — the runtime for
|
|
5
|
+
Multi-branch binding — the runtime for `{#switch}` blocks and for `{#if}` chains that
|
|
6
|
+
include `{:elseif}` branches (compiled as a switch over `true` with Boolean-coerced
|
|
7
|
+
match thunks so the first truthy branch wins). A swappable range evaluates the
|
|
13
8
|
subject, picks the first case whose `match` equals it (strict `===`), falling back
|
|
14
9
|
to the default (`match` undefined); the chosen case's content lives in a RANGE
|
|
15
10
|
bounded by two comment markers, so a case holds any content. Staying on the same
|
|
16
11
|
case across a subject change leaves it mounted; switching clears the range and
|
|
17
|
-
builds the new case fresh.
|
|
18
|
-
|
|
19
|
-
On hydrate it adopts the case the server rendered: claim the start marker, run the
|
|
20
|
-
matching case in place, claim the end marker. The effect's first run picks the same
|
|
21
|
-
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.
|
|
22
14
|
*/
|
|
23
15
|
// @documentation plumbing
|
|
24
16
|
export function switchBlock(
|
|
@@ -27,59 +19,21 @@ export function switchBlock(
|
|
|
27
19
|
cases: SwitchCase[],
|
|
28
20
|
before: Node | null = null,
|
|
29
21
|
): void {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
teardown — not only when the subject switches cases via replaceRange. */
|
|
33
|
-
const group = scopeGroup()
|
|
34
|
-
let dispose: (() => void) | undefined
|
|
35
|
-
let activeIndex: number
|
|
36
|
-
let end: Comment
|
|
37
|
-
|
|
22
|
+
/* Pick the first case matching the subject (`===`), else the default (`match`
|
|
23
|
+
undefined), else -1 for no match. */
|
|
38
24
|
const select = (value: unknown): number => {
|
|
39
25
|
const matched = cases.findIndex(
|
|
40
26
|
(entry) => entry.match !== undefined && entry.match() === value,
|
|
41
27
|
)
|
|
42
28
|
return matched === -1 ? cases.findIndex((entry) => entry.match === undefined) : matched
|
|
43
29
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const start = openMarker(parent, RANGE_OPEN, before)
|
|
54
|
-
if (hydration !== undefined) {
|
|
55
|
-
activeIndex = select(subject())
|
|
56
|
-
const chosen = caseAt(activeIndex)
|
|
57
|
-
if (chosen !== undefined) {
|
|
58
|
-
dispose = group.track(scope(() => chosen.render(parent))) // claim the SSR nodes in place
|
|
59
|
-
}
|
|
60
|
-
end = openMarker(parent, RANGE_CLOSE)
|
|
61
|
-
} else {
|
|
62
|
-
end = openMarker(parent, RANGE_CLOSE, before)
|
|
63
|
-
activeIndex = select(subject())
|
|
64
|
-
const chosen = caseAt(activeIndex)
|
|
65
|
-
if (chosen !== undefined) {
|
|
66
|
-
dispose = group.track(fillBefore(end, (p) => chosen.render(p)))
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
effect(() => {
|
|
71
|
-
const index = select(subject())
|
|
72
|
-
if (index === activeIndex) {
|
|
73
|
-
return
|
|
74
|
-
}
|
|
75
|
-
activeIndex = index
|
|
76
|
-
const chosen = caseAt(index)
|
|
77
|
-
/* Null `dispose` before `replaceRange` builds the new case: a reentrant switch
|
|
78
|
-
during that build (an effect in the new content writing the subject) would
|
|
79
|
-
otherwise re-enter with the already-disposed disposer and clear it twice. */
|
|
80
|
-
const prior = dispose
|
|
81
|
-
dispose = undefined
|
|
82
|
-
const next = replaceRange(start, end, prior, chosen && ((p) => chosen.render(p)))
|
|
83
|
-
dispose = next !== undefined ? group.track(next) : undefined
|
|
84
|
-
})
|
|
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
|
+
)
|
|
85
39
|
}
|
|
@@ -14,4 +14,8 @@ export type EachRow = {
|
|
|
14
14
|
cell: State<unknown>
|
|
15
15
|
indexCell: State<number>
|
|
16
16
|
pending?: DocumentFragment
|
|
17
|
+
/* The id of the last reconcile pass that saw this row's key in the source. A row
|
|
18
|
+
whose `gen` lags the current pass departed and is pruned — an int compare that
|
|
19
|
+
replaces a per-reconcile `Set` of present keys. */
|
|
20
|
+
gen?: number
|
|
17
21
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/* The holes a realized `skeleton` exposes for the build's attach code to wire up:
|
|
2
2
|
`el` the element holes (attribute/listener/bind nodes) in pre-order, `an` the anchor
|
|
3
|
-
holes (reactive text, control flow, components) in document order. Two arrays
|
|
4
|
-
|
|
3
|
+
holes (reactive text, control flow, components) in document order. Two arrays because
|
|
4
|
+
the element and anchor walks follow different traversal rules (`walkElementOrder` vs
|
|
5
|
+
`walkAnchorOrder`), so their indices are independent series — el[N] and an[N] are
|
|
6
|
+
unrelated holes, each numbered from 0 by its own walk. */
|
|
5
7
|
export type SkeletonHoles = {
|
|
6
8
|
el: Element[]
|
|
7
9
|
an: Node[]
|
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
|
}
|
package/src/lib/ui/history.ts
CHANGED
|
@@ -21,8 +21,7 @@ export function history(doc: Doc, { limit = 100 }: { limit?: number } = {}): His
|
|
|
21
21
|
— held by an in-progress transaction and by an undo/redo replay. */
|
|
22
22
|
let sink: Patch[] | undefined
|
|
23
23
|
|
|
24
|
-
/* Push one finished entry, evicting the oldest past `limit
|
|
25
|
-
sink) also invalidates the redo stack. */
|
|
24
|
+
/* Push one finished entry, evicting the oldest past `limit`. */
|
|
26
25
|
const push = (stack: Patch[][], entry: Patch[]): void => {
|
|
27
26
|
stack.push(entry)
|
|
28
27
|
if (stack.length > limit) {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { RAW_HTML, type RawHtml } from '../shared/html.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Marks a string as trusted raw HTML so a `{expr}` interpolation inserts its nodes
|
|
5
|
+
instead of escaped text — the abide idiom for raw markup (no `{@html}` mustache).
|
|
6
|
+
Works two ways:
|
|
7
|
+
|
|
8
|
+
html(trustedString) // plain call — insert the string verbatim
|
|
9
|
+
html`<b>${name}</b>` // tagged — concatenate parts verbatim
|
|
10
|
+
|
|
11
|
+
Calling `html` is the explicit opt-in to raw insertion; plain `{value}` always
|
|
12
|
+
escapes. The tag does NOT auto-escape interpolations (it's raw by intent), so only
|
|
13
|
+
build markup from values you trust, or escape them yourself. Imported by the author
|
|
14
|
+
(`import { html } from 'abide/ui/html'`) — it is UI-authoring vocabulary, only ever
|
|
15
|
+
written inside a template, so it lives in `ui/` (the reader, `rawHtmlString`, is the
|
|
16
|
+
isomorphic plumbing and stays in `shared/`).
|
|
17
|
+
*/
|
|
18
|
+
// @documentation templating
|
|
19
|
+
export function html(strings: TemplateStringsArray | string, ...values: unknown[]): RawHtml {
|
|
20
|
+
if (typeof strings === 'string') {
|
|
21
|
+
return { [RAW_HTML]: strings }
|
|
22
|
+
}
|
|
23
|
+
let markup = strings[0] ?? ''
|
|
24
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
25
|
+
markup += String(values[index]) + (strings[index + 1] ?? '')
|
|
26
|
+
}
|
|
27
|
+
return { [RAW_HTML]: markup }
|
|
28
|
+
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { html } from '../shared/html.ts'
|
|
2
1
|
import { snippet } from '../shared/snippet.ts'
|
|
3
2
|
import { anchorCursor } from './dom/anchorCursor.ts'
|
|
4
3
|
import { appendSnippet } from './dom/appendSnippet.ts'
|
|
@@ -49,7 +48,6 @@ dev (the live-reload script sets `window.__abideDev`).
|
|
|
49
48
|
*/
|
|
50
49
|
export function installHotBridge(): void {
|
|
51
50
|
;(globalThis as { __abide?: Record<string, unknown> }).__abide = {
|
|
52
|
-
html,
|
|
53
51
|
snippet,
|
|
54
52
|
scope,
|
|
55
53
|
enterScope,
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { CHILD_PRESENT } from './runtime/CHILD_PRESENT.ts'
|
|
1
2
|
import { OUTLET_CLOSE, OUTLET_OPEN } from './runtime/OUTLET_MARKER.ts'
|
|
2
3
|
import type { RenderContext } from './runtime/types/RenderContext.ts'
|
|
3
4
|
import type { SsrRender } from './runtime/types/SsrRender.ts'
|
|
4
5
|
import type { UiComponent } from './runtime/types/UiComponent.ts'
|
|
6
|
+
import type { UiProps } from './runtime/types/UiProps.ts'
|
|
5
7
|
|
|
6
8
|
const OPEN = `<!--${OUTLET_OPEN}-->`
|
|
7
9
|
const CLOSE = `<!--${OUTLET_CLOSE}-->`
|
|
@@ -31,8 +33,19 @@ export async function renderChain(
|
|
|
31
33
|
): Promise<SsrRender> {
|
|
32
34
|
const ctx: RenderContext = { next: 0 }
|
|
33
35
|
const renders: SsrRender[] = []
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
/* Route params as thunks (static server-side — only shape parity with the client so
|
|
37
|
+
`props()` reads `$props["id"]?.()` resolve). A layout (every view but the last) also
|
|
38
|
+
gets `$children` set to `CHILD_PRESENT` so SSR renders `{#if children}` the same way
|
|
39
|
+
the client does, keeping hydration congruent. */
|
|
40
|
+
const paramThunks: UiProps = {}
|
|
41
|
+
for (const key of Object.keys(params)) {
|
|
42
|
+
paramThunks[key] = () => params[key]
|
|
43
|
+
}
|
|
44
|
+
for (let index = 0; index < views.length; index += 1) {
|
|
45
|
+
const view = views[index] as UiComponent
|
|
46
|
+
const hasChild = index < views.length - 1
|
|
47
|
+
const props: UiProps = hasChild ? { ...paramThunks, $children: CHILD_PRESENT } : paramThunks
|
|
48
|
+
renders.push(await view.render(props, ctx))
|
|
36
49
|
}
|
|
37
50
|
let html = renders[renders.length - 1]?.html ?? ''
|
|
38
51
|
for (let index = renders.length - 2; index >= 0; index -= 1) {
|
|
@@ -42,7 +55,7 @@ export async function renderChain(
|
|
|
42
55
|
so a second outlet would mount the SSR child and the hydrated child into
|
|
43
56
|
DIFFERENT slots — a silent desync. Throw at build instead. */
|
|
44
57
|
if (parent.html.split(OUTLET_PLACEHOLDER).length - 1 !== 1) {
|
|
45
|
-
throw new Error('[abide] a layout.abide must contain exactly one
|
|
58
|
+
throw new Error('[abide] a layout.abide must contain exactly one {children()} outlet')
|
|
46
59
|
}
|
|
47
60
|
/* Fold the child between the outlet markers (function replacement so `$&`/`$\``
|
|
48
61
|
in the child html insert literally). */
|
package/src/lib/ui/router.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { outlet } from './dom/outlet.ts'
|
|
|
4
4
|
import { effect } from './effect.ts'
|
|
5
5
|
import { matchRoute } from './matchRoute.ts'
|
|
6
6
|
import { navigatePath } from './navigate.ts'
|
|
7
|
+
import { CHILD_PRESENT } from './runtime/CHILD_PRESENT.ts'
|
|
7
8
|
import { clientPage } from './runtime/clientPage.ts'
|
|
8
9
|
import { enterRenderPass } from './runtime/enterRenderPass.ts'
|
|
9
10
|
import { exitRenderPass } from './runtime/exitRenderPass.ts'
|
|
@@ -15,6 +16,7 @@ import type { AbideHistoryState } from './runtime/types/AbideHistoryState.ts'
|
|
|
15
16
|
import type { NavVerdict } from './runtime/types/NavVerdict.ts'
|
|
16
17
|
import type { Route } from './runtime/types/Route.ts'
|
|
17
18
|
import type { RouteLoader } from './runtime/types/RouteLoader.ts'
|
|
19
|
+
import type { UiProps } from './runtime/types/UiProps.ts'
|
|
18
20
|
import { untrack } from './runtime/untrack.ts'
|
|
19
21
|
|
|
20
22
|
/* An outlet boundary — the `<!--abide:outlet-->`…`<!--/abide:outlet-->` marker pair a
|
|
@@ -192,6 +194,23 @@ export function router(
|
|
|
192
194
|
outlet(host)
|
|
193
195
|
rootBoundary = PENDING_OUTLET.current as Boundary
|
|
194
196
|
}
|
|
197
|
+
/* Route params as reactive thunks: reading `clientPage.value.params` inside the
|
|
198
|
+
thunk tracks the page signal, so an in-place same-route hop (params change, the
|
|
199
|
+
page stays mounted) re-runs each `props()` derive. The key set is stable across
|
|
200
|
+
same-route hops, so the bag need not rebuild. A layout also gets `$children`
|
|
201
|
+
set to `CHILD_PRESENT` when a child layer exists below it, read by
|
|
202
|
+
`{#if children}` (a layout's `{children()}` lowers to its `outlet()` boundary,
|
|
203
|
+
so it ignores this value). */
|
|
204
|
+
const propsBag = (hasChild: boolean): UiProps => {
|
|
205
|
+
const bag: UiProps = {}
|
|
206
|
+
for (const key of Object.keys(params)) {
|
|
207
|
+
bag[key] = () => clientPage.value.params[key]
|
|
208
|
+
}
|
|
209
|
+
if (hasChild) {
|
|
210
|
+
bag.$children = CHILD_PRESENT
|
|
211
|
+
}
|
|
212
|
+
return bag
|
|
213
|
+
}
|
|
195
214
|
let boundary = baseBoundary(index)
|
|
196
215
|
for (let depth = index; depth < layoutViews.length; depth += 1) {
|
|
197
216
|
const view = layoutViews[depth] as Route
|
|
@@ -200,14 +219,15 @@ export function router(
|
|
|
200
219
|
boundary.open,
|
|
201
220
|
boundary.close,
|
|
202
221
|
view.build,
|
|
203
|
-
|
|
222
|
+
/* A layout always has a child below: a deeper layout, or the page. */
|
|
223
|
+
propsBag(depth < layoutViews.length - 1 || pageView !== undefined),
|
|
204
224
|
/* The layout's route key names its scope in the inspector's Reactive tab
|
|
205
225
|
(no host element to read a tag from — see `scopeLabel`). */
|
|
206
226
|
chainKeys[depth],
|
|
207
227
|
)
|
|
208
228
|
const slot = PENDING_OUTLET.current
|
|
209
229
|
if (slot === undefined) {
|
|
210
|
-
throw new Error('[abide] a layout.abide must contain a
|
|
230
|
+
throw new Error('[abide] a layout.abide must contain a {children()} outlet')
|
|
211
231
|
}
|
|
212
232
|
mountedLayouts.push({ key: chainKeys[depth] as string, dispose, slot })
|
|
213
233
|
boundary = slot
|
|
@@ -220,7 +240,8 @@ export function router(
|
|
|
220
240
|
boundary.open,
|
|
221
241
|
boundary.close,
|
|
222
242
|
pageView.build,
|
|
223
|
-
|
|
243
|
+
/* A page is a leaf — no child layer. */
|
|
244
|
+
propsBag(false),
|
|
224
245
|
/* The page's route key names its scope in the inspector (see above). */
|
|
225
246
|
pageKey,
|
|
226
247
|
).dispose
|
|
@@ -41,7 +41,7 @@ const keyFor = (url: string): string => `abide:outbox:${url}`
|
|
|
41
41
|
/*
|
|
42
42
|
A durable, app-owned FIFO retry queue for one RPC. A call fetches directly and throws as
|
|
43
43
|
before while the queue is empty; when the server can't be reached — a transport failure or
|
|
44
|
-
a 502/503/504/
|
|
44
|
+
a 502/503/504/520–527/530 — the caller `park`s the request here (a side-effect; the call still
|
|
45
45
|
throws), and once a backlog exists the caller parks every later call at the tail too, so
|
|
46
46
|
FIFO order is preserved on replay. Built on `doc` + `persist`, so the queue IS a persisted
|
|
47
47
|
document that survives a reload. Draining is manual — the app calls `retry()` (per-entry or
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/*
|
|
2
|
+
A no-op truthiness marker the router (client) and `renderChain` (SSR) set as a layout's
|
|
3
|
+
`$props.$children` when a child layer (a nested layout or the page) exists below it. Read
|
|
4
|
+
only by `{#if children}` (→ `$props?.$children`); a layout's `{children()}` lowers to the
|
|
5
|
+
`outlet()` boundary the router fills, so this value is never invoked — it exists purely so
|
|
6
|
+
`{#if children}` reads a uniform presence signal on both the client and the server.
|
|
7
|
+
*/
|
|
8
|
+
export const CHILD_PRESENT = (_host: Element): void => {}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
`<abide-outlet>` ELEMENT, so the next chain layer the router fills in lays out as a true
|
|
3
3
|
direct child of the slot's parent (no wrapper box breaking the layout's flex/grid/`:first-child`).
|
|
4
4
|
|
|
5
|
-
`abide:outlet` / `/abide:outlet` deliberately match `
|
|
6
|
-
(the `abide:` / `/abide:` convention), so a layout's own hole-scanning treats the outlet's
|
|
5
|
+
`abide:outlet` / `/abide:outlet` deliberately match `markerDepthDelta`'s `startsWith('abide:')` /
|
|
6
|
+
`startsWith('/abide:')` rule (the `abide:` / `/abide:` convention), so a layout's own hole-scanning treats the outlet's
|
|
7
7
|
future child content as a balanced range and skips it — exactly like an `await`/`try` boundary.
|
|
8
8
|
The router fills the boundary with the next layer (see `outlet`/`fillBoundary`). */
|
|
9
9
|
export const OUTLET_OPEN = 'abide:outlet'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* The comment-marker "wire alphabet" — the single source of truth for the sentinel
|
|
2
2
|
strings the SSR emit (`generateSSR`) writes into HTML comments and the hydrate scan
|
|
3
|
-
(`skeleton`) + every range-mount runtime (`when`/`
|
|
3
|
+
(`skeleton`) + every range-mount runtime (`when`/`switchBlock`/`each`/`mountRange`/
|
|
4
4
|
`mountSlot`/`appendSnippet`) creates as `document.createComment` nodes. Both sides
|
|
5
5
|
reference THESE constants, so a marker the server writes and the marker the client
|
|
6
6
|
looks for can never drift on a literal.
|
|
@@ -2,13 +2,19 @@ import type { ReactiveNode } from './types/ReactiveNode.ts'
|
|
|
2
2
|
|
|
3
3
|
/*
|
|
4
4
|
Mutable singletons shared by every reactive primitive: the observer currently
|
|
5
|
-
running (so a read can register itself as that observer's dependency), the
|
|
5
|
+
running (so a read can register itself as that observer's dependency), the queue
|
|
6
6
|
of effects dirtied since the last flush, and the batch depth (writes inside a
|
|
7
7
|
batch queue effects and flush once on exit). Held on one object so signal,
|
|
8
8
|
computed, and effect all reference the same graph state without a barrel.
|
|
9
|
+
|
|
10
|
+
`pendingEffects` is a plain array, not a `Set`: an effect is enqueued only on its
|
|
11
|
+
CLEAN→dirty transition (`mark`'s `wasClean` gate), so it can be pushed at most once
|
|
12
|
+
per flush cycle — the status machine already guarantees the dedup a `Set` would,
|
|
13
|
+
without hashing every enqueue. The flush double-buffers (swaps in a fresh array) so
|
|
14
|
+
effects re-dirtied mid-flush queue for the next pass.
|
|
9
15
|
*/
|
|
10
16
|
export const REACTIVE_CONTEXT: {
|
|
11
17
|
observer: ReactiveNode | undefined
|
|
12
|
-
pendingEffects:
|
|
18
|
+
pendingEffects: ReactiveNode[]
|
|
13
19
|
batchDepth: number
|
|
14
|
-
} = { observer: undefined, pendingEffects:
|
|
20
|
+
} = { observer: undefined, pendingEffects: [], batchDepth: 0 }
|
|
@@ -6,7 +6,7 @@ Runs a component `build` and returns its result (the mount handle / disposer)
|
|
|
6
6
|
alongside the component's own `model` document — the serializable `state` doc,
|
|
7
7
|
needed so a hot swap can carry its value across (see `hotReplace`). The model is
|
|
8
8
|
found, not threaded: a component seeds its `model` first (the desugared
|
|
9
|
-
`const model =
|
|
9
|
+
`const $$model = scope()` + its init `replace` patches (state slots lowered by `lowerDocAccess`) run before any child mounts or
|
|
10
10
|
control-flow blocks), so the FIRST patch announced on the bus during the build names
|
|
11
11
|
it. A component with no `state` mints no model and emits nothing first — `model` is
|
|
12
12
|
then `undefined` and there is nothing to preserve. Used only on the hot path; the
|
|
@@ -43,6 +43,18 @@ the change.
|
|
|
43
43
|
export function createDoc(initial: unknown): Doc {
|
|
44
44
|
let tree = initial
|
|
45
45
|
const nodes = new Map<string, ReactiveNode>()
|
|
46
|
+
/* Prefix index: every path → the set of its DIRECT children that lie on a route
|
|
47
|
+
to some minted node (a trie over the minted paths' ancestor chains). A
|
|
48
|
+
structural descend walks this from the change root through actual descendants
|
|
49
|
+
only, replacing the old O(all live cells) `startsWith` scan over the whole
|
|
50
|
+
`nodes` map. The links span INTERMEDIATE paths that have no node of their own
|
|
51
|
+
(e.g. `byId` and `byId/<key>` exist as links even when only `byId/<key>/n` was
|
|
52
|
+
read), so a descend from any ancestor reaches the leaf. Maintained in lockstep
|
|
53
|
+
with `nodes`: a node's whole ancestor chain is linked when `nodeFor` mints it,
|
|
54
|
+
unlinked from the leaf up when `wakeSubtree` evicts it. Membership of a path in
|
|
55
|
+
`nodes` (does it carry a signal?) is independent of its membership here (is it
|
|
56
|
+
on a route to one?). */
|
|
57
|
+
const childKeys = new Map<string, Set<string>>()
|
|
46
58
|
/* Computed slots: a path whose value is a function of other paths, not stored
|
|
47
59
|
truth. Held apart from `nodes` so the structural wake/eviction never touches
|
|
48
60
|
them; their dirtiness is driven entirely by the deps they read (the signal
|
|
@@ -54,15 +66,65 @@ export function createDoc(initial: unknown): Doc {
|
|
|
54
66
|
name the document it came from (reference identity, the undo/persistence key). */
|
|
55
67
|
let self: Doc
|
|
56
68
|
|
|
69
|
+
/* Links `path` into the trie under its parent and continues up the ancestor chain.
|
|
70
|
+
Stops at the first ancestor whose parent set already lists it — that link, and
|
|
71
|
+
everything above it, is already present (some sibling minted the upper chain). */
|
|
72
|
+
function linkPath(path: string): void {
|
|
73
|
+
let child = path
|
|
74
|
+
for (;;) {
|
|
75
|
+
const parentPath = parentPathOf(child)
|
|
76
|
+
const siblings = childKeys.get(parentPath)
|
|
77
|
+
if (siblings === undefined) {
|
|
78
|
+
childKeys.set(parentPath, new Set([child]))
|
|
79
|
+
} else if (siblings.has(child)) {
|
|
80
|
+
return
|
|
81
|
+
} else {
|
|
82
|
+
siblings.add(child)
|
|
83
|
+
}
|
|
84
|
+
if (parentPath === '') {
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
child = parentPath
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
57
91
|
function nodeFor(path: string): ReactiveNode {
|
|
58
92
|
let node = nodes.get(path)
|
|
59
93
|
if (node === undefined) {
|
|
60
94
|
node = createSignalNode(walkPath(tree, path).value)
|
|
61
95
|
nodes.set(path, node)
|
|
96
|
+
linkPath(path)
|
|
62
97
|
}
|
|
63
98
|
return node
|
|
64
99
|
}
|
|
65
100
|
|
|
101
|
+
/* Drops a node and unlinks it from the trie, pruning each emptied ancestor link up
|
|
102
|
+
the chain — the eviction half of the lockstep with `nodeFor`. A link is removed
|
|
103
|
+
only once its child set empties, so an intermediate path with other live
|
|
104
|
+
descendants keeps its place. */
|
|
105
|
+
function evict(path: string): void {
|
|
106
|
+
nodes.delete(path)
|
|
107
|
+
let child = path
|
|
108
|
+
for (;;) {
|
|
109
|
+
const parentPath = parentPathOf(child)
|
|
110
|
+
const siblings = childKeys.get(parentPath)
|
|
111
|
+
if (siblings === undefined) {
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
siblings.delete(child)
|
|
115
|
+
/* A parent still listing other children, or still carrying its own node,
|
|
116
|
+
stays — only a fully empty, node-less link is pruned and walked past. */
|
|
117
|
+
if (siblings.size > 0) {
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
childKeys.delete(parentPath)
|
|
121
|
+
if (parentPath === '' || nodes.has(parentPath)) {
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
child = parentPath
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
66
128
|
function read<T>(path: string): T {
|
|
67
129
|
/* Size-gated so a doc with no computed slots pays nothing on the stored hot
|
|
68
130
|
path — only one `.size` check, not a `.get` per read. */
|
|
@@ -109,21 +171,53 @@ export function createDoc(initial: unknown): Doc {
|
|
|
109
171
|
return
|
|
110
172
|
}
|
|
111
173
|
const prefix = rootPath === '' ? '' : `${rootPath}/`
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
174
|
+
/* Walk the prefix index from `rootPath` over its real descendants only — a
|
|
175
|
+
BFS over the parent→children adjacency map — instead of scanning every live
|
|
176
|
+
node. `pending` holds parent-paths whose direct children are yet to visit;
|
|
177
|
+
each visited child that is itself a container is enqueued. The total work is
|
|
178
|
+
the size of the changed subtree, not the whole doc. */
|
|
179
|
+
const pending: string[] = [rootPath]
|
|
180
|
+
while (pending.length > 0) {
|
|
181
|
+
const parentPath = pending.pop() as string
|
|
182
|
+
const siblings = childKeys.get(parentPath)
|
|
183
|
+
if (siblings === undefined) {
|
|
184
|
+
continue
|
|
185
|
+
}
|
|
186
|
+
/* Snapshot the set before iterating: eviction below mutates it, and a
|
|
187
|
+
deleted child must not skip its successor (Set iteration is order-
|
|
188
|
+
sensitive to in-place deletes). */
|
|
189
|
+
for (const candidate of [...siblings]) {
|
|
190
|
+
/* Walk from the already-resolved container (`rootValue`) using only the
|
|
191
|
+
path SUFFIX past the shared prefix, instead of re-walking every
|
|
192
|
+
candidate's full path from the tree root — the prefix is walked once
|
|
193
|
+
per wake, not once per descendant. */
|
|
194
|
+
const walk = walkPath(rootValue, candidate.slice(prefix.length))
|
|
195
|
+
/* An INTERMEDIATE link (a path on a route to a node but carrying none of
|
|
196
|
+
its own — e.g. `byId/<key>` when only `byId/<key>/n` was read) has no
|
|
197
|
+
signal to wake; it exists only to be descended through. So the
|
|
198
|
+
wake/evict touches a node only when one is present, while the descend
|
|
199
|
+
decision below is independent of node existence. */
|
|
200
|
+
const node = nodes.get(candidate)
|
|
122
201
|
if (walk.exists) {
|
|
123
|
-
|
|
202
|
+
if (node !== undefined) {
|
|
203
|
+
writeNode(node, walk.value)
|
|
204
|
+
}
|
|
205
|
+
/* Descend into a container child to reach its own descendants. */
|
|
206
|
+
if (walk.value !== null && typeof walk.value === 'object') {
|
|
207
|
+
pending.push(candidate)
|
|
208
|
+
}
|
|
124
209
|
} else {
|
|
125
|
-
|
|
126
|
-
|
|
210
|
+
/* A descendant whose path the mutation removed — a deleted key, an
|
|
211
|
+
out-of-range index after a shrink — is woken to undefined, then
|
|
212
|
+
dropped from the registry. The woken reader re-mints a fresh node
|
|
213
|
+
on its flush if the path ever returns. A removed container's own
|
|
214
|
+
descendants are still enqueued (whether or not THIS path held a
|
|
215
|
+
node) so they too are woken/evicted. */
|
|
216
|
+
if (node !== undefined) {
|
|
217
|
+
writeNode(node, undefined)
|
|
218
|
+
evict(candidate)
|
|
219
|
+
}
|
|
220
|
+
pending.push(candidate)
|
|
127
221
|
}
|
|
128
222
|
}
|
|
129
223
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { NODE_STATE } from './NODE_STATE.ts'
|
|
2
2
|
import { OWNER } from './OWNER.ts'
|
|
3
|
-
import { REACTIVE_CONTEXT } from './REACTIVE_CONTEXT.ts'
|
|
4
3
|
import { runNode } from './runNode.ts'
|
|
5
4
|
import { toTeardown } from './toTeardown.ts'
|
|
6
5
|
import type { EffectResult } from './types/EffectResult.ts'
|
|
@@ -52,11 +51,12 @@ export function createEffectNode(fn: () => EffectResult): () => void {
|
|
|
52
51
|
runCleanup()
|
|
53
52
|
unlinkDeps(node)
|
|
54
53
|
/* Clearing compute makes runNode a no-op: an effect disposed mid-flush (by an
|
|
55
|
-
earlier effect in the same batch) is still in flushEffects
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
earlier effect in the same batch) is still in the queue flushEffects is
|
|
55
|
+
draining, so without this it would re-run its body and re-link into the graph
|
|
56
|
+
— a disposed effect resurrected. This alone neutralizes it; the queue is a
|
|
57
|
+
plain array (no O(1) delete) and a settled no-op iteration is cheaper than an
|
|
58
|
+
O(n) splice, so the stale entry is left to be skipped on its flush. */
|
|
58
59
|
node.compute = undefined
|
|
59
|
-
REACTIVE_CONTEXT.pendingEffects.delete(node)
|
|
60
60
|
}
|
|
61
61
|
if (OWNER.current !== undefined) {
|
|
62
62
|
OWNER.current.push(dispose)
|