@abide/abide 0.45.0 → 0.47.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 +365 -320
- package/CHANGELOG.md +96 -0
- package/README.md +203 -168
- package/package.json +12 -8
- package/src/abideLsp.ts +11 -12
- package/src/abideResolverPlugin.ts +9 -4
- package/src/lib/bundle/disconnected.abide +3 -0
- package/src/lib/cli/printCommandHelp.ts +43 -0
- package/src/lib/cli/printSessionHelp.ts +2 -1
- package/src/lib/cli/{printHelp.ts → printTopLevelHelp.ts} +3 -40
- package/src/lib/cli/runCli.ts +2 -1
- package/src/lib/mcp/buildPrompts.ts +25 -0
- package/src/lib/mcp/dispatchMcpRequest.ts +8 -6
- package/src/lib/mcp/mcpResourceServerSlot.ts +5 -10
- package/src/lib/mcp/mcpSurface.ts +13 -252
- package/src/lib/mcp/mcpTools.ts +187 -0
- package/src/lib/mcp/renderPrompt.ts +25 -0
- package/src/lib/mcp/types/McpSurface.ts +15 -0
- package/src/lib/mcp/types/PromptDescriptor.ts +5 -0
- package/src/lib/mcp/types/PromptMessage.ts +2 -0
- package/src/lib/mcp/types/ToolDescriptor.ts +7 -0
- package/src/lib/mcp/types/ToolResult.ts +2 -0
- package/src/lib/server/agent.ts +1 -1
- package/src/lib/server/jsonl.ts +2 -2
- package/src/lib/server/rpc/defineRpc.ts +7 -1
- package/src/lib/server/runtime/containsTraversal.ts +21 -17
- package/src/lib/server/runtime/createAppAssetServer.ts +11 -16
- package/src/lib/server/runtime/createServer.ts +101 -71
- package/src/lib/server/runtime/createUiPageRenderer.ts +23 -4
- package/src/lib/server/runtime/devClientFingerprint.ts +3 -3
- package/src/lib/server/runtime/devHotModuleResponse.ts +2 -1
- package/src/lib/server/runtime/finalizeResponse.ts +32 -0
- package/src/lib/server/runtime/snapshotEntryFromCache.ts +8 -17
- package/src/lib/server/socket.ts +1 -1
- package/src/lib/server/sockets/createSocketDispatcher.ts +28 -5
- package/src/lib/server/sockets/defineSocket.ts +26 -7
- package/src/lib/server/sockets/types/SocketRegistryEntry.ts +1 -1
- package/src/lib/server/sockets/types/SocketRoutes.ts +1 -1
- package/src/lib/server/sse.ts +2 -2
- package/src/lib/shared/DEFER.ts +8 -0
- package/src/lib/shared/activeCacheStore.ts +2 -2
- package/src/lib/shared/activePage.ts +2 -2
- package/src/lib/shared/attachRpcSelectorMethods.ts +42 -0
- package/src/lib/shared/attachSocketSelectorMethods.ts +34 -0
- package/src/lib/shared/basePath.ts +2 -2
- package/src/lib/shared/baseSlot.ts +6 -5
- package/src/lib/shared/bodyValueForKind.ts +1 -2
- package/src/lib/shared/buildSocketOverChannel.ts +40 -4
- package/src/lib/shared/cache.ts +477 -110
- package/src/lib/shared/cacheEntryFromSnapshot.ts +3 -22
- package/src/lib/shared/cacheStoreSlot.ts +9 -5
- package/src/lib/{server/runtime → shared}/createReachable.ts +2 -2
- package/src/lib/shared/createRemoteFunction.ts +58 -9
- package/src/lib/shared/createResolverSlot.ts +24 -31
- package/src/lib/shared/createSocketSubRegistry.ts +2 -2
- package/src/lib/shared/decodeRefJson.ts +4 -2
- package/src/lib/shared/decodeResponse.ts +8 -6
- package/src/lib/shared/done.ts +14 -0
- package/src/lib/shared/encodeRefJson.ts +4 -2
- package/src/lib/shared/globalCacheStoreSlot.ts +9 -6
- package/src/lib/shared/hydratingSlot.ts +12 -0
- package/src/lib/shared/isLayoutFile.ts +12 -0
- package/src/lib/shared/keyForRemoteCall.ts +2 -1
- package/src/lib/shared/keyPrefixForRemote.ts +12 -0
- package/src/lib/shared/matchRoute.ts +175 -0
- package/src/lib/shared/normalizePathname.ts +11 -0
- package/src/lib/shared/pageSlot.ts +14 -5
- package/src/lib/shared/pageUrlForFile.ts +3 -3
- package/src/lib/shared/parseRouteSegments.ts +16 -7
- package/src/lib/shared/patch.ts +41 -0
- package/src/lib/shared/peek.ts +35 -0
- package/src/lib/shared/prepareRemoteExport.ts +40 -0
- package/src/lib/shared/prepareRpcModule.ts +98 -16
- package/src/lib/shared/prepareSocketModule.ts +11 -15
- package/src/lib/shared/queryStringFromArgs.ts +11 -0
- package/src/lib/shared/reachable.ts +102 -0
- package/src/lib/shared/refresh.ts +22 -0
- package/src/lib/shared/requestScopeSlot.ts +10 -7
- package/src/lib/shared/routeParamsShape.ts +9 -9
- package/src/lib/shared/rpcErrorRegistry.ts +69 -0
- package/src/lib/shared/selectorPrefix.ts +3 -10
- package/src/lib/shared/setOwnProperty.ts +19 -0
- package/src/lib/shared/snippet.ts +1 -1
- package/src/lib/shared/subscribableProbes.ts +111 -0
- package/src/lib/shared/tailProbeSlot.ts +8 -1
- package/src/lib/shared/types/CacheEntry.ts +9 -15
- package/src/lib/shared/types/CacheOptions.ts +8 -0
- package/src/lib/shared/types/CacheSnapshotEntry.ts +0 -5
- package/src/lib/shared/types/RemoteCallable.ts +25 -7
- package/src/lib/shared/types/RemoteFunction.ts +48 -13
- package/src/lib/shared/types/ResolverSlot.ts +7 -4
- package/src/lib/shared/types/RpcError.ts +26 -0
- package/src/lib/shared/types/SmartReadOptions.ts +32 -0
- package/src/lib/shared/types/Socket.ts +54 -0
- package/src/lib/shared/types/SsrBootState.ts +27 -0
- package/src/lib/shared/types/SsrPayload.ts +21 -0
- package/src/lib/shared/types/Subscribable.ts +13 -13
- package/src/lib/shared/types/TailHooks.ts +2 -1
- package/src/lib/shared/url.ts +29 -14
- package/src/lib/shared/wakeHydrationPeeks.ts +20 -0
- package/src/lib/shared/writeTestSocketsDts.ts +1 -1
- package/src/lib/test/createScriptedSurface.ts +1 -1
- package/src/lib/test/createTestApp.ts +2 -2
- package/src/lib/test/createTestSocketChannel.ts +3 -3
- package/src/lib/ui/compile/REACTIVE_CALLEES.ts +5 -6
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +10 -7
- package/src/lib/ui/compile/abideUiPlugin.ts +2 -2
- package/src/lib/ui/compile/analyzeComponent.ts +28 -6
- package/src/lib/ui/compile/compileModule.ts +137 -11
- package/src/lib/ui/compile/compileShadow.ts +101 -84
- package/src/lib/ui/compile/desugarSignals.ts +120 -107
- package/src/lib/ui/compile/generateBuild.ts +47 -11
- package/src/lib/ui/compile/generateSSR.ts +117 -14
- package/src/lib/ui/compile/identifierReferencePattern.ts +13 -0
- package/src/lib/ui/compile/lowerContext.ts +10 -4
- package/src/lib/ui/compile/lowerScript.ts +72 -5
- package/src/lib/ui/compile/prepareNestedScript.ts +31 -17
- package/src/lib/ui/compile/resolveReactiveExport.ts +95 -0
- package/src/lib/ui/compile/signalCallee.ts +24 -0
- package/src/lib/ui/compile/stripEffects.ts +14 -11
- package/src/lib/ui/compile/types/AnalyzedComponent.ts +5 -0
- package/src/lib/ui/dom/appendStatic.ts +9 -1
- package/src/lib/ui/dom/appendText.ts +15 -3
- package/src/lib/ui/dom/assertClaimedText.ts +20 -0
- package/src/lib/ui/dom/awaitBlock.ts +17 -56
- package/src/lib/ui/dom/bindSelectValue.ts +48 -0
- package/src/lib/ui/dom/each.ts +12 -1
- package/src/lib/ui/dom/hydrate.ts +10 -0
- package/src/lib/ui/effect.ts +4 -3
- package/src/lib/ui/installHotBridge.ts +4 -2
- package/src/lib/ui/remoteProxy.ts +18 -2
- package/src/lib/ui/renderToStream.ts +9 -13
- package/src/lib/ui/resumeSeedScript.ts +2 -2
- package/src/lib/ui/router.ts +21 -2
- package/src/lib/ui/runtime/RESUME.ts +0 -6
- package/src/lib/ui/runtime/ambientScopeBacking.ts +1 -1
- package/src/lib/ui/runtime/types/SsrRender.ts +3 -4
- package/src/lib/ui/scope.ts +6 -3
- package/src/lib/ui/seedBootState.ts +53 -0
- package/src/lib/ui/socketChannel.ts +2 -2
- package/src/lib/ui/socketProxy.ts +9 -3
- package/src/lib/ui/startClient.ts +16 -30
- package/src/lib/ui/state.ts +44 -13
- package/src/lib/ui/sync.ts +11 -5
- package/src/lib/ui/tryEncodeResume.ts +2 -5
- package/src/lib/ui/types/Scope.ts +14 -10
- package/src/lib/ui/watch.ts +140 -0
- package/src/serverEntry.ts +6 -6
- package/template/CLAUDE.md +1 -1
- package/template/package.json +1 -1
- package/template/src/server/rpc/getHello.ts +3 -3
- package/template/src/ui/pages/page.abide +2 -2
- package/template/test/app.test.ts +1 -1
- package/src/lib/server/reachable.ts +0 -45
- package/src/lib/server/sockets/types/Socket.ts +0 -23
- package/src/lib/shared/CACHE_WRAPPED.ts +0 -9
- package/src/lib/shared/baseResolver.ts +0 -10
- package/src/lib/shared/cacheKeyOf.ts +0 -7
- package/src/lib/shared/cacheKeyStore.ts +0 -8
- package/src/lib/shared/cacheStoreResolver.ts +0 -12
- package/src/lib/shared/globalCacheStoreResolver.ts +0 -12
- package/src/lib/shared/pageResolver.ts +0 -16
- package/src/lib/shared/recordCacheKey.ts +0 -6
- package/src/lib/shared/requestScopeResolver.ts +0 -12
- package/src/lib/shared/setBaseResolver.ts +0 -4
- package/src/lib/shared/setCacheStoreResolver.ts +0 -4
- package/src/lib/shared/setGlobalCacheStoreResolver.ts +0 -4
- package/src/lib/shared/setPageResolver.ts +0 -4
- package/src/lib/shared/setRequestScopeResolver.ts +0 -4
- package/src/lib/shared/toBunRoutePattern.ts +0 -28
- package/src/lib/shared/types/TailOptions.ts +0 -10
- package/src/lib/ui/deferResume.ts +0 -29
- package/src/lib/ui/matchRoute.ts +0 -106
- package/src/lib/ui/tail.ts +0 -324
- /package/src/lib/{server/sockets → shared}/types/SocketClientFrame.ts +0 -0
- /package/src/lib/{server/sockets → shared}/types/SocketServerFrame.ts +0 -0
|
@@ -4,6 +4,7 @@ import { effect } from '../effect.ts'
|
|
|
4
4
|
import { claimChild } from '../runtime/claimChild.ts'
|
|
5
5
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
6
6
|
import { appendSnippet } from './appendSnippet.ts'
|
|
7
|
+
import { assertClaimedText } from './assertClaimedText.ts'
|
|
7
8
|
import { isComment } from './isComment.ts'
|
|
8
9
|
import { parseRawNodes } from './parseRawNodes.ts'
|
|
9
10
|
|
|
@@ -49,6 +50,12 @@ export function appendText(parent: Node, read: () => unknown, splitAlways = fals
|
|
|
49
50
|
const node = (
|
|
50
51
|
isText ? claimed : parent.insertBefore(document.createTextNode(''), claimed)
|
|
51
52
|
) as Text
|
|
53
|
+
/* The claimed SSR node must begin with this binding's value, or the split below
|
|
54
|
+
lands mid-run and orphans the tail — throw legibly at the divergence instead. A
|
|
55
|
+
synthesized empty node is this binding's own, so there's nothing to disagree. */
|
|
56
|
+
if (isText) {
|
|
57
|
+
assertClaimedText(node, value)
|
|
58
|
+
}
|
|
52
59
|
/* Peel this binding's text off the merged SSR node. A non-final binding in a
|
|
53
60
|
run (`splitAlways`) splits even when it consumes the whole node, leaving an
|
|
54
61
|
empty node for the next binding — otherwise an interpolation that renders to
|
|
@@ -87,12 +94,17 @@ function appendRawHtml(parent: Node, read: () => unknown): void {
|
|
|
87
94
|
let nodes: Node[] = []
|
|
88
95
|
|
|
89
96
|
const set = (value: string): void => {
|
|
97
|
+
/* Insert/remove via the anchor's LIVE parent, not the build-time `parent` — when
|
|
98
|
+
this interpolation is a bare child of a control-flow branch, `parent` is the
|
|
99
|
+
branch's build fragment, which the enclosing block has since emptied into the
|
|
100
|
+
document (same reason each/awaitBlock use the anchor's parentNode). */
|
|
101
|
+
const liveParent = anchor.parentNode ?? parent
|
|
90
102
|
for (const node of nodes) {
|
|
91
|
-
|
|
103
|
+
liveParent.removeChild(node)
|
|
92
104
|
}
|
|
93
|
-
nodes = parseRawNodes(
|
|
105
|
+
nodes = parseRawNodes(liveParent, value)
|
|
94
106
|
for (const node of nodes) {
|
|
95
|
-
|
|
107
|
+
liveParent.insertBefore(node, anchor)
|
|
96
108
|
}
|
|
97
109
|
}
|
|
98
110
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/*
|
|
2
|
+
A text claim that MUST match the server's rendered text. On hydrate,
|
|
3
|
+
appendStatic/appendText adopt a merged SSR text node and split it at the CLIENT
|
|
4
|
+
value's length, trusting the client renders the same string the server did. When
|
|
5
|
+
they diverge — a binding whose value only materializes client-side (a peeked cache
|
|
6
|
+
value, `Date.now()`, a random) — the split lands mid-run and orphans the tail,
|
|
7
|
+
silently corrupting the DOM several nodes downstream. This turns that into a legible
|
|
8
|
+
desync AT the divergence, the text counterpart of `claimExpected` for structure.
|
|
9
|
+
|
|
10
|
+
The claimed node's data must BEGIN with this binding's value: a merged run appends
|
|
11
|
+
the following siblings' text AFTER it, so the value is exactly the leading slice. A
|
|
12
|
+
mismatch means SSR and the client build disagree on the text here.
|
|
13
|
+
*/
|
|
14
|
+
export function assertClaimedText(node: Text, value: string): void {
|
|
15
|
+
if (!node.data.startsWith(value)) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
`[abide] hydration desync: expected server text beginning with ${JSON.stringify(value)} here, but the server DOM had ${JSON.stringify(node.data)} — SSR markup and the client build disagree on the text at this position.`,
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { activeCacheStore } from '../../shared/activeCacheStore.ts'
|
|
2
1
|
import { decodeRefJson } from '../../shared/decodeRefJson.ts'
|
|
3
2
|
import { effect } from '../effect.ts'
|
|
4
3
|
import { claimChild } from '../runtime/claimChild.ts'
|
|
5
4
|
import { generationGuard } from '../runtime/generationGuard.ts'
|
|
6
5
|
import { RANGE_CLOSE, RANGE_OPEN } from '../runtime/RANGE_MARKER.ts'
|
|
7
6
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
8
|
-
import type {
|
|
7
|
+
import type { ResumeEntry } from '../runtime/RESUME.ts'
|
|
9
8
|
import { RESUME } from '../runtime/RESUME.ts'
|
|
10
9
|
import { scope } from '../runtime/scope.ts'
|
|
11
10
|
import { scopeGroup } from '../runtime/scopeGroup.ts'
|
|
@@ -181,42 +180,6 @@ export function awaitBlock(
|
|
|
181
180
|
active = { start, end, dispose }
|
|
182
181
|
}
|
|
183
182
|
|
|
184
|
-
/* Inert adoption — a deferred `{#await cache()}` (large payload, shipped as a `{defer,key}`
|
|
185
|
-
resume marker). Adopt the SSR then-branch WITHOUT building its bindings or materializing
|
|
186
|
-
the awaited value: the server markup is already correct, so keep it verbatim and skip the
|
|
187
|
-
payload decode entirely (the boot-path cost deferral targets). Only subscribe THIS block's
|
|
188
|
-
effect to the cache key — reads, decodes and fetches all wait. A later cache.invalidate
|
|
189
|
-
re-runs the effect, which then reads the value for real and builds a fresh branch. Sound
|
|
190
|
-
because a display-first read replaces the whole branch on re-read anyway, so there is no
|
|
191
|
-
extra flash beyond the swap the re-read already performs. */
|
|
192
|
-
const adoptInert = (open: Node | null, key: string): void => {
|
|
193
|
-
const cursor = hydration as NonNullable<typeof hydration>
|
|
194
|
-
const firstKept = open?.nextSibling ?? null
|
|
195
|
-
/* Scan to THIS block's close marker, leaving every server node in place. Nested
|
|
196
|
-
blocks carry different ids, so the first data match is our own close. */
|
|
197
|
-
let node: Node | null = firstKept
|
|
198
|
-
while (node !== null && (node as { data?: string }).data !== `/abide:await:${id}`) {
|
|
199
|
-
node = node.nextSibling
|
|
200
|
-
}
|
|
201
|
-
const close = node
|
|
202
|
-
cursor.next.set(parent, close?.nextSibling ?? null)
|
|
203
|
-
/* Bracket the kept nodes as a `[`…`]` range + park an anchor, identical to `adopt`,
|
|
204
|
-
so the first re-run's `place`/`detach` evicts them like any other branch. */
|
|
205
|
-
const start = document.createComment(RANGE_OPEN)
|
|
206
|
-
parent.insertBefore(start, firstKept ?? close)
|
|
207
|
-
const end = document.createComment(RANGE_CLOSE)
|
|
208
|
-
parent.insertBefore(end, close)
|
|
209
|
-
anchor = document.createTextNode('')
|
|
210
|
-
parent.insertBefore(anchor, close)
|
|
211
|
-
/* No scope/effects were built for the kept nodes, so disposal only evicts the range. */
|
|
212
|
-
active = { start, end, dispose: () => undefined }
|
|
213
|
-
/* Not 'then' — the first re-run rebuilds the branch fresh via `place`. */
|
|
214
|
-
activeKind = 'pending'
|
|
215
|
-
/* Subscribe without a value read: no clone, no decode, no fetch — just the key, so
|
|
216
|
-
cache.invalidate re-runs this effect (createSubscriber ties it to the running scope). */
|
|
217
|
-
activeCacheStore().subscribe(key)
|
|
218
|
-
}
|
|
219
|
-
|
|
220
183
|
/* Discard the SSR boundary and (re)build the block from the live promise, fresh
|
|
221
184
|
(hydration off) — the recovery path when adoption can't use the server markup. */
|
|
222
185
|
const rebuildCold = (open: Node | null): void => {
|
|
@@ -253,28 +216,28 @@ export function awaitBlock(
|
|
|
253
216
|
lives. A decode failure (malformed/absent payload) reads as "no resume" — fall
|
|
254
217
|
through to the live promise rather than crash hydration. */
|
|
255
218
|
const raw = RESUME[id]
|
|
256
|
-
let decoded: ResumeEntry |
|
|
219
|
+
let decoded: ResumeEntry | undefined
|
|
257
220
|
if (raw !== undefined) {
|
|
258
221
|
try {
|
|
259
|
-
decoded = decodeRefJson(raw) as ResumeEntry
|
|
222
|
+
decoded = decodeRefJson(raw) as ResumeEntry
|
|
260
223
|
} catch {
|
|
261
224
|
decoded = undefined
|
|
262
225
|
}
|
|
263
226
|
}
|
|
264
|
-
/*
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
not throw past the decode try/catch and crash hydration. */
|
|
268
|
-
if (typeof decoded === 'object' && decoded !== null && 'defer' in decoded) {
|
|
269
|
-
adoptInert(open, decoded.key)
|
|
270
|
-
return
|
|
271
|
-
}
|
|
272
|
-
/* Non-deferred: read the promise now so the block subscribes to its reactive source
|
|
273
|
-
(a cache key) — warm on resume, so no round-trip — then adopt the resume value /
|
|
274
|
-
warm-sync result below, or discard and build the pending branch fresh. */
|
|
227
|
+
/* Read the promise now so the block subscribes to its reactive source (a cache key) —
|
|
228
|
+
warm on resume, so no round-trip — then adopt the resume value / warm-sync result
|
|
229
|
+
below, or discard and build the pending branch fresh. */
|
|
275
230
|
const result = promiseThunk()
|
|
276
|
-
const entry = decoded
|
|
231
|
+
const entry = decoded
|
|
277
232
|
if (entry !== undefined) {
|
|
233
|
+
/* The resume entry drives the adopted branch below; the live `result` read to
|
|
234
|
+
subscribe the reactive source is discarded here. Swallow its rejection — a
|
|
235
|
+
cold-cache warm-seed miss starts a real fetch, and if that fetch rejects it
|
|
236
|
+
would otherwise surface as an unhandledrejection at boot (process-fatal under
|
|
237
|
+
Bun) instead of being harmlessly dropped. */
|
|
238
|
+
if (isThenable(result)) {
|
|
239
|
+
void (result as PromiseLike<unknown>).then(undefined, () => undefined)
|
|
240
|
+
}
|
|
278
241
|
/* Build the adopted branch around a value CELL (then) so a later re-run updates
|
|
279
242
|
it in place, exactly like a fresh mount. The `throw` for a catch-less rejection
|
|
280
243
|
stays OUTSIDE the adopt try/catch so it surfaces rather than triggering the
|
|
@@ -336,10 +299,8 @@ export function awaitBlock(
|
|
|
336
299
|
if (first) {
|
|
337
300
|
first = false
|
|
338
301
|
if (hydration !== undefined) {
|
|
339
|
-
/* firstHydrate reads the promise ITSELF
|
|
340
|
-
|
|
341
|
-
value we're deferring) and subscribes by key instead; every other path reads
|
|
342
|
-
it so the block subscribes to its reactive source (a cache key). */
|
|
302
|
+
/* firstHydrate reads the promise ITSELF so the block subscribes to its reactive
|
|
303
|
+
source (a cache key), then adopts the resume value / warm-sync result. */
|
|
343
304
|
firstHydrate()
|
|
344
305
|
return
|
|
345
306
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { effect } from '../effect.ts'
|
|
2
|
+
import { on } from './on.ts'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
Two-way `bind:value` for a `<select>`. The bound value drives the selection through
|
|
6
|
+
a reactive effect, and the user's pick writes back on `change`. A plain `bind:value`
|
|
7
|
+
would set `el.value` once, up front — but a `<select>`'s options are frequently
|
|
8
|
+
mounted AFTER the binding runs (a `{#for}` child, an async list), and the browser
|
|
9
|
+
silently drops a `value` assignment naming an option that isn't present yet. So a
|
|
10
|
+
`MutationObserver` re-applies the selection whenever the option set changes (its
|
|
11
|
+
callback runs in a microtask before paint, so no visible flash), disconnected on
|
|
12
|
+
teardown. `multiple` switches single-value (`el.value`) semantics for array
|
|
13
|
+
membership: each option's `selected` is set from the bound array, and the write-back
|
|
14
|
+
collects `selectedOptions` back into an array.
|
|
15
|
+
*/
|
|
16
|
+
// @documentation plumbing
|
|
17
|
+
export function bindSelectValue(
|
|
18
|
+
element: HTMLSelectElement,
|
|
19
|
+
read: () => unknown,
|
|
20
|
+
write: (value: unknown) => void,
|
|
21
|
+
multiple: boolean,
|
|
22
|
+
): void {
|
|
23
|
+
/* Push the bound value into the DOM selection. */
|
|
24
|
+
const apply = (): void => {
|
|
25
|
+
if (multiple) {
|
|
26
|
+
const selected = (read() as unknown[]) ?? []
|
|
27
|
+
for (const option of element.options) {
|
|
28
|
+
option.selected = selected.includes(option.value)
|
|
29
|
+
}
|
|
30
|
+
} else {
|
|
31
|
+
element.value = read() as string
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/* Read the current DOM selection back out for the write. */
|
|
35
|
+
const collect = (): unknown =>
|
|
36
|
+
multiple ? Array.from(element.selectedOptions, (option) => option.value) : element.value
|
|
37
|
+
/* Re-apply whenever the bound value changes (this effect tracks `read`). */
|
|
38
|
+
effect(apply)
|
|
39
|
+
/* Re-apply whenever the option set changes — covers `{#for}`/async options that
|
|
40
|
+
mount after the first apply. This effect reads nothing reactive, so it runs once
|
|
41
|
+
and only its teardown (disconnect) fires on dispose. */
|
|
42
|
+
effect(() => {
|
|
43
|
+
const observer = new MutationObserver(apply)
|
|
44
|
+
observer.observe(element, { childList: true, subtree: true })
|
|
45
|
+
return () => observer.disconnect()
|
|
46
|
+
})
|
|
47
|
+
on(element, 'change', () => write(collect()))
|
|
48
|
+
}
|
package/src/lib/ui/dom/each.ts
CHANGED
|
@@ -107,7 +107,18 @@ export function each<T>(
|
|
|
107
107
|
if (hydration !== undefined) {
|
|
108
108
|
let position = 0
|
|
109
109
|
for (const item of items()) {
|
|
110
|
-
|
|
110
|
+
const key = keyOf(item)
|
|
111
|
+
const row = buildRow(item, position) // claims the SSR row where it sits
|
|
112
|
+
if (rows.has(key)) {
|
|
113
|
+
/* Duplicate key: this row already claimed its SSR markers, but the map holds
|
|
114
|
+
one row per key (create mode keeps the FIRST built row too). Dispose+remove
|
|
115
|
+
this later duplicate so it isn't orphaned on screen forever — the reconcile
|
|
116
|
+
prune only reaches rows still in the map. */
|
|
117
|
+
row.dispose()
|
|
118
|
+
removeRange(row.start, row.end)
|
|
119
|
+
} else {
|
|
120
|
+
rows.set(key, row)
|
|
121
|
+
}
|
|
111
122
|
position += 1
|
|
112
123
|
}
|
|
113
124
|
anchor = document.createTextNode('')
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { hydratingSlot } from '../../shared/hydratingSlot.ts'
|
|
2
|
+
import { wakeHydrationPeeks } from '../../shared/wakeHydrationPeeks.ts'
|
|
1
3
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
2
4
|
import { scope } from '../runtime/scope.ts'
|
|
3
5
|
import { scopeLabel } from './scopeLabel.ts'
|
|
@@ -25,7 +27,9 @@ export function hydrate(
|
|
|
25
27
|
props?: unknown,
|
|
26
28
|
): () => void {
|
|
27
29
|
const previous = RENDER.hydration
|
|
30
|
+
const previousHydrating = hydratingSlot.active
|
|
28
31
|
RENDER.hydration = { next: new Map() }
|
|
32
|
+
hydratingSlot.active = true
|
|
29
33
|
try {
|
|
30
34
|
/* Same shared mount core as `mount` (see `withScope`) — a hydrated component owns a
|
|
31
35
|
scope too, adopting the model its build adopts — run with the claim cursor active. */
|
|
@@ -33,5 +37,11 @@ export function hydrate(
|
|
|
33
37
|
return () => lexical.dispose()
|
|
34
38
|
} finally {
|
|
35
39
|
RENDER.hydration = previous
|
|
40
|
+
hydratingSlot.active = previousHydrating
|
|
41
|
+
/* Wake the peeks this pass withheld — only when unwinding the OUTERMOST pass, so a
|
|
42
|
+
nested child hydrate doesn't fire it early (save/restore, not a bare reset). */
|
|
43
|
+
if (!previousHydrating) {
|
|
44
|
+
wakeHydrationPeeks()
|
|
45
|
+
}
|
|
36
46
|
}
|
|
37
47
|
}
|
package/src/lib/ui/effect.ts
CHANGED
|
@@ -10,9 +10,10 @@ dispose — and may be async (its teardown then runs once the promise settles, b
|
|
|
10
10
|
only the reads before its first `await` are tracked). Returns a dispose that runs
|
|
11
11
|
the final teardown and detaches it from the graph. This is abide's from-scratch
|
|
12
12
|
effect primitive: the open-on-first-read / close-on-last-reader lifecycle,
|
|
13
|
-
grounded in abide's own reactive core.
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
grounded in abide's own reactive core. Imported and called bare
|
|
14
|
+
(`import { effect } from '@abide/abide/ui/effect'`); the compiler resolves the import
|
|
15
|
+
binding (alias-safe) and the SSR back-end strips the call (effects are client
|
|
16
|
+
lifecycle). Generated binding code also calls it directly.
|
|
16
17
|
*/
|
|
17
18
|
// @documentation plumbing
|
|
18
19
|
export function effect(fn: () => EffectResult): () => void {
|
|
@@ -7,6 +7,7 @@ import { appendTextAt } from './dom/appendTextAt.ts'
|
|
|
7
7
|
import { attach } from './dom/attach.ts'
|
|
8
8
|
import { attr } from './dom/attr.ts'
|
|
9
9
|
import { awaitBlock } from './dom/awaitBlock.ts'
|
|
10
|
+
import { bindSelectValue } from './dom/bindSelectValue.ts'
|
|
10
11
|
import { cloneStatic } from './dom/cloneStatic.ts'
|
|
11
12
|
import { each } from './dom/each.ts'
|
|
12
13
|
import { eachAsync } from './dom/eachAsync.ts'
|
|
@@ -25,7 +26,6 @@ import { spreadProps } from './dom/spreadProps.ts'
|
|
|
25
26
|
import { switchBlock } from './dom/switchBlock.ts'
|
|
26
27
|
import { tryBlock } from './dom/tryBlock.ts'
|
|
27
28
|
import { when } from './dom/when.ts'
|
|
28
|
-
import { deferResume } from './deferResume.ts'
|
|
29
29
|
import { effect } from './effect.ts'
|
|
30
30
|
import { enterScope } from './enterScope.ts'
|
|
31
31
|
import { exitScope } from './exitScope.ts'
|
|
@@ -36,6 +36,7 @@ import { hotReloadEnabled } from './runtime/hotReloadEnabled.ts'
|
|
|
36
36
|
import { hotReplace } from './runtime/hotReplace.ts'
|
|
37
37
|
import { nextBlockId } from './runtime/nextBlockId.ts'
|
|
38
38
|
import { scope } from './scope.ts'
|
|
39
|
+
import { watch } from './watch.ts'
|
|
39
40
|
|
|
40
41
|
/*
|
|
41
42
|
Dev-only: exposes the abide-ui runtime plus `hotReplace` on `window.__abide`, and
|
|
@@ -54,6 +55,7 @@ export function installHotBridge(): void {
|
|
|
54
55
|
enterScope,
|
|
55
56
|
exitScope,
|
|
56
57
|
effect,
|
|
58
|
+
watch,
|
|
57
59
|
mount,
|
|
58
60
|
appendText,
|
|
59
61
|
appendTextAt,
|
|
@@ -65,11 +67,11 @@ export function installHotBridge(): void {
|
|
|
65
67
|
attr,
|
|
66
68
|
on,
|
|
67
69
|
attach,
|
|
70
|
+
bindSelectValue,
|
|
68
71
|
each,
|
|
69
72
|
eachAsync,
|
|
70
73
|
when,
|
|
71
74
|
awaitBlock,
|
|
72
|
-
deferResume,
|
|
73
75
|
tryBlock,
|
|
74
76
|
switchBlock,
|
|
75
77
|
mountSlot,
|
|
@@ -17,16 +17,20 @@ import { outboxRegistry } from './rpcOutbox/outboxRegistry.ts'
|
|
|
17
17
|
import { currentAbortSignal } from './runtime/currentAbortSignal.ts'
|
|
18
18
|
import { REQUEST_SUPERSEDED } from './runtime/REQUEST_SUPERSEDED.ts'
|
|
19
19
|
import type { PersistenceStore } from './types/PersistenceStore.ts'
|
|
20
|
+
import { watch } from './watch.ts'
|
|
20
21
|
|
|
21
22
|
/* The framework-reserved `HttpError.kind` for a request the durable outbox parked because
|
|
22
23
|
the server was unreachable — distinct from a handler-declared error name. Lets a caller
|
|
23
24
|
branch with `error instanceof HttpError && error.kind === 'queued'`. */
|
|
24
25
|
const QUEUED = 'queued'
|
|
25
26
|
|
|
26
|
-
/*
|
|
27
|
-
|
|
27
|
+
/* Build-time options the bundler stamps onto the client proxy stub. `outbox: true` parks an
|
|
28
|
+
unreachable call for replay; `streaming: true` (handler returns jsonl()/sse()) makes the bare
|
|
29
|
+
call return the Subscribable directly; `store` exists for testing (production uses the default
|
|
30
|
+
persistence store). */
|
|
28
31
|
export type DurableOptions = {
|
|
29
32
|
outbox?: boolean
|
|
33
|
+
streaming?: boolean
|
|
30
34
|
store?: PersistenceStore
|
|
31
35
|
}
|
|
32
36
|
|
|
@@ -75,6 +79,7 @@ export function remoteProxy<Args, Return>(
|
|
|
75
79
|
method,
|
|
76
80
|
url,
|
|
77
81
|
clients: browserClientFlags,
|
|
82
|
+
streaming: durable?.streaming ?? false,
|
|
78
83
|
/*
|
|
79
84
|
The Request URL carries the mount base so the fetch routes through the
|
|
80
85
|
proxy (/v2/rpc/…); the cache key keeps the bare `url` (keyForRemoteCall
|
|
@@ -143,6 +148,17 @@ export function remoteProxy<Args, Return>(
|
|
|
143
148
|
)
|
|
144
149
|
},
|
|
145
150
|
})
|
|
151
|
+
/* Overwrite the inert `.watch` the shared attach bound: on the client the real reaction
|
|
152
|
+
sugar routes to the `watch` ui primitive (`fn.watch(handler)` / `fn.watch(args, handler)`).
|
|
153
|
+
Attached here — not in the shared attach — so `watch` never rides into a server bundle. */
|
|
154
|
+
Object.assign(fn, {
|
|
155
|
+
watch: (argsOrHandler: unknown, handler?: unknown) =>
|
|
156
|
+
(watch as (source: unknown, a?: unknown, b?: unknown) => () => void)(
|
|
157
|
+
fn,
|
|
158
|
+
argsOrHandler,
|
|
159
|
+
handler,
|
|
160
|
+
),
|
|
161
|
+
})
|
|
146
162
|
if (durable?.outbox === true) {
|
|
147
163
|
queue = getOrCreateOutboxQueue<Args, Return>(url, fn, durable)
|
|
148
164
|
Object.assign(fn, { outbox: outboxFace(queue) })
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { deferResume } from './deferResume.ts'
|
|
2
1
|
import { resumeSeedScript } from './resumeSeedScript.ts'
|
|
3
|
-
import type {
|
|
2
|
+
import type { ResumeEntry } from './runtime/RESUME.ts'
|
|
4
3
|
import type { SsrAwait, SsrRender } from './runtime/types/SsrRender.ts'
|
|
5
4
|
import { tryEncodeResume } from './tryEncodeResume.ts'
|
|
6
5
|
|
|
@@ -40,8 +39,8 @@ export async function* renderToStream(
|
|
|
40
39
|
instead of refetching. (`resume` is the render body's live object, so late writes
|
|
41
40
|
appear here.) */
|
|
42
41
|
const seededResume = new Set<number>(Object.keys(resume).map(Number))
|
|
43
|
-
const resumeDelta = (): Record<number, ResumeEntry
|
|
44
|
-
const delta: Record<number, ResumeEntry
|
|
42
|
+
const resumeDelta = (): Record<number, ResumeEntry> => {
|
|
43
|
+
const delta: Record<number, ResumeEntry> = {}
|
|
45
44
|
for (const [key, entry] of Object.entries(resume)) {
|
|
46
45
|
const id = Number(key)
|
|
47
46
|
if (!seededResume.has(id)) {
|
|
@@ -84,22 +83,19 @@ export async function* renderToStream(
|
|
|
84
83
|
}
|
|
85
84
|
}
|
|
86
85
|
|
|
87
|
-
type Settled = { id: number; html: string; resume: ResumeEntry
|
|
86
|
+
type Settled = { id: number; html: string; resume: ResumeEntry }
|
|
88
87
|
|
|
89
88
|
/* Awaits one streaming block's promise and renders the resolved or error branch to
|
|
90
89
|
HTML (the renderers are async so a nested `await` block composes), capturing the
|
|
91
90
|
value (serializable) for the resume manifest. Errors serialize as their message —
|
|
92
|
-
enough for the catch branch, without leaking a stack.
|
|
93
|
-
|
|
94
|
-
branch inert instead of decoding the value; a non-cache value ships inline as before. */
|
|
91
|
+
enough for the catch branch, without leaking a stack. The resolved value ships inline
|
|
92
|
+
so hydration adopts the streamed branch warm. */
|
|
95
93
|
function settle(block: SsrAwait): Promise<Settled> {
|
|
96
|
-
|
|
97
|
-
const pending = block.promise()
|
|
98
|
-
return Promise.resolve(pending).then(
|
|
94
|
+
return Promise.resolve(block.promise()).then(
|
|
99
95
|
async (value) => ({
|
|
100
96
|
id: block.id,
|
|
101
97
|
html: await block.then(value),
|
|
102
|
-
resume:
|
|
98
|
+
resume: { ok: true, value },
|
|
103
99
|
}),
|
|
104
100
|
async (error) => {
|
|
105
101
|
/* No catch branch → surface the rejection (500 before the first flush,
|
|
@@ -123,6 +119,6 @@ function settle(block: SsrAwait): Promise<Settled> {
|
|
|
123
119
|
strings. `tryEncodeResume` handles the serialize-or-refetch policy (undefined → no
|
|
124
120
|
script → the swap consumers skip registration → hydration re-runs that one promise).
|
|
125
121
|
`applyResolved`/the inline swap script store it via `.textContent`; `awaitBlock` decodes it. */
|
|
126
|
-
function encodeStreamResume(resume: ResumeEntry
|
|
122
|
+
function encodeStreamResume(resume: ResumeEntry, id: number): string | undefined {
|
|
127
123
|
return tryEncodeResume(resume, id)?.replace(/</g, '\\u003c')
|
|
128
124
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { safeJsonForScript } from '../shared/safeJsonForScript.ts'
|
|
2
|
-
import type {
|
|
2
|
+
import type { ResumeEntry } from './runtime/RESUME.ts'
|
|
3
3
|
import { tryEncodeResume } from './tryEncodeResume.ts'
|
|
4
4
|
|
|
5
5
|
/* A self-contained `<script>` seeding the await-resume manifest with the blocking
|
|
@@ -11,7 +11,7 @@ import { tryEncodeResume } from './tryEncodeResume.ts'
|
|
|
11
11
|
early or parse as a line terminator. Shared by the buffered (`createUiPageRenderer`)
|
|
12
12
|
and streaming (`renderToStream`) paths. */
|
|
13
13
|
// @documentation plumbing
|
|
14
|
-
export function resumeSeedScript(resume: Record<number, ResumeEntry
|
|
14
|
+
export function resumeSeedScript(resume: Record<number, ResumeEntry>): string {
|
|
15
15
|
/* ref-json (not JSON) so a value carrying cycles or shared back-references — a
|
|
16
16
|
media tree with parent↔child links — seeds instead of being dropped. `tryEncodeResume`
|
|
17
17
|
drops just an unserializable entry (the client re-runs that one branch's promise),
|
package/src/lib/ui/router.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { hydratingSlot } from '../shared/hydratingSlot.ts'
|
|
1
2
|
import { layoutChainForRoute } from '../shared/layoutChainForRoute.ts'
|
|
3
|
+
import { matchRoute } from '../shared/matchRoute.ts'
|
|
4
|
+
import { wakeHydrationPeeks } from '../shared/wakeHydrationPeeks.ts'
|
|
2
5
|
import { fillBoundary } from './dom/fillBoundary.ts'
|
|
3
6
|
import { outlet } from './dom/outlet.ts'
|
|
4
7
|
import { effect } from './effect.ts'
|
|
5
|
-
import { matchRoute } from './matchRoute.ts'
|
|
6
8
|
import { navigatePath } from './navigate.ts'
|
|
7
9
|
import { CHILD_PRESENT } from './runtime/CHILD_PRESENT.ts'
|
|
8
10
|
import { clientPage } from './runtime/clientPage.ts'
|
|
@@ -81,7 +83,8 @@ function clearRecoveryReloads(url: string): void {
|
|
|
81
83
|
|
|
82
84
|
/*
|
|
83
85
|
A minimal client router on the History API. `router` matches the current path
|
|
84
|
-
against the route patterns (literal / `[name]` / `[...rest]`, via
|
|
86
|
+
against the route patterns (literal / `[name]` / `[[name]]` / `[...rest]`, via
|
|
87
|
+
the shared matchRoute — the same matcher the server dispatches with),
|
|
85
88
|
imports the matching page's chunk — plus every `layout.abide` chunk that wraps it
|
|
86
89
|
(`layoutChainForRoute`) — on demand, and mounts them as a chain: each layout into
|
|
87
90
|
the previous layout's `<slot/>` outlet, the page into the innermost outlet.
|
|
@@ -249,13 +252,21 @@ export function router(
|
|
|
249
252
|
}
|
|
250
253
|
if (hydrating) {
|
|
251
254
|
const previous = RENDER.hydration
|
|
255
|
+
const previousHydrating = hydratingSlot.active
|
|
252
256
|
RENDER.hydration = { next: new Map() }
|
|
257
|
+
hydratingSlot.active = true
|
|
253
258
|
enterRenderPass()
|
|
254
259
|
try {
|
|
255
260
|
run()
|
|
256
261
|
} finally {
|
|
257
262
|
exitRenderPass()
|
|
258
263
|
RENDER.hydration = previous
|
|
264
|
+
hydratingSlot.active = previousHydrating
|
|
265
|
+
/* Wake the peeks this pass withheld for SSR congruence, now that the pass is
|
|
266
|
+
over and the warm value is congruent to show. Only on the outermost unwind. */
|
|
267
|
+
if (!previousHydrating) {
|
|
268
|
+
wakeHydrationPeeks()
|
|
269
|
+
}
|
|
259
270
|
}
|
|
260
271
|
return
|
|
261
272
|
}
|
|
@@ -428,6 +439,14 @@ export function router(
|
|
|
428
439
|
])
|
|
429
440
|
.then(([pageView, resolvedLayouts, decision]) => {
|
|
430
441
|
if (token !== sequence || disposed) {
|
|
442
|
+
/* Consume the first-paint flag even when superseded: the boot SSR DOM
|
|
443
|
+
is valid only for THIS boot navigation. Leaving `first` true would
|
|
444
|
+
let a navigation that started before this settled (to a different
|
|
445
|
+
page) skip its auth probe and hydrate the boot URL's SSR DOM as its
|
|
446
|
+
own — claimExpected then throws and commit swallows it, stranding a
|
|
447
|
+
broken page. Cleared here → the superseding nav builds create-mode
|
|
448
|
+
(clearing the stale SSR DOM) and runs its probe. */
|
|
449
|
+
first = false
|
|
431
450
|
return
|
|
432
451
|
}
|
|
433
452
|
/* handle() redirected: go where it pointed, replacing the blocked
|
|
@@ -16,12 +16,6 @@
|
|
|
16
16
|
same reference. */
|
|
17
17
|
export type ResumeEntry = { ok: true; value: unknown } | { ok: false; error: unknown }
|
|
18
18
|
|
|
19
|
-
/* Deferred-resume marker (Tier 2): a large cache-backed `{#await cache()}` ships this in
|
|
20
|
-
place of its value — just the cache key, so hydration adopts the server branch inert and
|
|
21
|
-
pays no value decode. The client seeds that key lazily and materializes it only on a later
|
|
22
|
-
re-read. Discriminated from a ResumeEntry by the `defer` field. */
|
|
23
|
-
export type DeferMarker = { defer: true; key: string }
|
|
24
|
-
|
|
25
19
|
const globalScope = globalThis as { __abideResume?: Record<number, string> }
|
|
26
20
|
globalScope.__abideResume ??= {}
|
|
27
21
|
|
|
@@ -9,7 +9,7 @@ with `enterScope`/`exitScope` and `await`s inline between them (blocking `{#awai
|
|
|
9
9
|
child renders, slots, a top-level `await`). A module global held across those awaits
|
|
10
10
|
interleaves across concurrent requests — one render resumes to read another's scope.
|
|
11
11
|
Keying the ambient off the per-request store (which the async context propagates
|
|
12
|
-
correctly) isolates it. Mirrors `
|
|
12
|
+
correctly) isolates it. Mirrors `requestScopeSlot`'s server-installed slot; the
|
|
13
13
|
indirection cost lands at build/effect-creation time, never on the signal hot path.
|
|
14
14
|
*/
|
|
15
15
|
type ScopeBacking = { get(): Scope | undefined; set(value: Scope | undefined): void }
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ResumeEntry } from '../RESUME.ts'
|
|
2
2
|
|
|
3
3
|
/* One STREAMING await block captured during SSR (no `then` on the `await` tag): its
|
|
4
4
|
boundary id, the promise to await, and the async string-renderers for the resolved
|
|
@@ -18,11 +18,10 @@ export type SsrAwait = {
|
|
|
18
18
|
/* The result of a component's server `render()`: the pending-shell HTML, the
|
|
19
19
|
serializable document snapshot for client resume, the STREAMING await blocks to
|
|
20
20
|
flush out of order, and `resume` — the inline-rendered BLOCKING await values keyed
|
|
21
|
-
by boundary id, seeded into the manifest so hydration adopts them without a refetch.
|
|
22
|
-
deferred (cache-backed) blocking value is a `DeferMarker` in place of the value. */
|
|
21
|
+
by boundary id, seeded into the manifest so hydration adopts them without a refetch. */
|
|
23
22
|
export type SsrRender = {
|
|
24
23
|
html: string
|
|
25
24
|
state: unknown
|
|
26
25
|
awaits: SsrAwait[]
|
|
27
|
-
resume: Record<number, ResumeEntry
|
|
26
|
+
resume: Record<number, ResumeEntry>
|
|
28
27
|
}
|
package/src/lib/ui/scope.ts
CHANGED
|
@@ -5,10 +5,13 @@ import type { Scope } from './types/Scope.ts'
|
|
|
5
5
|
/*
|
|
6
6
|
Resolves the current lexical scope — established per lexical level by the compiler,
|
|
7
7
|
so it reads "where you are" with no handle. Outside any scope (boot, a script) it
|
|
8
|
-
mints a detached root once and reuses it.
|
|
9
|
-
|
|
8
|
+
mints a detached root once and reuses it. Now the INTERNAL lowering host: the author
|
|
9
|
+
reactive surface is the imported `state`/`state.linked`/`state.computed`/`effect`; the
|
|
10
|
+
compiler lowers each onto this scope (`$$scope().derive`/`.linked`/`.effect`), and
|
|
11
|
+
`state.share`/`.shared` route through it. Still published so generated code and the
|
|
12
|
+
type-checking shadow can import it; not part of the author-facing surface.
|
|
10
13
|
*/
|
|
11
|
-
// @documentation
|
|
14
|
+
// @documentation plumbing
|
|
12
15
|
export function scope(): Scope {
|
|
13
16
|
if (!CURRENT_SCOPE.current) {
|
|
14
17
|
CURRENT_SCOPE.current = createScope()
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { baseSlot } from '../shared/baseSlot.ts'
|
|
2
|
+
import { createTraceContext } from '../shared/createTraceContext.ts'
|
|
3
|
+
import { healthSeedSlot } from '../shared/healthSeedSlot.ts'
|
|
4
|
+
import { requestScopeSlot } from '../shared/requestScopeSlot.ts'
|
|
5
|
+
import { rpcTimeoutSlot } from '../shared/rpcTimeoutSlot.ts'
|
|
6
|
+
import { setAppName } from '../shared/setAppName.ts'
|
|
7
|
+
import type { SsrBootState } from '../shared/types/SsrBootState.ts'
|
|
8
|
+
|
|
9
|
+
/* Seeds every `__SSR__` boot-state field into its ambient slot. The map is typed
|
|
10
|
+
EXHAUSTIVELY over `SsrBootState`, so a boot field can't be added to the payload
|
|
11
|
+
without a seeder here — the compile error is what guarantees a stamped field is
|
|
12
|
+
never silently dropped client-side. Base is seeded first (object-key order), as the
|
|
13
|
+
prior hand-written sequence required: the cache seed that follows keys call urls
|
|
14
|
+
through the base resolver. */
|
|
15
|
+
const SEED: { [K in keyof Required<SsrBootState>]: (value: SsrBootState[K]) => void } = {
|
|
16
|
+
base: (value) => {
|
|
17
|
+
baseSlot.resolver = () => value ?? ''
|
|
18
|
+
},
|
|
19
|
+
app: (value) => setAppName(value),
|
|
20
|
+
health: (value) => {
|
|
21
|
+
healthSeedSlot.payload = value
|
|
22
|
+
},
|
|
23
|
+
clientTimeout: (value) => {
|
|
24
|
+
rpcTimeoutSlot.ms = value
|
|
25
|
+
},
|
|
26
|
+
/* Continue the SSR request's trace (or mint a fresh sampled one if the stamp is
|
|
27
|
+
missing) and publish the browser's request scope: trace(), log line prefixes, and
|
|
28
|
+
the RPC traceparent header resolve through it. elapsedMs is navigation-relative —
|
|
29
|
+
exactly what performance.now() measures — and the path tracks client navigations
|
|
30
|
+
because the resolver reads location per call. */
|
|
31
|
+
trace: (value) => {
|
|
32
|
+
const traceContext = createTraceContext(value)
|
|
33
|
+
requestScopeSlot.resolver = () => ({
|
|
34
|
+
trace: traceContext,
|
|
35
|
+
elapsedMs: performance.now(),
|
|
36
|
+
method: 'GET',
|
|
37
|
+
path: typeof location === 'undefined' ? '' : location.pathname,
|
|
38
|
+
})
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* Applies one seeder to its own field — generic over the key so the seeder and its
|
|
43
|
+
value stay correlated (a bare `SEED[key](boot[key])` loses the correlation). */
|
|
44
|
+
function seedField<K extends keyof SsrBootState>(key: K, boot: SsrBootState): void {
|
|
45
|
+
SEED[key](boot[key])
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* Seeds all boot-state slots from the payload, before mount. */
|
|
49
|
+
export function seedBootState(boot: SsrBootState): void {
|
|
50
|
+
for (const key of Object.keys(SEED) as Array<keyof SsrBootState>) {
|
|
51
|
+
seedField(key, boot)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { SocketClientFrame } from '../server/sockets/types/SocketClientFrame.ts'
|
|
2
|
-
import type { SocketServerFrame } from '../server/sockets/types/SocketServerFrame.ts'
|
|
3
1
|
import { createSocketSubRegistry } from '../shared/createSocketSubRegistry.ts'
|
|
4
2
|
import { decodeRefJson } from '../shared/decodeRefJson.ts'
|
|
5
3
|
import { encodeRefJson } from '../shared/encodeRefJson.ts'
|
|
6
4
|
import { SOCKETS_PATH } from '../shared/SOCKETS_PATH.ts'
|
|
7
5
|
import type { SocketChannel } from '../shared/types/SocketChannel.ts'
|
|
6
|
+
import type { SocketClientFrame } from '../shared/types/SocketClientFrame.ts'
|
|
7
|
+
import type { SocketServerFrame } from '../shared/types/SocketServerFrame.ts'
|
|
8
8
|
import { withBase } from '../shared/withBase.ts'
|
|
9
9
|
|
|
10
10
|
let singleton: SocketChannel | undefined
|