@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
package/src/lib/ui/dom/each.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { effect } from '../effect.ts'
|
|
2
2
|
import { CURRENT_PATH } from '../runtime/CURRENT_PATH.ts'
|
|
3
3
|
import { claimChild } from '../runtime/claimChild.ts'
|
|
4
|
-
import {
|
|
4
|
+
import { claimMarker } from '../runtime/claimMarker.ts'
|
|
5
5
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
6
6
|
import { scope } from '../runtime/scope.ts'
|
|
7
7
|
import { scopeGroup } from '../runtime/scopeGroup.ts'
|
|
@@ -82,8 +82,7 @@ export function each<T>(
|
|
|
82
82
|
const segment = keyed ? keyOf(item) : position
|
|
83
83
|
const hydration = RENDER.hydration
|
|
84
84
|
if (hydration !== undefined) {
|
|
85
|
-
const start =
|
|
86
|
-
hydration.next.set(parent, start.nextSibling)
|
|
85
|
+
const start = claimMarker(hydration, parent, 'each row start marker')
|
|
87
86
|
const dispose = group.track(
|
|
88
87
|
scope(() =>
|
|
89
88
|
withPathFrom(basePath, segment, () =>
|
|
@@ -91,8 +90,7 @@ export function each<T>(
|
|
|
91
90
|
),
|
|
92
91
|
),
|
|
93
92
|
)
|
|
94
|
-
const end =
|
|
95
|
-
hydration.next.set(parent, end.nextSibling)
|
|
93
|
+
const end = claimMarker(hydration, parent, 'each row end marker')
|
|
96
94
|
return { start, end, dispose, cell, indexCell }
|
|
97
95
|
}
|
|
98
96
|
/* Shared detached-range create primitive: a `[ … ]` fragment built under `parent`'s
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parkCursor } from '../runtime/parkCursor.ts'
|
|
1
2
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
2
3
|
import { scope } from '../runtime/scope.ts'
|
|
3
4
|
import type { UiProps } from '../runtime/types/UiProps.ts'
|
|
@@ -31,7 +32,7 @@ export function fillBoundary(
|
|
|
31
32
|
}
|
|
32
33
|
/* Hydrate: adopt the server content between the markers in place. */
|
|
33
34
|
const parent = open.parentNode as Node
|
|
34
|
-
hydration
|
|
35
|
+
parkCursor(hydration, parent, open.nextSibling)
|
|
35
36
|
const scoped = withScope(() => scope(() => build(parent, props)))
|
|
36
37
|
return { dispose: disposeRange(scoped, open, close) }
|
|
37
38
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { RENDER } from '../runtime/RENDER.ts'
|
|
1
|
+
import { runHydrationPass } from '../runtime/runHydrationPass.ts'
|
|
3
2
|
import { scope } from '../runtime/scope.ts'
|
|
4
3
|
import { withScope } from './withScope.ts'
|
|
5
4
|
|
|
@@ -24,18 +23,12 @@ export function hydrate(
|
|
|
24
23
|
build: (host: Element, props: unknown) => void,
|
|
25
24
|
props?: unknown,
|
|
26
25
|
): () => void {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
try {
|
|
26
|
+
/* The pass lifecycle (claim cursor, withhold window, render pass, restore-on-throw)
|
|
27
|
+
is owned by `runHydrationPass` — the same bracket the router's hydrating mount runs. */
|
|
28
|
+
return runHydrationPass(() => {
|
|
31
29
|
/* Same shared mount core as `mount` (see `withScope`) — a hydrated component owns a
|
|
32
30
|
scope too, adopting the model its build adopts — run with the claim cursor active. */
|
|
33
31
|
const { lexical } = withScope(() => scope(() => build(host, props)))
|
|
34
32
|
return () => lexical.dispose()
|
|
35
|
-
}
|
|
36
|
-
RENDER.hydration = previous
|
|
37
|
-
/* Outermost exit clears the window and wakes the peeks this pass withheld; a nested
|
|
38
|
-
child hydrate raises/lowers the depth without firing it early. */
|
|
39
|
-
hydrationWindow.exit()
|
|
40
|
-
}
|
|
33
|
+
})
|
|
41
34
|
}
|
|
@@ -1,13 +1,37 @@
|
|
|
1
|
+
import { CURRENT_PATH } from '../runtime/CURRENT_PATH.ts'
|
|
2
|
+
import { claimChild } from '../runtime/claimChild.ts'
|
|
1
3
|
import { OWNER } from '../runtime/OWNER.ts'
|
|
4
|
+
import { RANGE_OPEN } from '../runtime/RANGE_MARKER.ts'
|
|
5
|
+
import { RENDER } from '../runtime/RENDER.ts'
|
|
2
6
|
import type { UiComponent } from '../runtime/types/UiComponent.ts'
|
|
3
7
|
import { withOptionalPath } from '../runtime/withOptionalPath.ts'
|
|
8
|
+
import { commentData } from './commentData.ts'
|
|
9
|
+
import { discardAndRebuild } from './discardAndRebuild.ts'
|
|
4
10
|
import { mountRange } from './mountRange.ts'
|
|
11
|
+
import { openMarker } from './openMarker.ts'
|
|
5
12
|
|
|
6
13
|
/*
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
14
|
+
The client mount for a child component — a DUAL-MODE adopter, and the ONLY child mount the
|
|
15
|
+
compiler emits. A child mounts as a marker-bounded range at `before` in `parent` (no wrapper
|
|
16
|
+
element, so the child's root is a true direct child — see `mountRange`), its dispose filed
|
|
17
|
+
with the mounting owner so scope and DOM leave together on teardown.
|
|
18
|
+
|
|
19
|
+
The server decides per render whether a HOISTABLE child (ADR-0039) inlined (fast/settled) or
|
|
20
|
+
streamed (still-pending), and the client build can't know which — nor whether this child was
|
|
21
|
+
hoistable at all — so hydrate mode probes the cursor:
|
|
22
|
+
|
|
23
|
+
- `<!--[-->` (RANGE_OPEN) next → the server INLINED the child (every non-hoistable child,
|
|
24
|
+
and a hoistable one whose flight settled); adopt its range in place.
|
|
25
|
+
- `<!--abide:await:CHILDPATH-->` next → the server STREAMED it; `__abideSwap` has already
|
|
26
|
+
spliced the child's `<!--[-->…<!--]-->` between the boundary markers before the bundle
|
|
27
|
+
ran, so claim the boundary open, adopt the inner range in place, claim the boundary close.
|
|
28
|
+
- no hydration (a client-side navigation) → a plain create-mode mount.
|
|
29
|
+
|
|
30
|
+
CHILDPATH is the child's render-path — `withPath(ordinal, () => CURRENT_PATH.current)` —
|
|
31
|
+
computed the same way the server's `renderPath(ordinal)` wrote the boundary id, so the two
|
|
32
|
+
agree with no counter. A streamed child re-mounts (no RESUME value of its own — the boundary
|
|
33
|
+
is html-only); nested `{#await}` blocks and async cells inside it adopt through their own
|
|
34
|
+
existing channels.
|
|
11
35
|
*/
|
|
12
36
|
// @documentation plumbing
|
|
13
37
|
export function mountChild(
|
|
@@ -21,6 +45,50 @@ export function mountChild(
|
|
|
21
45
|
row key the each block already pushed). Absent (a non-compiled caller) → no path segment. */
|
|
22
46
|
ordinal?: number,
|
|
23
47
|
): void {
|
|
24
|
-
const
|
|
48
|
+
const run = <T>(build: () => T): T => withOptionalPath(ordinal, build)
|
|
49
|
+
const mount = (): { dispose: () => void } =>
|
|
50
|
+
run(() => mountRange(parent, factory.build, props, before))
|
|
51
|
+
|
|
52
|
+
const hydration = RENDER.hydration
|
|
53
|
+
if (hydration === undefined) {
|
|
54
|
+
/* Call mount() first, THEN register its dispose — `OWNER.current?.push(mount())` would
|
|
55
|
+
short-circuit the whole expression (never mounting) when there is no owner. */
|
|
56
|
+
const handle = mount()
|
|
57
|
+
OWNER.current?.push(handle.dispose)
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* Probe the cursor without consuming it. Streamed iff the next node is this child's boundary. */
|
|
62
|
+
const childPath = run(() => CURRENT_PATH.current)
|
|
63
|
+
const cursor = claimChild(hydration, parent)
|
|
64
|
+
const streamed = cursor !== null && commentData(cursor) === `abide:await:${childPath}`
|
|
65
|
+
|
|
66
|
+
if (!streamed) {
|
|
67
|
+
/* INLINE — the server inlined the child's `[ … ]` range; adopt it in place. */
|
|
68
|
+
const handle = mount()
|
|
69
|
+
OWNER.current?.push(handle.dispose)
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* STREAMED — claim the boundary open (advances the cursor to the swapped-in `[`). */
|
|
74
|
+
const open = openMarker(parent, `abide:await:${childPath}`)
|
|
75
|
+
const inner = claimChild(hydration, parent)
|
|
76
|
+
const warm = inner !== null && commentData(inner) === RANGE_OPEN
|
|
77
|
+
if (warm) {
|
|
78
|
+
const handle = mount()
|
|
79
|
+
openMarker(parent, `/abide:await:${childPath}`)
|
|
80
|
+
OWNER.current?.push(handle.dispose)
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
/* COLD (defensive — renderToStream drains every await before closing, so a warm boundary is the
|
|
84
|
+
deterministic path). The fragment never arrived: discard the empty boundary and create-mount
|
|
85
|
+
the child fresh at that position; its streamed cells resolve post-mount via receiveStreamedCell. */
|
|
86
|
+
const handle = discardAndRebuild(
|
|
87
|
+
hydration,
|
|
88
|
+
parent,
|
|
89
|
+
open,
|
|
90
|
+
`/abide:await:${childPath}`,
|
|
91
|
+
(after) => run(() => mountRange(parent, factory.build, props, after)),
|
|
92
|
+
)
|
|
25
93
|
OWNER.current?.push(handle.dispose)
|
|
26
94
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { effect } from '../effect.ts'
|
|
2
2
|
import { CURRENT_PATH } from '../runtime/CURRENT_PATH.ts'
|
|
3
|
+
import { parkCursor } from '../runtime/parkCursor.ts'
|
|
3
4
|
import { RANGE_CLOSE, RANGE_OPEN } from '../runtime/RANGE_MARKER.ts'
|
|
4
5
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
5
6
|
import { scope } from '../runtime/scope.ts'
|
|
@@ -82,7 +83,7 @@ export function mountSwappableRange<Key>(
|
|
|
82
83
|
const chosen = chosenFor(activeKey)
|
|
83
84
|
const matchedEnd = matchingRangeClose(start)
|
|
84
85
|
clearBetween(start, matchedEnd)
|
|
85
|
-
hydration
|
|
86
|
+
parkCursor(hydration, parent, matchedEnd)
|
|
86
87
|
end = openMarker(parent, RANGE_CLOSE)
|
|
87
88
|
if (chosen !== undefined) {
|
|
88
89
|
dispose = group.track(fillBefore(end, chosen))
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { claimMarker } from '../runtime/claimMarker.ts'
|
|
2
2
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
3
3
|
|
|
4
4
|
/*
|
|
@@ -12,9 +12,7 @@ rather than a list of single nodes.
|
|
|
12
12
|
export function openMarker(parent: Node, data: string, before: Node | null = null): Comment {
|
|
13
13
|
const hydration = RENDER.hydration
|
|
14
14
|
if (hydration !== undefined) {
|
|
15
|
-
|
|
16
|
-
hydration.next.set(parent, node.nextSibling)
|
|
17
|
-
return node
|
|
15
|
+
return claimMarker(hydration, parent, `control-flow marker "${data}"`)
|
|
18
16
|
}
|
|
19
17
|
const node = document.createComment(data)
|
|
20
18
|
/* `before` (a node already in `parent`) places the block among static siblings —
|
package/src/lib/ui/dom/outlet.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { claimMarker } from '../runtime/claimMarker.ts'
|
|
2
2
|
import { OUTLET_CLOSE, OUTLET_OPEN } from '../runtime/OUTLET_MARKER.ts'
|
|
3
3
|
import { PENDING_OUTLET } from '../runtime/PENDING_OUTLET.ts'
|
|
4
|
+
import { parkCursor } from '../runtime/parkCursor.ts'
|
|
4
5
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
5
6
|
import { commentData } from './commentData.ts'
|
|
6
7
|
|
|
@@ -30,7 +31,7 @@ export function outlet(
|
|
|
30
31
|
PENDING_OUTLET.current = { open, close }
|
|
31
32
|
return { open, close }
|
|
32
33
|
}
|
|
33
|
-
const open =
|
|
34
|
+
const open = claimMarker(hydration, parent, 'outlet open marker')
|
|
34
35
|
/* Skip to the matching close: depth-count outlet markers (nested child-layer slots
|
|
35
36
|
are balanced), so a layout wrapping another layout skips its WHOLE subtree. */
|
|
36
37
|
let depth = 1
|
|
@@ -56,7 +57,7 @@ export function outlet(
|
|
|
56
57
|
)
|
|
57
58
|
}
|
|
58
59
|
const close = node as Comment
|
|
59
|
-
hydration
|
|
60
|
+
parkCursor(hydration, parent, close.nextSibling)
|
|
60
61
|
PENDING_OUTLET.current = { open, close }
|
|
61
62
|
return { open, close }
|
|
62
63
|
}
|
|
@@ -1,35 +1,50 @@
|
|
|
1
1
|
import { isAsyncCell } from '../../shared/isAsyncCell.ts'
|
|
2
2
|
import type { AsyncComputed } from '../../shared/types/AsyncComputed.ts'
|
|
3
3
|
import { AsyncCellError } from '../runtime/AsyncCellError.ts'
|
|
4
|
+
import { SuspenseSignal } from '../runtime/SuspenseSignal.ts'
|
|
4
5
|
|
|
5
6
|
/*
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
reader (a function) is called, and a sync
|
|
9
|
-
shape lets a `linked`/`computed` binding
|
|
10
|
-
settling promise, a stream, or a plain value —
|
|
7
|
+
The one read for a `computed`/`linked` reference the compiler lowers to `$$readCell(NAME)`,
|
|
8
|
+
everywhere — script and template, both sides. An async cell (`AsyncComputed`/`AsyncState`)
|
|
9
|
+
yields its latest retained value, a `derive` reader (a function) is called, and a sync
|
|
10
|
+
`Computed`/`State` yields its `.value`. One read shape lets a `linked`/`computed` binding
|
|
11
|
+
auto-track whichever source it resolved to — a settling promise, a stream, or a plain value —
|
|
12
|
+
with no read-site branching in codegen.
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
Whether a read PAUSES is a property of the NODE, not the read site (ADR-0042). A pending
|
|
15
|
+
blocking cell (`cell.blocking` — author `await`, the one that joins the SSR barrier) throws
|
|
16
|
+
a `SuspenseSignal`: the reader's branch of the graph pauses until the value resolves, and
|
|
17
|
+
the throwing read subscribed the reader to the cell so it re-runs on settle. A pending
|
|
18
|
+
STREAMING cell peeks `undefined` (composes with `?.`/`??`). So a rendering region withholds,
|
|
19
|
+
a derive over it pauses too, and the SSR barrier awaits it — all off the same node bit,
|
|
20
|
+
with no per-site knowledge threaded through the compiler.
|
|
21
|
+
|
|
22
|
+
For a settled async cell this stays the value-aware **throwing peek** (ADR-0019 D3.2):
|
|
14
23
|
- error AND no retained value → throw `AsyncCellError` (carries the cell for keep-the-watch);
|
|
15
24
|
- error WITH a retained value → the value (a failed background refresh stays visible — SWR);
|
|
16
|
-
- pending (no value) → `undefined`;
|
|
17
25
|
- resolved → the value.
|
|
18
|
-
The
|
|
26
|
+
The throws are codegen behaviour only — `cell.peek()`/`cell.error()` in JS never throw; the
|
|
19
27
|
author handles an error locally via the probes, or lets it reach a `{#try}`.
|
|
20
28
|
*/
|
|
21
29
|
// @documentation plumbing
|
|
22
30
|
export function readCell(cell: unknown): unknown {
|
|
23
31
|
if (isAsyncCell(cell)) {
|
|
24
|
-
const asyncCell = cell as AsyncComputed<unknown>
|
|
32
|
+
const asyncCell = cell as AsyncComputed<unknown> & { blocking?: boolean }
|
|
25
33
|
const value = asyncCell.peek()
|
|
26
34
|
/* A retained value wins over an error (SWR) and over pending — return it. Only when
|
|
27
|
-
nothing is retained does an error
|
|
35
|
+
nothing is retained does the cell's state decide: an error is "nothing to render"
|
|
36
|
+
(throw to `{#try}`); a pending blocking cell PAUSES (throw suspense); a pending
|
|
37
|
+
streaming cell — or a blocking cell that legitimately resolved to `undefined` — is
|
|
38
|
+
the bare `undefined`. `pending()` (in flight, no value) gates the suspend so a
|
|
39
|
+
resolved-`undefined` blocking cell never suspends forever. */
|
|
28
40
|
if (value === undefined) {
|
|
29
41
|
const error = asyncCell.error()
|
|
30
42
|
if (error !== undefined) {
|
|
31
43
|
throw new AsyncCellError(asyncCell, error)
|
|
32
44
|
}
|
|
45
|
+
if (asyncCell.blocking === true && asyncCell.pending()) {
|
|
46
|
+
throw new SuspenseSignal(asyncCell)
|
|
47
|
+
}
|
|
33
48
|
}
|
|
34
49
|
return value
|
|
35
50
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { walkAnchorOrder } from '../compile/walkAnchorOrder.ts'
|
|
2
2
|
import { walkElementOrder } from '../compile/walkElementOrder.ts'
|
|
3
|
-
import {
|
|
3
|
+
import { claimRun } from '../runtime/claimRun.ts'
|
|
4
4
|
import { HOLE_ATTRIBUTE } from '../runtime/HOLE_ATTRIBUTE.ts'
|
|
5
5
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
6
6
|
import { commentData } from './commentData.ts'
|
|
@@ -121,12 +121,7 @@ export function skeleton(parent: Node, html: string): SkeletonHoles {
|
|
|
121
121
|
const hydration = RENDER.hydration
|
|
122
122
|
const topLevel: Node[] = []
|
|
123
123
|
if (hydration !== undefined) {
|
|
124
|
-
|
|
125
|
-
for (let count = 0; count < topLevelCount && node !== null; count += 1) {
|
|
126
|
-
topLevel.push(node)
|
|
127
|
-
node = node.nextSibling
|
|
128
|
-
}
|
|
129
|
-
hydration.next.set(parent, node)
|
|
124
|
+
claimRun(hydration, parent, topLevelCount, topLevel)
|
|
130
125
|
} else {
|
|
131
126
|
const children = source.childNodes
|
|
132
127
|
/* Stage clones in a fragment ONLY for a live (connected) parent, where one
|
|
@@ -11,7 +11,8 @@ to the default (`match` undefined); the chosen case's content lives in a RANGE
|
|
|
11
11
|
bounded by two comment markers, so a case holds any content. Staying on the same
|
|
12
12
|
case across a subject change leaves it mounted; switching clears the range and
|
|
13
13
|
builds the new case fresh. See `mountSwappableRange` for the shared
|
|
14
|
-
hydrate/swap/teardown semantics — the case index is the swap key
|
|
14
|
+
hydrate/swap/teardown semantics — the case index is the swap key, which doubles as
|
|
15
|
+
the case's render-path segment (`caseSegment` — the alphabet SSR also emits).
|
|
15
16
|
*/
|
|
16
17
|
// @documentation plumbing
|
|
17
18
|
export function switchBlock(
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import { effect } from '../effect.ts'
|
|
2
2
|
import { AsyncCellError } from '../runtime/AsyncCellError.ts'
|
|
3
3
|
import { CURRENT_BOUNDARY } from '../runtime/CURRENT_BOUNDARY.ts'
|
|
4
|
-
import {
|
|
4
|
+
import { claimMarker } from '../runtime/claimMarker.ts'
|
|
5
5
|
import { OWNER } from '../runtime/OWNER.ts'
|
|
6
6
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
7
|
-
import { scopeGroup } from '../runtime/scopeGroup.ts'
|
|
8
7
|
import { SuspenseSignal } from '../runtime/SuspenseSignal.ts'
|
|
8
|
+
import { scopeGroup } from '../runtime/scopeGroup.ts'
|
|
9
9
|
import type { Boundary } from '../runtime/types/Boundary.ts'
|
|
10
10
|
import type { State } from '../runtime/types/State.ts'
|
|
11
|
-
import { withoutHydration } from '../runtime/withoutHydration.ts'
|
|
12
11
|
import { state } from '../state.ts'
|
|
13
12
|
import { anchoredBranch } from './anchoredBranch.ts'
|
|
14
|
-
import {
|
|
13
|
+
import { discardAndRebuild } from './discardAndRebuild.ts'
|
|
15
14
|
|
|
16
15
|
/*
|
|
17
16
|
Reactive error boundary — the runtime for `<template try>` (ADR-0019 D3). Unlike the old
|
|
@@ -191,9 +190,7 @@ export function tryBlock(
|
|
|
191
190
|
firstHydrate()
|
|
192
191
|
return
|
|
193
192
|
}
|
|
194
|
-
|
|
195
|
-
branch.anchor = anchorNode
|
|
196
|
-
parent.insertBefore(anchorNode, before)
|
|
193
|
+
branch.parkAnchor(before)
|
|
197
194
|
showTry()
|
|
198
195
|
|
|
199
196
|
/* The first run when hydrating: claim the boundary's open marker, adopt the guarded
|
|
@@ -202,26 +199,25 @@ export function tryBlock(
|
|
|
202
199
|
throws too) discards the boundary and builds the catch fresh. */
|
|
203
200
|
function firstHydrate(): void {
|
|
204
201
|
const cursor = hydration as NonNullable<typeof hydration>
|
|
205
|
-
const open =
|
|
202
|
+
const open = claimMarker(cursor, parent, `abide:try:${id} open marker`)
|
|
206
203
|
try {
|
|
207
|
-
adopt(
|
|
204
|
+
adopt()
|
|
208
205
|
} catch (error) {
|
|
209
206
|
rebuildCold(open, error)
|
|
210
207
|
}
|
|
211
208
|
}
|
|
212
209
|
|
|
213
|
-
/* Adopt the guarded branch in place
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
`firstHydrate`
|
|
218
|
-
|
|
210
|
+
/* Adopt the guarded branch in place — the cursor already sits past the boundary open
|
|
211
|
+
(claiming it advanced): claim the server's `[` range-open marker, build the guarded
|
|
212
|
+
content (which claims its nodes) under the boundary, then claim the `]` range-close
|
|
213
|
+
and the boundary close, parking an anchor so a later swap detaches this range like
|
|
214
|
+
any other. A build throw disposes the partial scope and rethrows so `firstHydrate`
|
|
215
|
+
rebuilds cold. */
|
|
216
|
+
function adopt(): void {
|
|
219
217
|
const cursor = hydration as NonNullable<typeof hydration>
|
|
220
|
-
cursor.next.set(parent, open.nextSibling ?? null)
|
|
221
218
|
/* The server emits the `[ … ]` range inside the boundary — claim `[` as the live
|
|
222
|
-
range start,
|
|
223
|
-
const start =
|
|
224
|
-
cursor.next.set(parent, start.nextSibling ?? null)
|
|
219
|
+
range start, advancing the cursor to the first content node. */
|
|
220
|
+
const start = claimMarker(cursor, parent, `abide:try:${id} range-open marker`)
|
|
225
221
|
/* Install the boundary around the whole adopt (build + marker claims); the shared
|
|
226
222
|
strand-dispose guard in `adoptStrand` disposes the partial scope and rethrows on any
|
|
227
223
|
failure, and this `finally` restores the boundary before the rethrow reaches
|
|
@@ -234,17 +230,9 @@ export function tryBlock(
|
|
|
234
230
|
branch.adoptStrand(
|
|
235
231
|
(host) => renderTry(host),
|
|
236
232
|
() => {
|
|
237
|
-
const end =
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
`abide:try:${id} range-close marker`,
|
|
241
|
-
) as Comment
|
|
242
|
-
cursor.next.set(parent, end.nextSibling ?? null)
|
|
243
|
-
const close = claimExpected(cursor, parent, `/abide:try:${id} close marker`)
|
|
244
|
-
cursor.next.set(parent, close.nextSibling ?? null)
|
|
245
|
-
const anchorNode = document.createTextNode('')
|
|
246
|
-
parent.insertBefore(anchorNode, close)
|
|
247
|
-
branch.anchor = anchorNode
|
|
233
|
+
const end = claimMarker(cursor, parent, `abide:try:${id} range-close marker`)
|
|
234
|
+
const close = claimMarker(cursor, parent, `/abide:try:${id} close marker`)
|
|
235
|
+
branch.parkAnchor(close)
|
|
248
236
|
return { start, end }
|
|
249
237
|
},
|
|
250
238
|
)
|
|
@@ -258,18 +246,18 @@ export function tryBlock(
|
|
|
258
246
|
No catch → rethrow (the throw surfaces past the boundary, as the sync version did). */
|
|
259
247
|
function rebuildCold(open: Node, error: unknown): void {
|
|
260
248
|
branch.detach()
|
|
261
|
-
|
|
249
|
+
discardAndRebuild(
|
|
250
|
+
hydration as NonNullable<typeof hydration>,
|
|
262
251
|
parent,
|
|
263
252
|
open,
|
|
264
253
|
`/abide:try:${id}`,
|
|
265
|
-
|
|
254
|
+
(after) => {
|
|
255
|
+
if (!hasCatch) {
|
|
256
|
+
throw error
|
|
257
|
+
}
|
|
258
|
+
branch.parkAnchor(after)
|
|
259
|
+
showCatch(error)
|
|
260
|
+
},
|
|
266
261
|
)
|
|
267
|
-
if (!hasCatch) {
|
|
268
|
-
throw error
|
|
269
|
-
}
|
|
270
|
-
const anchorNode = document.createTextNode('')
|
|
271
|
-
branch.anchor = anchorNode
|
|
272
|
-
parent.insertBefore(anchorNode, after)
|
|
273
|
-
withoutHydration(() => showCatch(error))
|
|
274
262
|
}
|
|
275
263
|
}
|
package/src/lib/ui/dom/when.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ELSE_SEGMENT, THEN_SEGMENT } from '../runtime/BRANCH_SEGMENT.ts'
|
|
1
2
|
import { SuspenseSignal } from '../runtime/SuspenseSignal.ts'
|
|
2
3
|
import { mountSwappableRange } from './mountSwappableRange.ts'
|
|
3
4
|
|
|
@@ -37,7 +38,9 @@ export function when(
|
|
|
37
38
|
block (render neither branch) until it settles, exactly as a bare async subject holds;
|
|
38
39
|
the read tracked its cell, so the swap effect re-runs on resolve. */
|
|
39
40
|
try {
|
|
40
|
-
|
|
41
|
+
/* The branch key doubles as the branch's render-path segment (see
|
|
42
|
+
BRANCH_SEGMENT), so it is sourced from the shared alphabet SSR also emits. */
|
|
43
|
+
return condition() ? THEN_SEGMENT : ELSE_SEGMENT
|
|
41
44
|
} catch (signal) {
|
|
42
45
|
if (!(signal instanceof SuspenseSignal)) {
|
|
43
46
|
throw signal
|
|
@@ -45,7 +48,8 @@ export function when(
|
|
|
45
48
|
return 'pending'
|
|
46
49
|
}
|
|
47
50
|
},
|
|
48
|
-
(branch) =>
|
|
51
|
+
(branch) =>
|
|
52
|
+
branch === THEN_SEGMENT ? render : branch === ELSE_SEGMENT ? renderElse : undefined,
|
|
49
53
|
before,
|
|
50
54
|
/* An async subject (compiler supplies `isPending`) can render a different branch on the
|
|
51
55
|
server than the pending client, so hydration must not adopt the SSR range in place. */
|
package/src/lib/ui/linked.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { NamedAsyncIterable } from '../shared/types/NamedAsyncIterable.ts'
|
|
|
4
4
|
import { createAsyncCell } from './runtime/createAsyncCell.ts'
|
|
5
5
|
import { createEffectNode } from './runtime/createEffectNode.ts'
|
|
6
6
|
import { isAsyncFunction } from './runtime/isAsyncFunction.ts'
|
|
7
|
+
import { routeBareSeed } from './runtime/routeBareSeed.ts'
|
|
7
8
|
import type { State } from './runtime/types/State.ts'
|
|
8
9
|
import { state } from './state.ts'
|
|
9
10
|
|
|
@@ -41,6 +42,19 @@ export function linked<T>(
|
|
|
41
42
|
transform?: (next: T, previous: T) => T,
|
|
42
43
|
): State<T> | AsyncState<T> {
|
|
43
44
|
const coerce = transform as ((next: unknown, previous: unknown) => unknown) | undefined
|
|
45
|
+
/* A BARE seed that reached the runtime literally (a branch-nested `<script>`, a direct
|
|
46
|
+
JS caller) — the compile-time `wrapSeed` normalization only covers the leading script,
|
|
47
|
+
so honor the authored contract (ADR-0045) by VALUE via the shared `routeBareSeed`,
|
|
48
|
+
exactly as `computed` does; a non-async value becomes a constant seed thunk (reseeds
|
|
49
|
+
never fire — nothing tracked — matching the wrapped `() => value` form). */
|
|
50
|
+
if (typeof seed !== 'function') {
|
|
51
|
+
const value: unknown = seed
|
|
52
|
+
const cell = routeBareSeed(value, true, coerce)
|
|
53
|
+
if (cell !== undefined) {
|
|
54
|
+
return cell as AsyncState<T>
|
|
55
|
+
}
|
|
56
|
+
seed = () => value as T
|
|
57
|
+
}
|
|
44
58
|
/* `await` marker: an async-function seed unwraps its promise into a writable async cell. */
|
|
45
59
|
if (isAsyncFunction(seed)) {
|
|
46
60
|
return createAsyncCell(seed as () => unknown, {
|
|
@@ -23,5 +23,5 @@ export function resumeSeedScript(resume: Record<string, ResumeEntry>): string {
|
|
|
23
23
|
if (encoded.length === 0) {
|
|
24
24
|
return ''
|
|
25
25
|
}
|
|
26
|
-
return `<script>Object.assign(window.
|
|
26
|
+
return `<script>Object.assign((window.__abideSeeds=window.__abideSeeds||{}).resume=window.__abideSeeds.resume||{},${safeJsonForScript(Object.fromEntries(encoded))})</script>`
|
|
27
27
|
}
|
package/src/lib/ui/router.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { hydrationWindow } from '../shared/hydrationWindow.ts'
|
|
2
1
|
import { layoutChainForRoute } from '../shared/layoutChainForRoute.ts'
|
|
3
2
|
import { matchRoute } from '../shared/matchRoute.ts'
|
|
4
3
|
import { fillBoundary } from './dom/fillBoundary.ts'
|
|
@@ -7,11 +6,10 @@ import { effect } from './effect.ts'
|
|
|
7
6
|
import { navigatePath } from './navigate.ts'
|
|
8
7
|
import { CHILD_PRESENT } from './runtime/CHILD_PRESENT.ts'
|
|
9
8
|
import { clientPage } from './runtime/clientPage.ts'
|
|
10
|
-
import { enterRenderPass } from './runtime/enterRenderPass.ts'
|
|
11
|
-
import { exitRenderPass } from './runtime/exitRenderPass.ts'
|
|
12
9
|
import { historyEntries } from './runtime/historyEntries.ts'
|
|
13
10
|
import { PENDING_OUTLET } from './runtime/PENDING_OUTLET.ts'
|
|
14
11
|
import { RENDER } from './runtime/RENDER.ts'
|
|
12
|
+
import { runHydrationPass } from './runtime/runHydrationPass.ts'
|
|
15
13
|
import { runtimePath } from './runtime/runtimePath.ts'
|
|
16
14
|
import type { AbideHistoryState } from './runtime/types/AbideHistoryState.ts'
|
|
17
15
|
import type { NavVerdict } from './runtime/types/NavVerdict.ts'
|
|
@@ -257,19 +255,10 @@ export function router(
|
|
|
257
255
|
mountedPageKey = pageKey
|
|
258
256
|
}
|
|
259
257
|
if (hydrating) {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
try {
|
|
265
|
-
run()
|
|
266
|
-
} finally {
|
|
267
|
-
exitRenderPass()
|
|
268
|
-
RENDER.hydration = previous
|
|
269
|
-
/* Outermost exit clears the window and wakes the peeks this pass withheld
|
|
270
|
-
for SSR congruence, now that the warm value is congruent to show. */
|
|
271
|
-
hydrationWindow.exit()
|
|
272
|
-
}
|
|
258
|
+
/* The pass lifecycle (claim cursor, withhold window, render pass, and the
|
|
259
|
+
warm-seed restore a throwing pass owes its cold rebuild) is owned by
|
|
260
|
+
`runHydrationPass` — the same bracket `hydrate` runs. */
|
|
261
|
+
runHydrationPass(run)
|
|
273
262
|
return
|
|
274
263
|
}
|
|
275
264
|
run()
|
|
@@ -566,6 +555,13 @@ export function router(
|
|
|
566
555
|
disposeFrom(0)
|
|
567
556
|
rootBoundary = undefined
|
|
568
557
|
mountedPageKey = undefined
|
|
558
|
+
/* The cell/doc warm seeds the failed pass adopted are still in place —
|
|
559
|
+
a pass only MARKS them, deleting on a clean exit (`consumeSeed`,
|
|
560
|
+
ADR-0048) — so this cold rebuild re-adopts the SSR-resolved values
|
|
561
|
+
instead of refetching (a cold refetch would leave blocking `await`
|
|
562
|
+
cells pending, and a top-level `$$readCell` of one sits in no suspense
|
|
563
|
+
region — its SuspenseSignal would escape this rebuild and kill the
|
|
564
|
+
mount). */
|
|
569
565
|
buildFrom(0, chainKeys, layoutViews, pageView, key, params, false)
|
|
570
566
|
}
|
|
571
567
|
/* Reapply the destination entry's scroll once its DOM exists — a
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { messageFromError } from '../../shared/messageFromError.ts'
|
|
1
2
|
import type { AsyncComputed } from '../../shared/types/AsyncComputed.ts'
|
|
2
3
|
|
|
3
4
|
/*
|
|
@@ -13,7 +14,7 @@ export class AsyncCellError extends Error {
|
|
|
13
14
|
readonly cell: AsyncComputed<unknown>
|
|
14
15
|
|
|
15
16
|
constructor(cell: AsyncComputed<unknown>, cause: unknown) {
|
|
16
|
-
super(
|
|
17
|
+
super(messageFromError(cause), { cause })
|
|
17
18
|
this.name = 'AsyncCellError'
|
|
18
19
|
this.cell = cell
|
|
19
20
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/*
|
|
2
|
+
The render-path segment alphabet for control-flow branches — the one place the segment a
|
|
3
|
+
branch's content composes under is decided, read by BOTH sides (the same pattern
|
|
4
|
+
`RANGE_MARKER` uses for the wire markers, so a literal can never drift):
|
|
5
|
+
|
|
6
|
+
- the client runtimes (`when` returns THEN/ELSE as its swap key, `switchBlock` its case
|
|
7
|
+
index) hand the key to `mountSwappableRange`, which pushes `String(key)` as the branch's
|
|
8
|
+
path segment;
|
|
9
|
+
- the SSR back-end (`generateSSR`) emits the same segment around a branch's content
|
|
10
|
+
(`withPathBranch`), so a component/cell inside composes a byte-identical
|
|
11
|
+
serialization-stable id — the warm-seed key (ADR-0033).
|
|
12
|
+
|
|
13
|
+
A `when` keys its branches by name; a `switchBlock` (a `{#switch}`, or an `{#if}` chain with
|
|
14
|
+
any `{:elseif}`) keys each case by its index in the `[then, ...branches]` source order —
|
|
15
|
+
the default at its own position.
|
|
16
|
+
*/
|
|
17
|
+
export const THEN_SEGMENT = 'then'
|
|
18
|
+
export const ELSE_SEGMENT = 'else'
|
|
19
|
+
|
|
20
|
+
/* A switchBlock case's segment: its source-order index, stringified exactly as
|
|
21
|
+
`mountSwappableRange` stringifies the numeric swap key. */
|
|
22
|
+
export function caseSegment(index: number): string {
|
|
23
|
+
return String(index)
|
|
24
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { SEEDS } from '../../shared/SEEDS.ts'
|
|
2
|
+
|
|
1
3
|
/* The async-cell warm-seed manifest: an SSR-resolved cell value, keyed by the cell's
|
|
2
4
|
serialization-stable render-path id (`${scope.id}:${index}`), as a ref-json-encoded STRING
|
|
3
5
|
(decoded lazily at the read site in `createAsyncCell`, so an in-process graph with cycles /
|
|
@@ -5,10 +7,6 @@
|
|
|
5
7
|
`startClient` drains `__SSR__.cells` into here before mount; a hydrating cell reads its key to
|
|
6
8
|
seed its value warm instead of re-running the seed (no refetch, no pending flash).
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
store — whoever runs first creates it, the other adopts the same reference (mirrors RESUME). */
|
|
10
|
-
const globalScope = globalThis as { __abideCells?: Record<string, string> }
|
|
11
|
-
globalScope.__abideCells ??= {}
|
|
12
|
-
|
|
10
|
+
The `cells` partition of the one `__abideSeeds` manifest (ADR-0048, see SEEDS). */
|
|
13
11
|
// @documentation plumbing
|
|
14
|
-
export const CELL_SEED: Record<string, string> =
|
|
12
|
+
export const CELL_SEED: Record<string, string> = SEEDS.cells
|