@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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import ts from 'typescript'
|
|
2
2
|
import { escapeKey } from '../runtime/escapeKey.ts'
|
|
3
|
-
import {
|
|
3
|
+
import { lowerCompoundAssignment } from './lowerCompoundAssignment.ts'
|
|
4
|
+
import { lowerUpdateExpression } from './lowerUpdateExpression.ts'
|
|
4
5
|
|
|
5
6
|
/*
|
|
6
7
|
The linchpin compiler pass. Rewrites idiomatic data access on a reactive document
|
|
@@ -91,33 +92,24 @@ export function docAccessTransformer(docName: string): ts.TransformerFactory<ts.
|
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
function visit(node: ts.Node): ts.Node {
|
|
94
|
-
/* Assignment (plain or
|
|
95
|
+
/* Assignment (plain, compound, or logical) to a doc path → replace patch. */
|
|
95
96
|
if (ts.isBinaryExpression(node)) {
|
|
96
97
|
const segments = pathSegments(node.left as ts.Expression)
|
|
97
98
|
if (segments) {
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (binary) {
|
|
107
|
-
const next = ts.factory.createBinaryExpression(
|
|
108
|
-
docCall(docName, 'read', [buildPath(segments)]),
|
|
109
|
-
binary,
|
|
110
|
-
ts.visitNode(node.right, visit) as ts.Expression,
|
|
111
|
-
)
|
|
112
|
-
return docCall(docName, 'replace', [buildPath(segments), next])
|
|
99
|
+
const lowered = lowerCompoundAssignment(
|
|
100
|
+
node.operatorToken.kind,
|
|
101
|
+
() => docCall(docName, 'read', [buildPath(segments)]),
|
|
102
|
+
ts.visitNode(node.right, visit) as ts.Expression,
|
|
103
|
+
(value) => docCall(docName, 'replace', [buildPath(segments), value]),
|
|
104
|
+
)
|
|
105
|
+
if (lowered !== undefined) {
|
|
106
|
+
return lowered
|
|
113
107
|
}
|
|
114
108
|
}
|
|
115
109
|
}
|
|
116
|
-
/* `path++` / `++path` / `path--` on a doc path → a replace patch
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
an lvalue. abide handlers are statements, so the postfix/prefix value is
|
|
120
|
-
discarded and both lower identically. Only `++`/`--` are handled here; other
|
|
110
|
+
/* `path++` / `++path` / `path--` on a doc path → a replace patch. A bare `++`
|
|
111
|
+
would otherwise survive onto the lowered read (`model.read("n")++`) — invalid,
|
|
112
|
+
since a call result is not an lvalue. Only `++`/`--` are handled here; other
|
|
121
113
|
unary operators (`!path`, `-path`) fall through so their operand lowers as a
|
|
122
114
|
normal read. */
|
|
123
115
|
if (
|
|
@@ -127,16 +119,12 @@ export function docAccessTransformer(docName: string): ts.TransformerFactory<ts.
|
|
|
127
119
|
) {
|
|
128
120
|
const segments = pathSegments(node.operand)
|
|
129
121
|
if (segments) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
docCall(docName, 'read', [buildPath(segments)]),
|
|
136
|
-
step,
|
|
137
|
-
ts.factory.createNumericLiteral(1),
|
|
122
|
+
return lowerUpdateExpression(
|
|
123
|
+
ts.isPostfixUnaryExpression(node),
|
|
124
|
+
node.operator === ts.SyntaxKind.PlusPlusToken,
|
|
125
|
+
() => docCall(docName, 'read', [buildPath(segments)]),
|
|
126
|
+
(value) => docCall(docName, 'replace', [buildPath(segments), value]),
|
|
138
127
|
)
|
|
139
|
-
return docCall(docName, 'replace', [buildPath(segments), next])
|
|
140
128
|
}
|
|
141
129
|
}
|
|
142
130
|
/* doc array `.push(a, b, …)` → one `add` patch per argument at the array's `-`
|
|
@@ -91,19 +91,10 @@ export function lowerScript(
|
|
|
91
91
|
derivedNames: Set<string>
|
|
92
92
|
computedNames: Set<string>
|
|
93
93
|
cellReadNames: Set<string>
|
|
94
|
-
/* Template-injected + script-level `await` cells (ADR-0042), read via `$$readCellBlocking`. */
|
|
95
|
-
blockingCellNames: Set<string>
|
|
96
94
|
droppedReactiveImports: Set<string>
|
|
97
95
|
} {
|
|
98
96
|
const source = ts.createSourceFile('component.ts', scriptBody, ts.ScriptTarget.Latest, true)
|
|
99
|
-
const {
|
|
100
|
-
transformer,
|
|
101
|
-
stateNames,
|
|
102
|
-
derivedNames,
|
|
103
|
-
computedNames,
|
|
104
|
-
cellReadNames,
|
|
105
|
-
blockingCellNames: allBlockingNames,
|
|
106
|
-
} = desugarSignals(
|
|
97
|
+
const { transformer, stateNames, derivedNames, computedNames, cellReadNames } = desugarSignals(
|
|
107
98
|
source,
|
|
108
99
|
injectedCellNames,
|
|
109
100
|
blockingCellNames,
|
|
@@ -194,7 +185,6 @@ export function lowerScript(
|
|
|
194
185
|
derivedNames,
|
|
195
186
|
computedNames,
|
|
196
187
|
cellReadNames,
|
|
197
|
-
blockingCellNames: allBlockingNames,
|
|
198
188
|
droppedReactiveImports,
|
|
199
189
|
}
|
|
200
190
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import ts from 'typescript'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Lowers `x++` / `++x` / `x--` / `--x` on reactive state to a write of the stepped value,
|
|
5
|
+
shared by the `$$model` doc path (lowerDocAccess) and the `$$writeCell` linked-cell path
|
|
6
|
+
(renameSignalRefs). `makeRead`/`makeWrite` mirror `lowerCompoundAssignment`. The bare `++`
|
|
7
|
+
can't survive onto the read form (`$$readCell(x)` / `$$model.read(...)` are calls, not
|
|
8
|
+
lvalues), so it becomes an explicit write of `read ± 1`.
|
|
9
|
+
|
|
10
|
+
Prefix evaluates to the NEW value, which is exactly what the write returns, so
|
|
11
|
+
`write(read ± 1)` is correct as-is. POSTFIX must evaluate to the PREVIOUS value, so its
|
|
12
|
+
result is corrected back by the opposite step (`x++` → `write(read + 1) - 1`) — no temp or
|
|
13
|
+
closure, since the write returns the written value (`Cell.set` / `Doc.replace`). The
|
|
14
|
+
correction is dead when the value is discarded (a statement `x++;`), so it costs nothing
|
|
15
|
+
there.
|
|
16
|
+
*/
|
|
17
|
+
export function lowerUpdateExpression(
|
|
18
|
+
isPostfix: boolean,
|
|
19
|
+
isIncrement: boolean,
|
|
20
|
+
makeRead: () => ts.Expression,
|
|
21
|
+
makeWrite: (value: ts.Expression) => ts.Expression,
|
|
22
|
+
): ts.Expression {
|
|
23
|
+
const step = isIncrement ? ts.SyntaxKind.PlusToken : ts.SyntaxKind.MinusToken
|
|
24
|
+
const write = makeWrite(
|
|
25
|
+
ts.factory.createBinaryExpression(makeRead(), step, ts.factory.createNumericLiteral(1)),
|
|
26
|
+
)
|
|
27
|
+
if (!isPostfix) {
|
|
28
|
+
return write
|
|
29
|
+
}
|
|
30
|
+
const correction = isIncrement ? ts.SyntaxKind.MinusToken : ts.SyntaxKind.PlusToken
|
|
31
|
+
return ts.factory.createBinaryExpression(write, correction, ts.factory.createNumericLiteral(1))
|
|
32
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import ts from 'typescript'
|
|
2
|
-
import {
|
|
2
|
+
import { lowerCompoundAssignment } from './lowerCompoundAssignment.ts'
|
|
3
|
+
import { lowerUpdateExpression } from './lowerUpdateExpression.ts'
|
|
3
4
|
|
|
4
5
|
/*
|
|
5
6
|
Rewrites references to a component's signal bindings into the document form the
|
|
@@ -53,12 +54,10 @@ export function signalRefsTransformer(
|
|
|
53
54
|
blockLocalPlain: ReadonlySet<string> = new Set(),
|
|
54
55
|
/* Component signals read through `$$readCell(name)` — every `linked` and every async
|
|
55
56
|
`computed` (see `desugarSignals`). A nearer lexical binding of the same name shadows
|
|
56
|
-
them like any other signal.
|
|
57
|
+
them like any other signal. Whether a read PAUSES (blocking `await` cell) or peeks
|
|
58
|
+
(streaming) is decided at runtime by the cell itself (`readCell` consults `cell.blocking`),
|
|
59
|
+
so there is one read form here for every cell and nothing site-specific to thread. */
|
|
57
60
|
cellReadNames: ReadonlySet<string> = new Set(),
|
|
58
|
-
/* The subset of `cellReadNames` that are BLOCKING `await` cells (ADR-0042), read through
|
|
59
|
-
`$$readCellBlocking(name)` (suspend-on-pending) instead of `$$readCell`. Non-empty only on
|
|
60
|
-
the CLIENT template lowering; the script pass and SSR keep it empty (`$$readCell`). */
|
|
61
|
-
blockingCellNames: ReadonlySet<string> = new Set(),
|
|
62
61
|
): ts.TransformerFactory<ts.SourceFile> {
|
|
63
62
|
/* The signal names that a nested binding can shadow — only these matter for
|
|
64
63
|
scope tracking, so we ignore every other local binding. */
|
|
@@ -120,21 +119,19 @@ export function signalRefsTransformer(
|
|
|
120
119
|
!shadowed.has(node.left.text)
|
|
121
120
|
) {
|
|
122
121
|
const target = node.left.text
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
ts.factory.createBinaryExpression(cellReadCall(target), binary, right),
|
|
132
|
-
)
|
|
122
|
+
const lowered = lowerCompoundAssignment(
|
|
123
|
+
node.operatorToken.kind,
|
|
124
|
+
() => cellReadCall(target),
|
|
125
|
+
ts.visitNode(node.right, visit) as ts.Expression,
|
|
126
|
+
(value) => writeCellCall(target, value),
|
|
127
|
+
)
|
|
128
|
+
if (lowered !== undefined) {
|
|
129
|
+
return lowered
|
|
133
130
|
}
|
|
134
131
|
}
|
|
135
132
|
/* `draft++` / `++draft` / `draft--` on a `linked` cell → a `$$writeCell` of the
|
|
136
|
-
stepped value
|
|
137
|
-
|
|
133
|
+
stepped value (the bare `++` would otherwise land on `$$readCell(draft)`, an
|
|
134
|
+
invalid lvalue). */
|
|
138
135
|
if (
|
|
139
136
|
(ts.isPostfixUnaryExpression(node) || ts.isPrefixUnaryExpression(node)) &&
|
|
140
137
|
(node.operator === ts.SyntaxKind.PlusPlusToken ||
|
|
@@ -143,17 +140,12 @@ export function signalRefsTransformer(
|
|
|
143
140
|
cellReadNames.has(node.operand.text) &&
|
|
144
141
|
!shadowed.has(node.operand.text)
|
|
145
142
|
) {
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
ts.factory.createBinaryExpression(
|
|
153
|
-
cellReadCall(node.operand.text),
|
|
154
|
-
step,
|
|
155
|
-
ts.factory.createNumericLiteral(1),
|
|
156
|
-
),
|
|
143
|
+
const target = node.operand.text
|
|
144
|
+
return lowerUpdateExpression(
|
|
145
|
+
ts.isPostfixUnaryExpression(node),
|
|
146
|
+
node.operator === ts.SyntaxKind.PlusPlusToken,
|
|
147
|
+
() => cellReadCall(target),
|
|
148
|
+
(value) => writeCellCall(target, value),
|
|
157
149
|
)
|
|
158
150
|
}
|
|
159
151
|
/* Shorthand `{ count }` → `{ count: $$model.count }` / `{ total: total.value }`,
|
|
@@ -166,7 +158,6 @@ export function signalRefsTransformer(
|
|
|
166
158
|
computedNames,
|
|
167
159
|
blockLocal,
|
|
168
160
|
cellReadNames,
|
|
169
|
-
blockingCellNames,
|
|
170
161
|
)
|
|
171
162
|
if (replacement !== undefined) {
|
|
172
163
|
return ts.factory.createPropertyAssignment(node.name.text, replacement)
|
|
@@ -180,7 +171,6 @@ export function signalRefsTransformer(
|
|
|
180
171
|
computedNames,
|
|
181
172
|
blockLocal,
|
|
182
173
|
cellReadNames,
|
|
183
|
-
blockingCellNames,
|
|
184
174
|
)
|
|
185
175
|
if (replacement !== undefined) {
|
|
186
176
|
return replacement
|
|
@@ -399,7 +389,6 @@ function referenceFor(
|
|
|
399
389
|
computedNames: ReadonlySet<string>,
|
|
400
390
|
blockLocal: ReadonlySet<string> = new Set(),
|
|
401
391
|
cellReadNames: ReadonlySet<string> = new Set(),
|
|
402
|
-
blockingCellNames: ReadonlySet<string> = new Set(),
|
|
403
392
|
): ts.Expression | undefined {
|
|
404
393
|
if (blockLocal.has(name)) {
|
|
405
394
|
return ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier(name), 'value')
|
|
@@ -409,19 +398,10 @@ function referenceFor(
|
|
|
409
398
|
if (name === 'scope') {
|
|
410
399
|
return ts.factory.createIdentifier('$$scope')
|
|
411
400
|
}
|
|
412
|
-
/* A
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
if (blockingCellNames.has(name)) {
|
|
417
|
-
return ts.factory.createCallExpression(
|
|
418
|
-
ts.factory.createIdentifier('$$readCellBlocking'),
|
|
419
|
-
undefined,
|
|
420
|
-
[ts.factory.createIdentifier(name)],
|
|
421
|
-
)
|
|
422
|
-
}
|
|
423
|
-
/* A `linked` / async `computed` cell → `$$readCell(name)`: one read shape that peeks an
|
|
424
|
-
async cell and reads `.value` off a sync one. */
|
|
401
|
+
/* A `linked` / async `computed` cell → `$$readCell(name)`: ONE read shape for every cell,
|
|
402
|
+
script and template. It peeks a sync cell's `.value`, and for an async cell decides at
|
|
403
|
+
runtime whether to pause (blocking `await`) or peek `undefined` (streaming) by the cell's
|
|
404
|
+
own `blocking` bit — so no blocking/streaming distinction is threaded through codegen. */
|
|
425
405
|
if (cellReadNames.has(name)) {
|
|
426
406
|
return ts.factory.createCallExpression(
|
|
427
407
|
ts.factory.createIdentifier('$$readCell'),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { cachedShadowProgram } from './cachedShadowProgram.ts'
|
|
1
2
|
import { classifyInterpolationType } from './classifyInterpolationType.ts'
|
|
2
|
-
import {
|
|
3
|
+
import type { ShadowProgram } from './createShadowProgram.ts'
|
|
3
4
|
import { nodeAtShadowOffset } from './nodeAtShadowOffset.ts'
|
|
4
5
|
import { shadowNaming } from './shadowNaming.ts'
|
|
5
6
|
import { sourceToShadowOffset } from './sourceToShadowOffset.ts'
|
|
@@ -18,25 +19,19 @@ as `state.computed` — either way the seed sub-expression maps back to source a
|
|
|
18
19
|
own type), so the identical `sourceToShadowOffset → nodeAtShadowOffset → classifyInterpolation
|
|
19
20
|
Type` pipeline the interpolation half uses resolves a seed with no node-finder variant.
|
|
20
21
|
|
|
21
|
-
FAIL-OPEN, but NOT to `'sync'`: if the program can't build
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
mis-route a failed stream seed to the
|
|
22
|
+
FAIL-OPEN, but NOT to `'sync'`: if the program can't build (warned once per root by
|
|
23
|
+
`cachedShadowProgram`), the file has no shadow, the seed's location doesn't map, no node is
|
|
24
|
+
found, or the checker throws, the classifier is absent / returns `undefined` — the signal the
|
|
25
|
+
caller reads as "degrade to `isBareCallComputed`", NOT as "the seed is sync". Collapsing failure
|
|
26
|
+
to `'sync'` (as the interpolation classifier does) would mis-route a failed stream seed to the
|
|
27
|
+
lazy `derive` slot instead of today's syntax heuristic.
|
|
26
28
|
*/
|
|
27
29
|
export function seedTypeClassifierForRoot(
|
|
28
30
|
cache: Map<string, ShadowProgram | undefined>,
|
|
29
31
|
root: string,
|
|
30
32
|
abidePath: string,
|
|
31
33
|
): SeedTypeClassifier | undefined {
|
|
32
|
-
|
|
33
|
-
try {
|
|
34
|
-
cache.set(root, createShadowProgram(root))
|
|
35
|
-
} catch {
|
|
36
|
-
cache.set(root, undefined)
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
const shadowProgram = cache.get(root)
|
|
34
|
+
const shadowProgram = cachedShadowProgram(cache, root)
|
|
40
35
|
if (shadowProgram === undefined) {
|
|
41
36
|
return undefined
|
|
42
37
|
}
|
|
@@ -46,9 +41,9 @@ export function seedTypeClassifierForRoot(
|
|
|
46
41
|
return undefined
|
|
47
42
|
}
|
|
48
43
|
const checker = program.getTypeChecker()
|
|
44
|
+
const mappings = shadows.get(abidePath)?.mappings ?? []
|
|
49
45
|
return (loc, code) => {
|
|
50
46
|
try {
|
|
51
|
-
const mappings = shadows.get(abidePath)?.mappings ?? []
|
|
52
47
|
const offset = sourceToShadowOffset(mappings, loc)
|
|
53
48
|
if (offset === undefined) {
|
|
54
49
|
return undefined
|
|
@@ -28,9 +28,9 @@ export function shadowInterpolationClassifier(
|
|
|
28
28
|
return undefined
|
|
29
29
|
}
|
|
30
30
|
const checker = program.getTypeChecker()
|
|
31
|
+
const mappings = shadows.get(abidePath)?.mappings ?? []
|
|
31
32
|
return (loc, code) => {
|
|
32
33
|
try {
|
|
33
|
-
const mappings = shadows.get(abidePath)?.mappings ?? []
|
|
34
34
|
const offset = sourceToShadowOffset(mappings, loc)
|
|
35
35
|
if (offset === undefined) {
|
|
36
36
|
return 'sync'
|
|
@@ -26,12 +26,10 @@ export type AnalyzedComponent = {
|
|
|
26
26
|
computedNames: Set<string>
|
|
27
27
|
/* Component signals read through `$$readCell(name)`: every `linked` and every async
|
|
28
28
|
`computed`. Threaded to both back-ends so a template reference (`{draft}`) lowers to
|
|
29
|
-
the unified cell read consistently on client and server.
|
|
29
|
+
the unified cell read consistently on client and server. Whether a pending read pauses
|
|
30
|
+
(blocking `await`) or peeks (streaming) is decided at runtime by the cell's own
|
|
31
|
+
`blocking` bit (ADR-0042), so no blocking subset is threaded through codegen. */
|
|
30
32
|
cellReadNames: Set<string>
|
|
31
|
-
/* The subset of `cellReadNames` that are BLOCKING `await` cells (ADR-0042): a template-injected
|
|
32
|
-
`{await X}` cell or a script `computed(await …)`. The CLIENT back-end reads these via
|
|
33
|
-
`$$readCellBlocking` (suspend-on-pending) rather than `$$readCell`. */
|
|
34
|
-
blockingCellNames: Set<string>
|
|
35
33
|
nodes: TemplateNode[]
|
|
36
34
|
/* One entry per non-empty `<style>` in the template (in source order): the scope
|
|
37
35
|
attribute its covered elements carry (annotated onto `nodes`) and the scoped
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import ts from 'typescript'
|
|
2
|
+
import { AbideCompileError } from './AbideCompileError.ts'
|
|
2
3
|
|
|
3
4
|
/*
|
|
4
5
|
Folds a cell-source `watch(source, handler)` into the auto-tracked thunk form
|
|
@@ -51,7 +52,7 @@ export function wrapReactionCellSources(
|
|
|
51
52
|
expression is read ONCE as a plain value (outside any tracking scope) and handed to the
|
|
52
53
|
runtime as a dead value: nothing subscribes, the reaction never fires. It can't be folded
|
|
53
54
|
like a bare cell (a member access isn't a whole-cell subscription), so return the base
|
|
54
|
-
name to
|
|
55
|
+
name to REJECT on and point the author at the corrective forms. */
|
|
55
56
|
function cellMemberBase(source: ts.Expression): string | undefined {
|
|
56
57
|
if (
|
|
57
58
|
(ts.isPropertyAccessExpression(source) || ts.isElementAccessExpression(source)) &&
|
|
@@ -95,19 +96,22 @@ export function wrapReactionCellSources(
|
|
|
95
96
|
)
|
|
96
97
|
return ts.factory.createCallExpression(node.expression, undefined, [thunk])
|
|
97
98
|
}
|
|
98
|
-
/*
|
|
99
|
-
subscribes to nothing (see `cellMemberBase`)
|
|
99
|
+
/* Reject the inert member-expression form `watch(s.foo, handler)` — it looks valid but
|
|
100
|
+
subscribes to nothing (see `cellMemberBase`), so the handler silently never fires. Not
|
|
101
|
+
foldable; a compile error turns the silent no-op into a caught mistake. A member of a
|
|
102
|
+
cell is never itself a cell (no per-property cells), so this can't reject a valid watch. */
|
|
103
|
+
const memberBase = sourceArg !== undefined ? cellMemberBase(sourceArg) : undefined
|
|
100
104
|
if (
|
|
101
105
|
ts.isCallExpression(node) &&
|
|
102
106
|
ts.isIdentifier(node.expression) &&
|
|
103
107
|
watchLocalNames.has(node.expression.text) &&
|
|
104
108
|
node.arguments.length === 2 &&
|
|
105
109
|
sourceArg !== undefined &&
|
|
106
|
-
|
|
110
|
+
memberBase !== undefined
|
|
107
111
|
) {
|
|
108
112
|
const sourceText = sourceArg.getText(root)
|
|
109
|
-
|
|
110
|
-
`[abide] \`watch(${sourceText}, …)\` does not track \`${sourceText}\` — a member access on a cell is read once as a plain value (abide has no per-property cells), so the reaction
|
|
113
|
+
throw new AbideCompileError(
|
|
114
|
+
`[abide] \`watch(${sourceText}, …)\` does not track \`${sourceText}\` — a member access on a cell is read once as a plain value (abide has no per-property cells), so the reaction never fires. Wrap it in a thunk (\`watch(() => …(${sourceText}))\`) or watch the whole cell (\`watch(${memberBase}, v => …)\`).`,
|
|
111
115
|
)
|
|
112
116
|
}
|
|
113
117
|
return ts.visitEachChild(node, visit, context)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import ts from 'typescript'
|
|
2
|
+
import { NESTED_REACTIVE_BINDINGS, resolveReactiveExport } from './resolveReactiveExport.ts'
|
|
3
|
+
import { wrapSeed } from './wrapSeed.ts'
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
Normalises every `state.computed(seed)` / `state.linked(seed)` in a nested branch
|
|
7
|
+
`<script>` so a BARE-VALUE seed becomes a thunk (`state.computed(a * 2)` →
|
|
8
|
+
`state.computed(() => a * 2)`), exactly as the top-level desugar (`desugarSignals` via
|
|
9
|
+
`wrapSeed`) does. A nested script keeps its reactive calls literal — it is not desugared to
|
|
10
|
+
the doc — so absent this the bare value reaches the runtime primitive, which stores it as
|
|
11
|
+
the compute function and calls it on read (`a * 2 is not a function`, a runtime crash). A
|
|
12
|
+
literal `() => …` / `function` thunk passes through unchanged, so the wrap is a no-op on the
|
|
13
|
+
already-thunked form the author usually writes.
|
|
14
|
+
|
|
15
|
+
Recognition is name-based (`NESTED_REACTIVE_BINDINGS`) because a nested script carries no
|
|
16
|
+
import of its own — it inherits the surface by the canonical `state` name, the same way
|
|
17
|
+
`nestedBindingNames` resolves it. Only the seed argument (`arguments[0]`) is wrapped; a
|
|
18
|
+
`state.linked(seed, ...)`'s trailing args pass through. Exposed as a `ts.TransformerFactory`
|
|
19
|
+
so the nested-script lowering can chain it ahead of the reference rename over ONE parsed tree.
|
|
20
|
+
*/
|
|
21
|
+
export function wrapReactiveSeedsTransformer(): ts.TransformerFactory<ts.SourceFile> {
|
|
22
|
+
return (context) => (root) => {
|
|
23
|
+
const visit = (node: ts.Node): ts.Node => {
|
|
24
|
+
if (ts.isCallExpression(node) && node.arguments.length > 0) {
|
|
25
|
+
const primitive = resolveReactiveExport(node.expression, NESTED_REACTIVE_BINDINGS)
|
|
26
|
+
if (primitive === 'computed' || primitive === 'linked') {
|
|
27
|
+
const seed = node.arguments[0] as ts.Expression
|
|
28
|
+
const wrapped = wrapSeed(ts.visitNode(seed, visit) as ts.Expression)
|
|
29
|
+
const rest = node.arguments
|
|
30
|
+
.slice(1)
|
|
31
|
+
.map((argument) => ts.visitNode(argument, visit) as ts.Expression)
|
|
32
|
+
return ts.factory.updateCallExpression(
|
|
33
|
+
node,
|
|
34
|
+
node.expression,
|
|
35
|
+
node.typeArguments,
|
|
36
|
+
[wrapped, ...rest],
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return ts.visitEachChild(node, visit, context)
|
|
41
|
+
}
|
|
42
|
+
return ts.visitNode(root, visit) as ts.SourceFile
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import ts from 'typescript'
|
|
2
|
+
import { hasTopLevelAwait } from './hasTopLevelAwait.ts'
|
|
3
|
+
|
|
4
|
+
const factory = ts.factory
|
|
5
|
+
|
|
6
|
+
/* Normalises a `computed`/`linked` argument into a seed THUNK: a literal `() => …` /
|
|
7
|
+
`function` argument passes through unchanged (the author already wrote the thunk), any
|
|
8
|
+
other expression is wrapped as `() => arg`. The wrapper arrow is made ASYNC when the
|
|
9
|
+
expression contains a top-level `await` (`computed(await load())` → `async () => await
|
|
10
|
+
load()`), which is exactly the marker the runtime primitive uses to route to an async cell.
|
|
11
|
+
Shared by the top-level desugar (`desugarSignals`) and the nested-`<script>` lowering
|
|
12
|
+
(`wrapReactiveSeeds`) so a bare-value seed is normalised identically on both surfaces. */
|
|
13
|
+
export function wrapSeed(argument: ts.Expression): ts.Expression {
|
|
14
|
+
if (ts.isArrowFunction(argument) || ts.isFunctionExpression(argument)) {
|
|
15
|
+
return argument
|
|
16
|
+
}
|
|
17
|
+
const modifiers = hasTopLevelAwait(argument)
|
|
18
|
+
? [factory.createModifier(ts.SyntaxKind.AsyncKeyword)]
|
|
19
|
+
: undefined
|
|
20
|
+
return factory.createArrowFunction(
|
|
21
|
+
modifiers,
|
|
22
|
+
undefined,
|
|
23
|
+
[],
|
|
24
|
+
undefined,
|
|
25
|
+
factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
|
|
26
|
+
argument,
|
|
27
|
+
)
|
|
28
|
+
}
|
package/src/lib/ui/computed.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { createComputedNode } from './runtime/createComputedNode.ts'
|
|
|
5
5
|
import { isAsyncFunction } from './runtime/isAsyncFunction.ts'
|
|
6
6
|
import { OWNER } from './runtime/OWNER.ts'
|
|
7
7
|
import { readNode } from './runtime/readNode.ts'
|
|
8
|
+
import { routeBareSeed } from './runtime/routeBareSeed.ts'
|
|
8
9
|
import type { Computed } from './runtime/types/Computed.ts'
|
|
9
10
|
import { unlinkDeps } from './runtime/unlinkDeps.ts'
|
|
10
11
|
|
|
@@ -32,6 +33,21 @@ export function computed<T>(compute: () => T): Computed<T>
|
|
|
32
33
|
export function computed<T>(
|
|
33
34
|
compute: () => T | Promise<T> | NamedAsyncIterable<T>,
|
|
34
35
|
): Computed<T> | AsyncComputed<T> {
|
|
36
|
+
/* A BARE seed that reached the runtime literally — a branch-nested `<script>` (which
|
|
37
|
+
keeps its `state.computed(...)` calls literal; see lowerScript) or a direct JS caller.
|
|
38
|
+
The compile-time `wrapSeed` normalization only covers the leading script, so honor the
|
|
39
|
+
same authored contract (ADR-0045: a bare seed behaves like its thunk form) by VALUE via
|
|
40
|
+
the shared `routeBareSeed`; a non-async value becomes a constant computed over it.
|
|
41
|
+
Without this a raw value lands as `node.compute` and the first read calls it
|
|
42
|
+
(`node.compute is 3` — a dead render mid-stream). */
|
|
43
|
+
if (typeof compute !== 'function') {
|
|
44
|
+
const seed: unknown = compute
|
|
45
|
+
const cell = routeBareSeed(seed, false)
|
|
46
|
+
if (cell !== undefined) {
|
|
47
|
+
return cell as AsyncComputed<T>
|
|
48
|
+
}
|
|
49
|
+
compute = () => seed as T
|
|
50
|
+
}
|
|
35
51
|
/* `await` marker: an async-function thunk unwraps its promise into a tracked async cell.
|
|
36
52
|
This is the only classification the read-only primitive can make without running the
|
|
37
53
|
seed — a sync `computed` MUST stay lazy (it does not compute until first read). A stream
|
|
@@ -4,6 +4,7 @@ import { computed } from './computed.ts'
|
|
|
4
4
|
import { effect } from './effect.ts'
|
|
5
5
|
import { linked } from './linked.ts'
|
|
6
6
|
import { CURRENT_PATH } from './runtime/CURRENT_PATH.ts'
|
|
7
|
+
import { consumeSeed } from './runtime/consumeSeed.ts'
|
|
7
8
|
import { createDoc } from './runtime/createDoc.ts'
|
|
8
9
|
import { DOC_SEED } from './runtime/DOC_SEED.ts'
|
|
9
10
|
import type { Cell } from './runtime/types/Cell.ts'
|
|
@@ -50,13 +51,13 @@ export function createScope(
|
|
|
50
51
|
on the client, so a uuid/timestamp/random would diverge from the SSR HTML. The server snapshot
|
|
51
52
|
for this scope's render-path id is decoded here and consumed by `replace` on each slot's FIRST
|
|
52
53
|
write (the eager init), keeping the server value while the throwaway init is discarded. One-shot
|
|
53
|
-
|
|
54
|
+
via `consumeSeed` (two-phase, ADR-0048) — an SPA re-nav to the same path re-inits fresh, and a
|
|
55
|
+
hydration-desync throw hands the seed to the cold rebuild; empty on the server. */
|
|
54
56
|
let pendingSeed: Map<string, unknown> | undefined
|
|
55
57
|
/* `DOC_SEED` is populated only on the client by `startClient` (empty on the server), so the read
|
|
56
58
|
needs no window guard — it mirrors the async-cell warm read. */
|
|
57
|
-
const encodedSeed = DOC_SEED
|
|
59
|
+
const encodedSeed = consumeSeed(DOC_SEED, id)
|
|
58
60
|
if (encodedSeed !== undefined) {
|
|
59
|
-
delete DOC_SEED[id]
|
|
60
61
|
try {
|
|
61
62
|
pendingSeed = new Map(
|
|
62
63
|
Object.entries(decodeRefJson(encodedSeed) as Record<string, unknown>),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parkCursor } from '../runtime/parkCursor.ts'
|
|
1
2
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
2
3
|
|
|
3
4
|
/*
|
|
@@ -18,7 +19,7 @@ export function anchorCursor(anchor: Node): Node | null {
|
|
|
18
19
|
const reference = anchor.nextSibling
|
|
19
20
|
const hydration = RENDER.hydration
|
|
20
21
|
if (hydration !== undefined) {
|
|
21
|
-
hydration
|
|
22
|
+
parkCursor(hydration, anchor.parentNode as Node, reference)
|
|
22
23
|
}
|
|
23
24
|
return reference
|
|
24
25
|
}
|
|
@@ -19,9 +19,10 @@ the accidental copy-paste, notably its two subtle guards:
|
|
|
19
19
|
rethrowing, so the caller's cold rebuild starts clean).
|
|
20
20
|
|
|
21
21
|
`anchor` is a mutable field: each block parks its own anchor text node at create /
|
|
22
|
-
hydrate / rebuild time
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
hydrate / rebuild time via `parkAnchor` (WHERE it parks is bespoke — before the boundary
|
|
23
|
+
close, after a discarded boundary, at the create-mode `before`), and `place` reads it. The
|
|
24
|
+
block owns its `scopeGroup`; each built branch's disposer is tracked in it so it disposes
|
|
25
|
+
on owner teardown, not only on a swap.
|
|
25
26
|
*/
|
|
26
27
|
export function anchoredBranch(
|
|
27
28
|
parent: Node,
|
|
@@ -29,6 +30,7 @@ export function anchoredBranch(
|
|
|
29
30
|
): {
|
|
30
31
|
anchor: Node | undefined
|
|
31
32
|
detach: () => void
|
|
33
|
+
parkAnchor: (before: Node | null) => void
|
|
32
34
|
place: (build: (host: Node) => void, wrapBuild?: (run: () => void) => void) => void
|
|
33
35
|
adoptStrand: (
|
|
34
36
|
build: (host: Node) => void,
|
|
@@ -128,6 +130,14 @@ export function anchoredBranch(
|
|
|
128
130
|
}
|
|
129
131
|
}
|
|
130
132
|
|
|
133
|
+
/* Park a fresh empty text node as the branch's swap anchor at `before` (null appends) —
|
|
134
|
+
the create/hydrate/rebuild triple every block repeated by hand. */
|
|
135
|
+
const parkAnchor = (before: Node | null): void => {
|
|
136
|
+
const node = document.createTextNode('')
|
|
137
|
+
anchor = node
|
|
138
|
+
parent.insertBefore(node, before)
|
|
139
|
+
}
|
|
140
|
+
|
|
131
141
|
return {
|
|
132
142
|
get anchor(): Node | undefined {
|
|
133
143
|
return anchor
|
|
@@ -136,6 +146,7 @@ export function anchoredBranch(
|
|
|
136
146
|
anchor = node
|
|
137
147
|
},
|
|
138
148
|
detach,
|
|
149
|
+
parkAnchor,
|
|
139
150
|
place,
|
|
140
151
|
adoptStrand,
|
|
141
152
|
}
|
|
@@ -1,34 +1,25 @@
|
|
|
1
1
|
import { claimChild } from '../runtime/claimChild.ts'
|
|
2
|
+
import { parkCursor } from '../runtime/parkCursor.ts'
|
|
2
3
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
3
|
-
import {
|
|
4
|
+
import { claimText } from './claimText.ts'
|
|
4
5
|
|
|
5
6
|
/*
|
|
6
7
|
A static (non-reactive) text node under `parent`: created and appended (create
|
|
7
|
-
mode), or claimed from the server-rendered text (hydrate mode)
|
|
8
|
-
|
|
9
|
-
lines up
|
|
8
|
+
mode), or claimed from the server-rendered text (hydrate mode) — the shared
|
|
9
|
+
`claimText` asserts, splits the merged SSR node at this literal's length so the
|
|
10
|
+
next claim lines up, and advances. Nothing is bound since the text never changes.
|
|
11
|
+
A static always renders on the server, so a non-text node at the cursor is a
|
|
12
|
+
tolerated desync: advance past it (assertClaimedText's domain is a text mismatch;
|
|
13
|
+
a structural one is claimExpected's).
|
|
10
14
|
*/
|
|
11
15
|
// @documentation plumbing
|
|
12
16
|
export function appendStatic(parent: Node, value: string, splitAlways = false): void {
|
|
13
17
|
const hydration = RENDER.hydration
|
|
14
18
|
if (hydration !== undefined) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
legibly at the divergence (only when a text node is actually here; a structural
|
|
19
|
-
mismatch is claimExpected's domain). */
|
|
20
|
-
if (isText) {
|
|
21
|
-
assertClaimedText(node, value)
|
|
19
|
+
if (claimText(hydration, parent, value, splitAlways) === undefined) {
|
|
20
|
+
const node = claimChild(hydration, parent)
|
|
21
|
+
parkCursor(hydration, parent, node === null ? null : node.nextSibling)
|
|
22
22
|
}
|
|
23
|
-
/* Split even on an exact-length consume when a sibling text binding follows
|
|
24
|
-
(`splitAlways`), so it gets its own node; see appendText for the rationale. */
|
|
25
|
-
if (
|
|
26
|
-
isText &&
|
|
27
|
-
(splitAlways ? value.length <= node.data.length : value.length < node.data.length)
|
|
28
|
-
) {
|
|
29
|
-
node.splitText(value.length)
|
|
30
|
-
}
|
|
31
|
-
hydration.next.set(parent, node === null ? null : node.nextSibling)
|
|
32
23
|
return
|
|
33
24
|
}
|
|
34
25
|
parent.appendChild(document.createTextNode(value))
|