@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
|
@@ -71,7 +71,7 @@ export type RemoteFunction<
|
|
|
71
71
|
reloads lazily, while `refresh(args?)` REFETCHES now keeping the stale value visible — both
|
|
72
72
|
apply locally on the client and broadcast to every connected client from the server.
|
|
73
73
|
`peek(args?)` reads the retained value synchronously (a streaming rpc peeks its latest
|
|
74
|
-
frame). `
|
|
74
|
+
frame). `amend` is fetch-only, so it is omitted for a streaming rpc (a stream isn't a
|
|
75
75
|
memoized value) via the intersection below. */
|
|
76
76
|
pending(args?: Args): boolean
|
|
77
77
|
refreshing(args?: Args): boolean
|
|
@@ -84,22 +84,33 @@ export type RemoteFunction<
|
|
|
84
84
|
shadow the server `error()` thrower). Reads the rpc error registry captured at the call
|
|
85
85
|
boundary; the cache is untouched. */
|
|
86
86
|
error(args?: Args): RpcError<Errors> | undefined
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
} /* `patch` is fetch-only: a streaming rpc has no single memoized value to mutate, so the
|
|
96
|
-
method is present only when `Return` is not an AsyncIterable. Two signatures: with args
|
|
97
|
-
(`patch(args, updater)`) and without (`patch(updater)` — every args-variant). Tuple-wrapped
|
|
98
|
-
so the conditional doesn't distribute over a `never` Return (an error-only rpc). */ & ([
|
|
99
|
-
Return,
|
|
100
|
-
] extends [AsyncIterable<unknown>]
|
|
101
|
-
? Record<never, never>
|
|
87
|
+
} /* Client-only reaction sugar: `fn.watch(handler)` / `fn.watch(args, handler)` ≡ `watch(fn, …)`
|
|
88
|
+
— runs the smart read reactively and pipes each resolved value to the handler, returning a
|
|
89
|
+
scope-tied disposer. Inert no-op server-side (reaction is a ui concern; the real method is
|
|
90
|
+
attached client-side by remoteProxy). The keyed overload collapses away for a no-input rpc
|
|
91
|
+
(`undefined extends Args`) — there is no key to scope to (ADR-0043 §5, type-only). */ & (undefined extends Args
|
|
92
|
+
? {
|
|
93
|
+
watch(handler: (value: Return) => void): () => void
|
|
94
|
+
}
|
|
102
95
|
: {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
})
|
|
96
|
+
watch(handler: (value: Return) => void): () => void
|
|
97
|
+
watch(args: Args, handler: (value: Return) => void): () => void
|
|
98
|
+
}) /* `amend` is fetch-only: a streaming rpc has no single memoized value to mutate, so the
|
|
99
|
+
method is present only when `Return` is not an AsyncIterable. The last argument is a
|
|
100
|
+
replacement `Return` value (broadcastable — ADR-0043) or an updater `(current) => Return`
|
|
101
|
+
(a local closure). For a keyed rpc the value/updater takes the args key first; the bare
|
|
102
|
+
`amend(updater)` covers every args-variant (local-only). For a no-input rpc the key
|
|
103
|
+
collapses away (`amend(value)` / `amend(updater)`). Tuple-wrapped so the conditional
|
|
104
|
+
doesn't distribute over a `never` Return (an error-only rpc). */ &
|
|
105
|
+
([Return] extends [AsyncIterable<unknown>]
|
|
106
|
+
? Record<never, never>
|
|
107
|
+
: undefined extends Args
|
|
108
|
+
? {
|
|
109
|
+
amend(value: Return): void
|
|
110
|
+
amend(updater: (current: Return) => Return): void
|
|
111
|
+
}
|
|
112
|
+
: {
|
|
113
|
+
amend(args: Args, value: Return): void
|
|
114
|
+
amend(args: Args, updater: (current: Return) => Return): void
|
|
115
|
+
amend(updater: (current: Return) => Return): void
|
|
116
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/*
|
|
2
|
+
The per-request list of retained socket frames read via `peek(socket)` during an SSR render, each
|
|
3
|
+
keyed by the socket's name. `defineSocket`'s server `peek()` pushes an entry when a retained frame is
|
|
4
|
+
read server-side; the page renderer stamps them into `__SSR__.sockets` (ref-json-encoded, last write
|
|
5
|
+
per name winning) so the client seeds the socket's latest frame WARM — a not-yet-connected client's
|
|
6
|
+
`peek(socket)` then returns the same retained value the SSR render committed to instead of `undefined`,
|
|
7
|
+
which would otherwise diverge from the server HTML and trip a hydration desync. Sibling of
|
|
8
|
+
`ResolvedCells` — sockets carry a server value forward like cells, not withhold on the client like
|
|
9
|
+
cache (whose server peek is uniformly undefined).
|
|
10
|
+
*/
|
|
11
|
+
export type SocketTails = {
|
|
12
|
+
entries: { name: string; value: unknown }[]
|
|
13
|
+
}
|
|
@@ -27,4 +27,10 @@ export type SsrPayload = SsrBootState & {
|
|
|
27
27
|
write to each slot adopts the server value, so a plain `state(uuid())`/`state(Date.now())`
|
|
28
28
|
keeps the SSR value instead of recomputing a divergent one. */
|
|
29
29
|
docs?: Record<string, string>
|
|
30
|
+
/* Retained socket frames read via `peek(socket)` during SSR, keyed by socket name → ref-json
|
|
31
|
+
string. `startClient` seeds these into `SOCKET_SEED`; a hydrating `socketProxy` adopts the frame
|
|
32
|
+
as its `lastFrame` so `peek(socket)` returns the SAME retained value the server rendered instead
|
|
33
|
+
of `undefined` on the not-yet-connected client — otherwise the two disagree and hydration
|
|
34
|
+
discards the server markup. */
|
|
35
|
+
sockets?: Record<string, string>
|
|
30
36
|
}
|
|
@@ -47,6 +47,7 @@ import { pendingAsyncCellsSlot } from '../shared/pendingAsyncCellsSlot.ts'
|
|
|
47
47
|
import { resolvedCellsSlot } from '../shared/resolvedCellsSlot.ts'
|
|
48
48
|
import { SOCKETS_PATH } from '../shared/SOCKETS_PATH.ts'
|
|
49
49
|
import { sharedCacheStoreSlot } from '../shared/sharedCacheStoreSlot.ts'
|
|
50
|
+
import { socketTailsSlot } from '../shared/socketTailsSlot.ts'
|
|
50
51
|
import { streamedCellsSlot } from '../shared/streamedCellsSlot.ts'
|
|
51
52
|
import type { RemoteFunction } from '../shared/types/RemoteFunction.ts'
|
|
52
53
|
import type { Socket } from '../shared/types/Socket.ts'
|
|
@@ -111,6 +112,7 @@ export async function createTestApp(): Promise<TestApp> {
|
|
|
111
112
|
pendingAsyncCellsResolver: pendingAsyncCellsSlot.resolver,
|
|
112
113
|
resolvedCellsResolver: resolvedCellsSlot.resolver,
|
|
113
114
|
streamedCellsResolver: streamedCellsSlot.resolver,
|
|
115
|
+
socketTailsResolver: socketTailsSlot.resolver,
|
|
114
116
|
docSnapshotsResolver: docSnapshotsSlot.resolver,
|
|
115
117
|
activeServer: serverSlot.active,
|
|
116
118
|
pageRender: pageRenderSlot.render,
|
|
@@ -129,6 +131,9 @@ export async function createTestApp(): Promise<TestApp> {
|
|
|
129
131
|
const sharedStreamedCells = { entries: [] }
|
|
130
132
|
streamedCellsSlot.resolver = () =>
|
|
131
133
|
requestContext.getStore()?.streamedCells ?? sharedStreamedCells
|
|
134
|
+
/* No shared fallback — mirrors serverEntry: `defineSocket.peek` records off-request too, so a
|
|
135
|
+
fallback would leak. Off-request this resolves undefined and the record is skipped. */
|
|
136
|
+
socketTailsSlot.resolver = () => requestContext.getStore()?.socketTails
|
|
132
137
|
const sharedDocSnapshots = { entries: [] }
|
|
133
138
|
docSnapshotsSlot.resolver = () => requestContext.getStore()?.docSnapshots ?? sharedDocSnapshots
|
|
134
139
|
pageSlot.resolver = resolvePageSnapshot
|
|
@@ -218,6 +223,7 @@ export async function createTestApp(): Promise<TestApp> {
|
|
|
218
223
|
pendingAsyncCellsSlot.resolver = previous.pendingAsyncCellsResolver
|
|
219
224
|
resolvedCellsSlot.resolver = previous.resolvedCellsResolver
|
|
220
225
|
streamedCellsSlot.resolver = previous.streamedCellsResolver
|
|
226
|
+
socketTailsSlot.resolver = previous.socketTailsResolver
|
|
221
227
|
docSnapshotsSlot.resolver = previous.docSnapshotsResolver
|
|
222
228
|
serverSlot.active = previous.activeServer
|
|
223
229
|
pageRenderSlot.render = previous.pageRender
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { AMEND_TOPIC_PREFIX } from '../shared/AMEND_TOPIC_PREFIX.ts'
|
|
2
|
+
import { abideLog } from '../shared/abideLog.ts'
|
|
3
|
+
import { cache } from '../shared/cache.ts'
|
|
4
|
+
import { SocketDisconnectedError } from '../shared/SocketDisconnectedError.ts'
|
|
5
|
+
import type { CacheReaderHook } from '../shared/types/CacheReaderHook.ts'
|
|
6
|
+
import type { Socket } from '../shared/types/Socket.ts'
|
|
7
|
+
import { socketProxy } from './socketProxy.ts'
|
|
8
|
+
|
|
9
|
+
/* A cache key is a broadcastable amend target only when it carries cross-client wire
|
|
10
|
+
identity — a remote call key (`METHOD /url…`, per keyForRemoteCall). Producer keys
|
|
11
|
+
(per-process reference ids) and anything else can't be pushed across clients, so we
|
|
12
|
+
never open a subscription for them. */
|
|
13
|
+
const REMOTE_KEY = /^(?:GET|POST|PUT|PATCH|DELETE|HEAD) \//
|
|
14
|
+
|
|
15
|
+
/*
|
|
16
|
+
The client-only reactive-reader lifecycle hook (ADR-0043) installed into
|
|
17
|
+
cacheReaderSocketSlot by startClient. When a remote key gains its first live reader it
|
|
18
|
+
opens a subscription to that call's reserved amend topic and folds each pushed value
|
|
19
|
+
into the cache; when the key loses its last reader it closes the subscription — so the
|
|
20
|
+
set of subscribed topics stays congruent with the set of keys this tab is reading (and
|
|
21
|
+
therefore already authorized for). Refcounted per key so co-readers (or the active/shared
|
|
22
|
+
store both reporting a reader) share one subscription.
|
|
23
|
+
|
|
24
|
+
The channel is live-only (no tail): a value pushed while this tab was disconnected is
|
|
25
|
+
missed, so on a transport loss the hook re-opens a fresh live iterator AND refreshes the
|
|
26
|
+
key — the ADR-0043 convergence rule, reconciling against authoritative server state
|
|
27
|
+
rather than leaving a silently-stale value. `openSocket` is injectable so a test can drive
|
|
28
|
+
frames over its own channel without a live ws.
|
|
29
|
+
*/
|
|
30
|
+
export function createAmendReaderHook(
|
|
31
|
+
openSocket: (name: string) => Socket<unknown> = socketProxy,
|
|
32
|
+
): { hook: CacheReaderHook; dispose: () => void } {
|
|
33
|
+
const open = new Map<string, { readers: number; dispose: () => void }>()
|
|
34
|
+
|
|
35
|
+
function subscribe(key: string): () => void {
|
|
36
|
+
const socket = openSocket(AMEND_TOPIC_PREFIX + key)
|
|
37
|
+
const controller = new AbortController()
|
|
38
|
+
/* Opening the shared ws channel must never propagate into the cache read that engaged
|
|
39
|
+
this key — a read can't fail because an optional real-time subscription couldn't open
|
|
40
|
+
(e.g. no ws transport). Log and give up on this key's push channel; SWR still holds. */
|
|
41
|
+
let iterator: AsyncIterator<unknown>
|
|
42
|
+
try {
|
|
43
|
+
iterator = socket[Symbol.asyncIterator]()
|
|
44
|
+
} catch (error) {
|
|
45
|
+
abideLog.error(error)
|
|
46
|
+
return () => undefined
|
|
47
|
+
}
|
|
48
|
+
;(async () => {
|
|
49
|
+
while (!controller.signal.aborted) {
|
|
50
|
+
let next: IteratorResult<unknown>
|
|
51
|
+
try {
|
|
52
|
+
next = await iterator.next()
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (controller.signal.aborted) {
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
/* Transport loss: reconcile the possibly-missed value against server truth,
|
|
58
|
+
then re-open a fresh LIVE subscription (no replay) and keep going. */
|
|
59
|
+
if (error instanceof SocketDisconnectedError) {
|
|
60
|
+
cache.refreshMatching((entry) => entry.key === key, key, undefined, key)
|
|
61
|
+
try {
|
|
62
|
+
iterator = socket[Symbol.asyncIterator]()
|
|
63
|
+
} catch (reopenError) {
|
|
64
|
+
abideLog.error(reopenError)
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
69
|
+
abideLog.error(error)
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
if (controller.signal.aborted || next.done === true) {
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
/* One malformed frame must not tear down the live pipe. Frames come only from the
|
|
76
|
+
trusted server (clientPublish is false), so this is defense in depth. */
|
|
77
|
+
try {
|
|
78
|
+
cache.amendByKey(key, next.value)
|
|
79
|
+
} catch (error) {
|
|
80
|
+
abideLog.error(error)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
})()
|
|
84
|
+
return () => {
|
|
85
|
+
controller.abort()
|
|
86
|
+
iterator.return?.(undefined)?.catch(() => undefined)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const hook: CacheReaderHook = {
|
|
91
|
+
engage(key) {
|
|
92
|
+
if (!REMOTE_KEY.test(key)) {
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
const existing = open.get(key)
|
|
96
|
+
if (existing) {
|
|
97
|
+
existing.readers += 1
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
open.set(key, { readers: 1, dispose: subscribe(key) })
|
|
101
|
+
},
|
|
102
|
+
disengage(key) {
|
|
103
|
+
const existing = open.get(key)
|
|
104
|
+
if (existing === undefined) {
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
existing.readers -= 1
|
|
108
|
+
if (existing.readers <= 0) {
|
|
109
|
+
existing.dispose()
|
|
110
|
+
open.delete(key)
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* Tear down every open subscription — for the startClient disposer, in case reads are
|
|
116
|
+
still mounted when the whole client is torn down (e.g. a test harness). */
|
|
117
|
+
function dispose(): void {
|
|
118
|
+
for (const entry of open.values()) {
|
|
119
|
+
entry.dispose()
|
|
120
|
+
}
|
|
121
|
+
open.clear()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { hook, dispose }
|
|
125
|
+
}
|
|
@@ -38,16 +38,12 @@ export const UI_RUNTIME_IMPORTS: { name: string; specifier: string; alias?: stri
|
|
|
38
38
|
{ name: 'eachAsync', specifier: 'ui/dom/eachAsync', alias: '$$eachAsync' },
|
|
39
39
|
{ name: 'when', specifier: 'ui/dom/when', alias: '$$when' },
|
|
40
40
|
{ name: 'awaitBlock', specifier: 'ui/dom/awaitBlock', alias: '$$awaitBlock' },
|
|
41
|
+
{ name: 'awaitSubject', specifier: 'ui/dom/awaitSubject', alias: '$$awaitSubject' },
|
|
41
42
|
{ name: 'tryBlock', specifier: 'ui/dom/tryBlock', alias: '$$tryBlock' },
|
|
42
43
|
{ name: 'switchBlock', specifier: 'ui/dom/switchBlock', alias: '$$switchBlock' },
|
|
43
44
|
{ name: 'mountSlot', specifier: 'ui/dom/mountSlot', alias: '$$mountSlot' },
|
|
44
45
|
{ name: 'outlet', specifier: 'ui/dom/outlet', alias: '$$outlet' },
|
|
45
46
|
{ name: 'mountChild', specifier: 'ui/dom/mountChild', alias: '$$mountChild' },
|
|
46
|
-
{
|
|
47
|
-
name: 'mountStreamedChild',
|
|
48
|
-
specifier: 'ui/dom/mountStreamedChild',
|
|
49
|
-
alias: '$$mountStreamedChild',
|
|
50
|
-
},
|
|
51
47
|
{ name: 'mergeProps', specifier: 'ui/dom/mergeProps', alias: '$$mergeProps' },
|
|
52
48
|
{ name: 'spreadProps', specifier: 'ui/dom/spreadProps', alias: '$$spreadProps' },
|
|
53
49
|
{ name: 'restProps', specifier: 'ui/dom/restProps', alias: '$$restProps' },
|
|
@@ -56,7 +52,6 @@ export const UI_RUNTIME_IMPORTS: { name: string; specifier: string; alias?: stri
|
|
|
56
52
|
{ name: 'spreadAttrs', specifier: 'ui/dom/spreadAttrs', alias: '$$spreadAttrs' },
|
|
57
53
|
{ name: 'readCall', specifier: 'ui/dom/readCall', alias: '$$readCall' },
|
|
58
54
|
{ name: 'readCell', specifier: 'ui/dom/readCell', alias: '$$readCell' },
|
|
59
|
-
{ name: 'readCellBlocking', specifier: 'ui/dom/readCellBlocking', alias: '$$readCellBlocking' },
|
|
60
55
|
{ name: 'writeCell', specifier: 'ui/dom/writeCell', alias: '$$writeCell' },
|
|
61
56
|
{ name: 'cellPending', specifier: 'ui/dom/cellPending', alias: '$$cellPending' },
|
|
62
57
|
{ name: 'withPath', specifier: 'ui/runtime/withPath', alias: '$$withPath' },
|
|
@@ -120,7 +120,6 @@ export function analyzeComponent(
|
|
|
120
120
|
derivedNames,
|
|
121
121
|
computedNames,
|
|
122
122
|
cellReadNames,
|
|
123
|
-
blockingCellNames: allBlockingCellNames,
|
|
124
123
|
droppedReactiveImports,
|
|
125
124
|
} = lowerScript(
|
|
126
125
|
fullScriptBody,
|
|
@@ -143,7 +142,6 @@ export function analyzeComponent(
|
|
|
143
142
|
derivedNames,
|
|
144
143
|
computedNames,
|
|
145
144
|
cellReadNames,
|
|
146
|
-
blockingCellNames: allBlockingCellNames,
|
|
147
145
|
nodes,
|
|
148
146
|
styles,
|
|
149
147
|
/* Hydration adopts every block in place — including `await`, which resumes
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/*
|
|
2
|
+
The lowered `{#await X}` subject, normalised so a cell subject AWAITS its resolution
|
|
3
|
+
(ADR-0047): a BARE async-cell reference is passed RAW (not peeked to `$$readCell(cell)`)
|
|
4
|
+
and wrapped in `$$awaitSubject(...)`, which resolves a cell to a promise-of-its-value —
|
|
5
|
+
so `{#await rates}` (a cell) shows pending then the value; any other subject lowers
|
|
6
|
+
unchanged, so `{#await getFoo()}` (a plain promise) is byte-identical. A name currently
|
|
7
|
+
SHADOWED by a block binding (`{#for rates of …}` / `{:then rates}` over a component cell
|
|
8
|
+
`rates`) is the binder's local, not the cell — it lowers through `lowerExpression` like any
|
|
9
|
+
other subject, matching how every other reference site honours the shadow stack. ONE
|
|
10
|
+
decision site shared by both back-ends (the client `awaitBlock` emit and SSR's blocking /
|
|
11
|
+
streaming / flight-hoist emits), so the bare-cell classification can't drift between them.
|
|
12
|
+
*/
|
|
13
|
+
export function awaitSubjectExpr(
|
|
14
|
+
promiseCode: string,
|
|
15
|
+
cellReadNames: ReadonlySet<string>,
|
|
16
|
+
isShadowed: (name: string) => boolean,
|
|
17
|
+
lowerExpression: (code: string) => string,
|
|
18
|
+
): string {
|
|
19
|
+
const bare = promiseCode.trim()
|
|
20
|
+
return cellReadNames.has(bare) && !isShadowed(bare)
|
|
21
|
+
? `$$awaitSubject(${bare})`
|
|
22
|
+
: lowerExpression(promiseCode)
|
|
23
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { messageFromError } from '../../shared/messageFromError.ts'
|
|
2
|
+
import { createShadowProgram, type ShadowProgram } from './createShadowProgram.ts'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
Get-or-build the warm shadow program for a project root, cached once per root and shared by
|
|
6
|
+
the interpolation and seed classifiers (whichever is requested first for a root warms it; the
|
|
7
|
+
other reuses it). The shadow program is what makes type-directed async detection possible —
|
|
8
|
+
resolving whether `{getFoo()}` is a promise/stream and lifting it to a streaming/blocking cell.
|
|
9
|
+
|
|
10
|
+
On a build FAILURE the program is cached as `undefined` and the classifiers degrade to today's
|
|
11
|
+
plain-value / syntactic behavior — the build never breaks. But the failure is WARNED once per
|
|
12
|
+
root: a silent `undefined` disables type-directed async detection for EVERY component in the
|
|
13
|
+
root, so a broken tsconfig would otherwise ship a bare `{getFoo()}` as the literal
|
|
14
|
+
`[object Promise]` text with no signal at all. The warn fires once (guarded by `cache.has`), so
|
|
15
|
+
the two classifiers sharing a cache never double-report.
|
|
16
|
+
|
|
17
|
+
`build` is injectable so the warn-once path is testable without a broken project on disk;
|
|
18
|
+
callers pass nothing and get `createShadowProgram`.
|
|
19
|
+
*/
|
|
20
|
+
export function cachedShadowProgram(
|
|
21
|
+
cache: Map<string, ShadowProgram | undefined>,
|
|
22
|
+
root: string,
|
|
23
|
+
build: (root: string) => ShadowProgram = createShadowProgram,
|
|
24
|
+
): ShadowProgram | undefined {
|
|
25
|
+
if (!cache.has(root)) {
|
|
26
|
+
try {
|
|
27
|
+
cache.set(root, build(root))
|
|
28
|
+
} catch (error) {
|
|
29
|
+
cache.set(root, undefined)
|
|
30
|
+
const detail = messageFromError(error)
|
|
31
|
+
console.warn(
|
|
32
|
+
`[abide] couldn't build the type program for ${root}: ${detail}\n` +
|
|
33
|
+
' type-directed async detection is disabled for this project — a bare promise/stream\n' +
|
|
34
|
+
' read like {getFoo()} renders as text instead of streaming. Check the tsconfig.',
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return cache.get(root)
|
|
39
|
+
}
|
|
@@ -29,8 +29,7 @@ export function compileComponent(
|
|
|
29
29
|
a direct caller (tests) omits it and the front-end runs here — threading `classify` and
|
|
30
30
|
`seedClassify` so type-directed interpolation + cell lowering happen on this path too. */
|
|
31
31
|
const resolved = analyzed ?? analyzeComponent(source, scopeSeed, classify, seedClassify)
|
|
32
|
-
const { script, stateNames, derivedNames, computedNames, cellReadNames,
|
|
33
|
-
resolved
|
|
32
|
+
const { script, stateNames, derivedNames, computedNames, cellReadNames, nodes } = resolved
|
|
34
33
|
const build = generateBuild(
|
|
35
34
|
nodes,
|
|
36
35
|
'host',
|
|
@@ -39,7 +38,6 @@ export function compileComponent(
|
|
|
39
38
|
computedNames,
|
|
40
39
|
isLayout,
|
|
41
40
|
cellReadNames,
|
|
42
|
-
blockingCellNames,
|
|
43
41
|
)
|
|
44
42
|
/* The scoped CSS is bundled into the entry stylesheet (see `abideUiPlugin`), not
|
|
45
43
|
injected at runtime; the build only needs the `data-a-…` scope attributes on
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { TemplateNode } from './types/TemplateNode.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Document-order (pre-order) numbering of every `<Child/>` mount site in a template — the
|
|
5
|
+
ordinal a child's render-path roots under (`mountChild` on the client, `$$withPath(ordinal)`
|
|
6
|
+
/ `$$renderPath(ordinal)` on the server). ONE walk both back-ends read (the `skeletonContext`
|
|
7
|
+
pattern), replacing the per-back-end `childOrdinal++` counters that numbered components in
|
|
8
|
+
each generator's own EMISSION order — an order that could silently diverge (build emits a
|
|
9
|
+
component's slot content before taking its own ordinal; SSR emits a switch's default case
|
|
10
|
+
after later cases). Numbering from the tree, not the emission, makes SSR↔client ordinal
|
|
11
|
+
congruence hold by construction.
|
|
12
|
+
|
|
13
|
+
Returns the lookup, not the map, so the missing-entry policy lives here too: a miss means a
|
|
14
|
+
back-end reached a mount site the shared walk didn't number — a structural divergence,
|
|
15
|
+
surfaced loudly at compile time rather than as a runtime warm-seed/boundary desync.
|
|
16
|
+
*/
|
|
17
|
+
export function componentOrdinals(nodes: TemplateNode[]): (node: TemplateNode) => number {
|
|
18
|
+
const ordinals = new WeakMap<TemplateNode, number>()
|
|
19
|
+
let next = 0
|
|
20
|
+
const walk = (children: TemplateNode[]): void => {
|
|
21
|
+
for (const child of children) {
|
|
22
|
+
if (child.kind === 'component') {
|
|
23
|
+
ordinals.set(child, next)
|
|
24
|
+
next += 1
|
|
25
|
+
}
|
|
26
|
+
if ('children' in child) {
|
|
27
|
+
walk(child.children)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
walk(nodes)
|
|
32
|
+
return (node: TemplateNode): number => {
|
|
33
|
+
const ordinal = ordinals.get(node)
|
|
34
|
+
if (ordinal === undefined) {
|
|
35
|
+
throw new Error('[abide] component mount site not numbered by the shared ordinal walk')
|
|
36
|
+
}
|
|
37
|
+
return ordinal
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -5,6 +5,7 @@ import { type ReactiveImportBindings, reactiveImportBindings } from './resolveRe
|
|
|
5
5
|
import { signalCallee } from './signalCallee.ts'
|
|
6
6
|
import type { InterpolationKind } from './types/InterpolationKind.ts'
|
|
7
7
|
import type { SeedTypeClassifier } from './types/SeedTypeClassifier.ts'
|
|
8
|
+
import { wrapSeed } from './wrapSeed.ts'
|
|
8
9
|
|
|
9
10
|
/* The routing decision for a no-marker `computed(seed)`: `true` routes to the eager
|
|
10
11
|
`trackedComputed` stream cell (read via `$$readCell`, the `cellReadNames` bucket), `false`
|
|
@@ -14,25 +15,21 @@ type EagerStreamPredicate = (declaration: ts.VariableDeclaration) => boolean
|
|
|
14
15
|
|
|
15
16
|
const factory = ts.factory
|
|
16
17
|
|
|
17
|
-
/*
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
const modifiers = hasTopLevelAwait(argument)
|
|
27
|
-
? [factory.createModifier(ts.SyntaxKind.AsyncKeyword)]
|
|
28
|
-
: undefined
|
|
18
|
+
/* Wraps a seed expression as an async unwrapping thunk `async () => await (arg)` — for a
|
|
19
|
+
type-directed PROMISE seed (ADR-0023/0043) the author wrote WITHOUT `await`
|
|
20
|
+
(`state.computed(getFoo())`). The `async`+`await` makes createAsyncCell unwrap the resolved
|
|
21
|
+
value; a plain `() => getFoo()` thunk would fall to trackedComputed's lazy opaque path (its
|
|
22
|
+
probe self-identifies only a stream, never a promise). The parens keep a comma/ternary seed
|
|
23
|
+
a single await operand. Distinct from `wrapSeed`, which only makes the arrow async when the
|
|
24
|
+
arg already carries a top-level `await`. */
|
|
25
|
+
function wrapAwaitSeed(argument: ts.Expression): ts.Expression {
|
|
29
26
|
return factory.createArrowFunction(
|
|
30
|
-
|
|
27
|
+
[factory.createModifier(ts.SyntaxKind.AsyncKeyword)],
|
|
31
28
|
undefined,
|
|
32
29
|
[],
|
|
33
30
|
undefined,
|
|
34
31
|
factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
|
|
35
|
-
argument,
|
|
32
|
+
factory.createAwaitExpression(factory.createParenthesizedExpression(argument)),
|
|
36
33
|
)
|
|
37
34
|
}
|
|
38
35
|
|
|
@@ -213,10 +210,6 @@ export function desugarSignals(
|
|
|
213
210
|
derivedNames: Set<string>
|
|
214
211
|
computedNames: Set<string>
|
|
215
212
|
cellReadNames: Set<string>
|
|
216
|
-
/* The full BLOCKING cell set (ADR-0042): the template-injected `await` cells passed in, unioned
|
|
217
|
-
with the script-level `await` computeds/linked collected here. The client template lowering
|
|
218
|
-
reads these via `$$readCellBlocking` (suspend-on-pending). */
|
|
219
|
-
blockingCellNames: Set<string>
|
|
220
213
|
} {
|
|
221
214
|
assertNoRemovedReaders(source)
|
|
222
215
|
/* The full set of names written anywhere (this script + the template). A prop in this set
|
|
@@ -235,10 +228,6 @@ export function desugarSignals(
|
|
|
235
228
|
One read shape covers both — `$$readCell` peeks an async cell and reads `.value` off a
|
|
236
229
|
sync one — so `linked(getStream())` auto-tracks with no read-site branching. */
|
|
237
230
|
const cellReadNames = new Set<string>()
|
|
238
|
-
/* Script-level BLOCKING cell names (ADR-0042 D6): a `computed`/`linked` whose seed carries a
|
|
239
|
-
top-level `await`. Unioned with the template-injected `blockingCellNames` and returned so the
|
|
240
|
-
CLIENT template lowering reads them via `$$readCellBlocking` (suspend-on-pending). */
|
|
241
|
-
const scriptBlockingNames = new Set<string>()
|
|
242
231
|
/* A `props()` destructure must be lowered even when it declares no reactive binding
|
|
243
232
|
(a rest-only `const { ...rest } = props()`), so track its presence on its own. */
|
|
244
233
|
let hasPropsDestructure = false
|
|
@@ -275,6 +264,21 @@ export function desugarSignals(
|
|
|
275
264
|
}
|
|
276
265
|
return isBareCallComputed(declaration)
|
|
277
266
|
}
|
|
267
|
+
/* A type-directed PROMISE seed (ADR-0023/0043): a no-`await` `state.computed(getFoo())` whose
|
|
268
|
+
seed's checker type resolves to a promise. Routed to an eager STREAMING async cell that
|
|
269
|
+
unwraps the resolved value and reactively re-resolves when its tracked deps change — the
|
|
270
|
+
script-level twin of a bare async interpolation (ADR-0032). Only fires with a warm classifier
|
|
271
|
+
('promise'); fail-open (no program) leaves it to `isEagerStreamComputed`'s bare-call probe,
|
|
272
|
+
and the `await`-marker seed is excluded (its wrapped thunk is already async). Kept separate
|
|
273
|
+
from `isEagerStreamComputed` so `computedStatements` can wrap it as `async () => await (seed)`
|
|
274
|
+
(a stream seed stays a bare probe thunk). */
|
|
275
|
+
const isPromiseComputed: EagerStreamPredicate = (declaration) => {
|
|
276
|
+
const argument = seedArgument(declaration)
|
|
277
|
+
if (argument === undefined || isAsyncSeed(wrapSeed(argument))) {
|
|
278
|
+
return false
|
|
279
|
+
}
|
|
280
|
+
return seedKind(argument) === 'promise'
|
|
281
|
+
}
|
|
278
282
|
for (const statement of source.statements) {
|
|
279
283
|
if (!ts.isVariableStatement(statement)) {
|
|
280
284
|
continue
|
|
@@ -321,26 +325,26 @@ export function desugarSignals(
|
|
|
321
325
|
`name()` (a sync/promise seed), or an eager cell read via `$$readCell(name)`
|
|
322
326
|
when the wrapped seed is async (an `await`-lowered / passthrough-`async` thunk)
|
|
323
327
|
or its seed type resolves to a stream (`isEagerStreamComputed`, ADR-0023). */
|
|
324
|
-
if (
|
|
328
|
+
if (
|
|
329
|
+
isAsyncComputed(declaration) ||
|
|
330
|
+
isEagerStreamComputed(declaration) ||
|
|
331
|
+
isPromiseComputed(declaration)
|
|
332
|
+
) {
|
|
333
|
+
/* Async `computed` (author `await`), an eager stream, or a promise seed — all
|
|
334
|
+
read via `$$readCell(name)`. Whether a pending read PAUSES (author `await`,
|
|
335
|
+
a blocking cell) or peeks `undefined` (streaming) is carried by the cell
|
|
336
|
+
itself at construction (the `trackedComputed(thunk, streaming)` flag), so the
|
|
337
|
+
read form is uniform and nothing blocking-specific is collected here. */
|
|
325
338
|
cellReadNames.add(declaration.name.text)
|
|
326
|
-
/* A blocking async computed (author `await`) reads suspend-on-pending. */
|
|
327
|
-
const argument = seedArgument(declaration)
|
|
328
|
-
if (argument !== undefined && isBlockingSeed(argument)) {
|
|
329
|
-
scriptBlockingNames.add(declaration.name.text)
|
|
330
|
-
}
|
|
331
339
|
} else {
|
|
332
340
|
computedNames.add(declaration.name.text)
|
|
333
341
|
}
|
|
334
342
|
} else if (callee === 'linked') {
|
|
335
343
|
/* `linked` → a cell read via `$$readCell(name)`: a plain `State` when the seed
|
|
336
344
|
is synchronous, an `AsyncState` when it tracks a promise/stream — one read
|
|
337
|
-
shape auto-tracks whichever source the runtime primitive resolved to.
|
|
345
|
+
shape auto-tracks whichever source the runtime primitive resolved to. Its
|
|
346
|
+
blocking-ness (author `await`) likewise rides on the constructed cell. */
|
|
338
347
|
cellReadNames.add(declaration.name.text)
|
|
339
|
-
/* A blocking async linked (author `await`) reads suspend-on-pending, like computed. */
|
|
340
|
-
const argument = seedArgument(declaration)
|
|
341
|
-
if (argument !== undefined && isBlockingSeed(argument)) {
|
|
342
|
-
scriptBlockingNames.add(declaration.name.text)
|
|
343
|
-
}
|
|
344
348
|
} else if (callee === 'state') {
|
|
345
349
|
/* `state(initial, transform)` → a `.value` cell (its write-coercion transform
|
|
346
350
|
forces a local store); referenced as `name.value`, unchanged. */
|
|
@@ -389,6 +393,7 @@ export function desugarSignals(
|
|
|
389
393
|
blockingCellNames,
|
|
390
394
|
writtenNames,
|
|
391
395
|
isEagerStreamComputed,
|
|
396
|
+
isPromiseComputed,
|
|
392
397
|
),
|
|
393
398
|
)
|
|
394
399
|
}
|
|
@@ -401,7 +406,6 @@ export function desugarSignals(
|
|
|
401
406
|
derivedNames,
|
|
402
407
|
computedNames,
|
|
403
408
|
cellReadNames,
|
|
404
|
-
blockingCellNames: new Set([...blockingCellNames, ...scriptBlockingNames]),
|
|
405
409
|
}
|
|
406
410
|
}
|
|
407
411
|
|
|
@@ -417,12 +421,19 @@ function loweredStatement(
|
|
|
417
421
|
blockingCellNames: ReadonlySet<string>,
|
|
418
422
|
writtenNames: ReadonlySet<string>,
|
|
419
423
|
isEagerStreamComputed: EagerStreamPredicate,
|
|
424
|
+
isPromiseComputed: EagerStreamPredicate,
|
|
420
425
|
): ts.Statement[] {
|
|
421
426
|
rejectMixedDeclaration(statement, bindings)
|
|
422
427
|
return (
|
|
423
428
|
injectedComputedStatements(statement, injectedCellNames, blockingCellNames) ??
|
|
424
429
|
stateAssignmentStatements(statement, bindings) ??
|
|
425
|
-
computedStatements(
|
|
430
|
+
computedStatements(
|
|
431
|
+
statement,
|
|
432
|
+
bindings,
|
|
433
|
+
signalNames,
|
|
434
|
+
isEagerStreamComputed,
|
|
435
|
+
isPromiseComputed,
|
|
436
|
+
) ??
|
|
426
437
|
propsStatements(statement, bindings, writtenNames) ??
|
|
427
438
|
cellStatements(statement, bindings, signalNames) ?? [statement]
|
|
428
439
|
)
|
|
@@ -613,6 +624,7 @@ function computedStatements(
|
|
|
613
624
|
bindings: ReactiveImportBindings,
|
|
614
625
|
signalNames: ReadonlySet<string>,
|
|
615
626
|
isEagerStreamComputed: EagerStreamPredicate,
|
|
627
|
+
isPromiseComputed: EagerStreamPredicate,
|
|
616
628
|
): ts.Statement[] | undefined {
|
|
617
629
|
if (!ts.isVariableStatement(statement)) {
|
|
618
630
|
return undefined
|
|
@@ -659,6 +671,23 @@ function computedStatements(
|
|
|
659
671
|
]),
|
|
660
672
|
),
|
|
661
673
|
)
|
|
674
|
+
} else if (argument !== undefined && isPromiseComputed(declaration)) {
|
|
675
|
+
/* Type-directed PROMISE seed (ADR-0023/0043): a bare `state.computed(getFoo())` whose
|
|
676
|
+
seed resolves to a promise. Wrap it as `async () => await (seed)` so createAsyncCell
|
|
677
|
+
unwraps the resolved value — a plain `() => getFoo()` thunk would fall to
|
|
678
|
+
trackedComputed's lazy opaque path (its probe self-identifies only a stream). Passed
|
|
679
|
+
`streaming: true` (the `true` arg): no author `await` → it does NOT join the SSR
|
|
680
|
+
blocking barrier — the shell ships pending and the client resolves + reactively
|
|
681
|
+
re-resolves, the ADR-0032 no-await tier. `await getFoo()` stays the BLOCKING form. */
|
|
682
|
+
statements.push(
|
|
683
|
+
constDeclaration(
|
|
684
|
+
name,
|
|
685
|
+
scopeMethodCall('trackedComputed', [
|
|
686
|
+
wrapAwaitSeed(argument),
|
|
687
|
+
factory.createTrue(),
|
|
688
|
+
]),
|
|
689
|
+
),
|
|
690
|
+
)
|
|
662
691
|
} else if (isEagerStreamComputed(declaration)) {
|
|
663
692
|
/* Stream seed → the eager `trackedComputed`, which probes the seed and auto-tracks a
|
|
664
693
|
stream (`AsyncComputed`) or falls back to a lazy computed; read via `$$readCell`.
|