@abide/abide 0.46.0 → 0.48.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 +370 -320
- package/CHANGELOG.md +94 -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 +5 -0
- package/src/lib/server/runtime/buildCacheSnapshot.ts +6 -6
- 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/runtime/types/InspectorCacheEntry.ts +1 -1
- package/src/lib/server/runtime/types/InspectorCacheSnapshot.ts +4 -4
- package/src/lib/server/runtime/types/InspectorContext.ts +1 -1
- 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 +522 -117
- package/src/lib/shared/cacheEntryFromSnapshot.ts +3 -22
- package/src/lib/shared/cacheStoreSlot.ts +9 -5
- package/src/lib/shared/cacheStores.ts +3 -3
- 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/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/sharedCacheStore.ts +14 -0
- package/src/lib/shared/sharedCacheStoreSlot.ts +10 -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 +23 -11
- package/src/lib/shared/types/CacheSnapshotEntry.ts +0 -5
- package/src/lib/shared/types/CacheStats.ts +1 -1
- 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 +36 -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 +8 -8
- 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 +48 -16
- 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/parseTemplate.ts +1 -14
- 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/compile/types/TemplateNode.ts +0 -5
- 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 +19 -83
- 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/dom/mountChild.ts +2 -5
- package/src/lib/ui/dom/mountRange.ts +0 -75
- 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 +17 -31
- 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 +13 -11
- 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/DEFER_MIN_ARRAY_LENGTH.ts +0 -14
- 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/globalCacheStore.ts +0 -15
- package/src/lib/shared/globalCacheStoreResolver.ts +0 -12
- package/src/lib/shared/globalCacheStoreSlot.ts +0 -9
- 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 -36
- package/src/lib/ui/dom/firstElementBetween.ts +0 -14
- package/src/lib/ui/matchRoute.ts +0 -106
- package/src/lib/ui/runtime/scheduleWake.ts +0 -28
- package/src/lib/ui/runtime/whenIdle.ts +0 -21
- package/src/lib/ui/runtime/whenVisible.ts +0 -105
- 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
|
@@ -80,11 +80,6 @@ export type TemplateNode =
|
|
|
80
80
|
the prop name's source offset — the anchor for an excess-prop diagnostic. */
|
|
81
81
|
props: { name: string; code: string; loc?: number; nameLoc?: number; spread?: boolean }[]
|
|
82
82
|
children: TemplateNode[]
|
|
83
|
-
/* `client:idle` / `client:visible` on the tag → hydrate this child as an island
|
|
84
|
-
(server markup kept, build deferred to the trigger). Extracted from the attributes
|
|
85
|
-
so it never becomes a prop; the build back-end passes it to `mountChild`, SSR and
|
|
86
|
-
the prop check ignore it (an island renders fully on the server). */
|
|
87
|
-
clientTrigger?: 'idle' | 'visible'
|
|
88
83
|
}
|
|
89
84
|
| { kind: 'switch'; subject: string; children: TemplateNode[]; loc?: number }
|
|
90
85
|
/* A branch of a `<template switch>` (`match` set) or `<template if>` chain. Inside an
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { claimChild } from '../runtime/claimChild.ts'
|
|
2
2
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
3
|
+
import { assertClaimedText } from './assertClaimedText.ts'
|
|
3
4
|
|
|
4
5
|
/*
|
|
5
6
|
A static (non-reactive) text node under `parent`: created and appended (create
|
|
@@ -12,10 +13,17 @@ export function appendStatic(parent: Node, value: string, splitAlways = false):
|
|
|
12
13
|
const hydration = RENDER.hydration
|
|
13
14
|
if (hydration !== undefined) {
|
|
14
15
|
const node = claimChild(hydration, parent) as unknown as Text
|
|
16
|
+
const isText = node !== null && typeof node.splitText === 'function'
|
|
17
|
+
/* The claimed SSR node must begin with this literal, or the split misaligns — throw
|
|
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)
|
|
22
|
+
}
|
|
15
23
|
/* Split even on an exact-length consume when a sibling text binding follows
|
|
16
24
|
(`splitAlways`), so it gets its own node; see appendText for the rationale. */
|
|
17
25
|
if (
|
|
18
|
-
|
|
26
|
+
isText &&
|
|
19
27
|
(splitAlways ? value.length <= node.data.length : value.length < node.data.length)
|
|
20
28
|
) {
|
|
21
29
|
node.splitText(value.length)
|
|
@@ -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,20 +1,17 @@
|
|
|
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
|
-
import { scheduleWake } from '../runtime/scheduleWake.ts'
|
|
11
9
|
import { scope } from '../runtime/scope.ts'
|
|
12
10
|
import { scopeGroup } from '../runtime/scopeGroup.ts'
|
|
13
11
|
import type { State } from '../runtime/types/State.ts'
|
|
14
12
|
import { state } from '../state.ts'
|
|
15
13
|
import { buildDetachedRange } from './buildDetachedRange.ts'
|
|
16
14
|
import { discardBoundary } from './discardBoundary.ts'
|
|
17
|
-
import { firstElementBetween } from './firstElementBetween.ts'
|
|
18
15
|
import { removeRange } from './removeRange.ts'
|
|
19
16
|
|
|
20
17
|
/*
|
|
@@ -60,20 +57,11 @@ export function awaitBlock(
|
|
|
60
57
|
let active: { start: Comment; end: Comment; dispose: () => void } | undefined
|
|
61
58
|
let anchor: Node | undefined
|
|
62
59
|
let first = true
|
|
63
|
-
/* A deferred (inert-adopted) block schedules a wake (idle or visible); cancel it if the
|
|
64
|
-
owner tears the block out before it fires, so no late re-run touches a detached anchor. */
|
|
65
|
-
let cancelWake: (() => void) | undefined
|
|
66
60
|
/* Bumped each run so a prior run's in-flight promise can't clobber a newer one, AND on
|
|
67
61
|
owner teardown so an in-flight promise that settles AFTER the enclosing `{#if}`/
|
|
68
62
|
`{#for}`/component tears this block out is abandoned — otherwise its settle runs
|
|
69
|
-
`place` on the block's now-detached anchor and `insertBefore` throws NotFoundError.
|
|
70
|
-
|
|
71
|
-
const guard = generationGuard(() => cancelWake?.())
|
|
72
|
-
/* Flipped by a deferred block's wake. Read at the top of the effect so the block subscribes
|
|
73
|
-
to it and re-runs when it flips — the same re-run path an invalidate takes, so wake needs
|
|
74
|
-
no bespoke build logic. Never flips for a non-deferred block, so it costs one inert read
|
|
75
|
-
per run there. */
|
|
76
|
-
const woken = state(false)
|
|
63
|
+
`place` on the block's now-detached anchor and `insertBefore` throws NotFoundError. */
|
|
64
|
+
const guard = generationGuard()
|
|
77
65
|
/* The resolved value, held as a reactive cell so the then-branch reads it through its
|
|
78
66
|
own effects. A re-run that resolves to a NEW value SETS this cell instead of rebuilding
|
|
79
67
|
the branch — the branch (and any keyed `each` inside it) survives and updates in place,
|
|
@@ -192,53 +180,6 @@ export function awaitBlock(
|
|
|
192
180
|
active = { start, end, dispose }
|
|
193
181
|
}
|
|
194
182
|
|
|
195
|
-
/* Inert adoption — a deferred `{#await cache()}` (large payload, shipped as a `{defer,key}`
|
|
196
|
-
resume marker). Adopt the SSR then-branch WITHOUT building its bindings or materializing
|
|
197
|
-
the awaited value: the server markup is already correct, so keep it verbatim and skip the
|
|
198
|
-
payload decode entirely (the boot-path cost deferral targets). Only subscribe THIS block's
|
|
199
|
-
effect to the cache key — reads, decodes and fetches all wait. A later cache.invalidate
|
|
200
|
-
re-runs the effect, which then reads the value for real and builds a fresh branch. Sound
|
|
201
|
-
because a display-first read replaces the whole branch on re-read anyway, so there is no
|
|
202
|
-
extra flash beyond the swap the re-read already performs. */
|
|
203
|
-
const adoptInert = (open: Node | null, key: string): void => {
|
|
204
|
-
const cursor = hydration as NonNullable<typeof hydration>
|
|
205
|
-
const firstKept = open?.nextSibling ?? null
|
|
206
|
-
/* Scan to THIS block's close marker, leaving every server node in place. Nested
|
|
207
|
-
blocks carry different ids, so the first data match is our own close. */
|
|
208
|
-
let node: Node | null = firstKept
|
|
209
|
-
while (node !== null && (node as { data?: string }).data !== `/abide:await:${id}`) {
|
|
210
|
-
node = node.nextSibling
|
|
211
|
-
}
|
|
212
|
-
const close = node
|
|
213
|
-
cursor.next.set(parent, close?.nextSibling ?? null)
|
|
214
|
-
/* Bracket the kept nodes as a `[`…`]` range + park an anchor, identical to `adopt`,
|
|
215
|
-
so the first re-run's `place`/`detach` evicts them like any other branch. */
|
|
216
|
-
const start = document.createComment(RANGE_OPEN)
|
|
217
|
-
parent.insertBefore(start, firstKept ?? close)
|
|
218
|
-
const end = document.createComment(RANGE_CLOSE)
|
|
219
|
-
parent.insertBefore(end, close)
|
|
220
|
-
anchor = document.createTextNode('')
|
|
221
|
-
parent.insertBefore(anchor, close)
|
|
222
|
-
/* No scope/effects were built for the kept nodes, so disposal only evicts the range. */
|
|
223
|
-
active = { start, end, dispose: () => undefined }
|
|
224
|
-
/* Not 'then' — the first re-run rebuilds the branch fresh via `place`. */
|
|
225
|
-
activeKind = 'pending'
|
|
226
|
-
/* Subscribe without a value read: no clone, no decode, no fetch — just the key, so
|
|
227
|
-
cache.invalidate re-runs this effect (createSubscriber ties it to the running scope). */
|
|
228
|
-
activeCacheStore().subscribe(key)
|
|
229
|
-
/* Wake the inert branch — it's a boot-frame optimization, never a lasting mode. The
|
|
230
|
-
wake flips `woken`, re-running the effect through its normal `render` path: the
|
|
231
|
-
lazily-seeded cache reads warm, so the branch materializes live and interactive with
|
|
232
|
-
no fetch. Closes the "deferred grid stays inert until invalidate" gap.
|
|
233
|
-
|
|
234
|
-
Position picks the trigger ('auto'): a branch with an element wakes on VISIBLE (a
|
|
235
|
-
below-the-fold grid decodes only when scrolled to, one never reached costs nothing);
|
|
236
|
-
an empty branch or a DOM with no observer wakes on IDLE — never lingering inert. */
|
|
237
|
-
cancelWake = scheduleWake('auto', firstElementBetween(firstKept, close), () => {
|
|
238
|
-
woken.value = true
|
|
239
|
-
})
|
|
240
|
-
}
|
|
241
|
-
|
|
242
183
|
/* Discard the SSR boundary and (re)build the block from the live promise, fresh
|
|
243
184
|
(hydration off) — the recovery path when adoption can't use the server markup. */
|
|
244
185
|
const rebuildCold = (open: Node | null): void => {
|
|
@@ -275,28 +216,28 @@ export function awaitBlock(
|
|
|
275
216
|
lives. A decode failure (malformed/absent payload) reads as "no resume" — fall
|
|
276
217
|
through to the live promise rather than crash hydration. */
|
|
277
218
|
const raw = RESUME[id]
|
|
278
|
-
let decoded: ResumeEntry |
|
|
219
|
+
let decoded: ResumeEntry | undefined
|
|
279
220
|
if (raw !== undefined) {
|
|
280
221
|
try {
|
|
281
|
-
decoded = decodeRefJson(raw) as ResumeEntry
|
|
222
|
+
decoded = decodeRefJson(raw) as ResumeEntry
|
|
282
223
|
} catch {
|
|
283
224
|
decoded = undefined
|
|
284
225
|
}
|
|
285
226
|
}
|
|
286
|
-
/*
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
not throw past the decode try/catch and crash hydration. */
|
|
290
|
-
if (typeof decoded === 'object' && decoded !== null && 'defer' in decoded) {
|
|
291
|
-
adoptInert(open, decoded.key)
|
|
292
|
-
return
|
|
293
|
-
}
|
|
294
|
-
/* Non-deferred: read the promise now so the block subscribes to its reactive source
|
|
295
|
-
(a cache key) — warm on resume, so no round-trip — then adopt the resume value /
|
|
296
|
-
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. */
|
|
297
230
|
const result = promiseThunk()
|
|
298
|
-
const entry = decoded
|
|
231
|
+
const entry = decoded
|
|
299
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
|
+
}
|
|
300
241
|
/* Build the adopted branch around a value CELL (then) so a later re-run updates
|
|
301
242
|
it in place, exactly like a fresh mount. The `throw` for a catch-less rejection
|
|
302
243
|
stays OUTSIDE the adopt try/catch so it surfaces rather than triggering the
|
|
@@ -354,17 +295,12 @@ export function awaitBlock(
|
|
|
354
295
|
}
|
|
355
296
|
|
|
356
297
|
effect(() => {
|
|
357
|
-
/* Subscribe to the wake cell every run (including the first, before it can flip) so a
|
|
358
|
-
deferred block's idle wake re-runs this effect. Inert for non-deferred blocks. */
|
|
359
|
-
woken.value
|
|
360
298
|
guard.renew()
|
|
361
299
|
if (first) {
|
|
362
300
|
first = false
|
|
363
301
|
if (hydration !== undefined) {
|
|
364
|
-
/* firstHydrate reads the promise ITSELF
|
|
365
|
-
|
|
366
|
-
value we're deferring) and subscribes by key instead; every other path reads
|
|
367
|
-
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. */
|
|
368
304
|
firstHydrate()
|
|
369
305
|
return
|
|
370
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
|
}
|
|
@@ -29,19 +29,16 @@ export function mountChild(
|
|
|
29
29
|
props: Parameters<UiComponent>[1],
|
|
30
30
|
before: Node | null = null,
|
|
31
31
|
label?: string,
|
|
32
|
-
/* `client:idle` / `client:visible` on the tag → hydrate this child as an island (server
|
|
33
|
-
markup kept, build deferred to the trigger). Undefined = hydrate eagerly, as before. */
|
|
34
|
-
clientTrigger?: 'idle' | 'visible',
|
|
35
32
|
): void {
|
|
36
33
|
const moduleId = factory.__abideId
|
|
37
34
|
if (!__ABIDE_DEV__ || !hotReloadEnabled.current || moduleId === undefined) {
|
|
38
|
-
mountRange(parent, factory.build, props, before, label
|
|
35
|
+
mountRange(parent, factory.build, props, before, label)
|
|
39
36
|
return
|
|
40
37
|
}
|
|
41
38
|
/* Capture the component's model alongside its mount handle, so a later swap can
|
|
42
39
|
carry its state across (see `hotReplace`). */
|
|
43
40
|
const { value: handle, model } = captureModelDoc(() =>
|
|
44
|
-
mountRange(parent, factory.build, props, before, label
|
|
41
|
+
mountRange(parent, factory.build, props, before, label),
|
|
45
42
|
)
|
|
46
43
|
const instance = {
|
|
47
44
|
factory,
|
|
@@ -1,15 +1,9 @@
|
|
|
1
|
-
import { claimChild } from '../runtime/claimChild.ts'
|
|
2
1
|
import { RANGE_CLOSE, RANGE_OPEN } from '../runtime/RANGE_MARKER.ts'
|
|
3
2
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
4
|
-
import { scheduleWake } from '../runtime/scheduleWake.ts'
|
|
5
3
|
import { scope } from '../runtime/scope.ts'
|
|
6
4
|
import type { UiProps } from '../runtime/types/UiProps.ts'
|
|
7
|
-
import { clearBetween } from './clearBetween.ts'
|
|
8
|
-
import { commentData } from './commentData.ts'
|
|
9
5
|
import { disposeRange } from './disposeRange.ts'
|
|
10
6
|
import { fillRange } from './fillRange.ts'
|
|
11
|
-
import { firstElementBetween } from './firstElementBetween.ts'
|
|
12
|
-
import { markerDepthDelta } from './markerDepthDelta.ts'
|
|
13
7
|
import { openMarker } from './openMarker.ts'
|
|
14
8
|
import { withScope } from './withScope.ts'
|
|
15
9
|
|
|
@@ -28,12 +22,6 @@ marker — mirrors `mountSlot`/`when`). `before` (a skeleton-located node, the b
|
|
|
28
22
|
`anchorCursor`) places the range among static siblings on create; hydrate ignores it
|
|
29
23
|
(the claim cursor drives placement). Returns the markers + a disposer so the hot path
|
|
30
24
|
can rebuild in place.
|
|
31
|
-
|
|
32
|
-
`clientTrigger` marks the component an ISLAND: on hydrate its server markup is kept
|
|
33
|
-
verbatim and its build is SKIPPED, then run on the trigger (`'idle'`/`'visible'`) —
|
|
34
|
-
so a below-the-fold widget ships as HTML and wires no effects until scrolled to (or
|
|
35
|
-
the first idle gap), off the critical boot path. Create ignores it (there is no server
|
|
36
|
-
markup to keep — an island is a hydration-cost optimization). See `adoptIsland`.
|
|
37
25
|
*/
|
|
38
26
|
// @documentation plumbing
|
|
39
27
|
export function mountRange(
|
|
@@ -42,7 +30,6 @@ export function mountRange(
|
|
|
42
30
|
props: UiProps | undefined,
|
|
43
31
|
before: Node | null = null,
|
|
44
32
|
label: string | undefined = undefined,
|
|
45
|
-
clientTrigger: 'idle' | 'visible' | undefined = undefined,
|
|
46
33
|
): { start: Comment; end: Comment; dispose: () => void } {
|
|
47
34
|
const hydration = RENDER.hydration
|
|
48
35
|
const start = openMarker(parent, RANGE_OPEN, before)
|
|
@@ -50,9 +37,6 @@ export function mountRange(
|
|
|
50
37
|
const end = openMarker(parent, RANGE_CLOSE, before)
|
|
51
38
|
return fillRange(start, end, build, props, label)
|
|
52
39
|
}
|
|
53
|
-
if (clientTrigger !== undefined) {
|
|
54
|
-
return adoptIsland(parent, start, build, props, label, clientTrigger, hydration)
|
|
55
|
-
}
|
|
56
40
|
/* Hydrate: adopt the server range in place. Establish the child's lexical scope
|
|
57
41
|
and render pass (same as `fillRange`), build claiming the existing nodes, then
|
|
58
42
|
claim the end marker the build's content stops before. */
|
|
@@ -60,62 +44,3 @@ export function mountRange(
|
|
|
60
44
|
const end = openMarker(parent, RANGE_CLOSE)
|
|
61
45
|
return { start, end, dispose: disposeRange(scoped, start, end) }
|
|
62
46
|
}
|
|
63
|
-
|
|
64
|
-
/*
|
|
65
|
-
Island hydration: keep the server markup, skip the build, wake it later on the trigger. Scan the
|
|
66
|
-
child's server nodes to THIS range's depth-matched close (`markerDepthDelta` balances nested
|
|
67
|
-
`[`…`]` / `abide:` ranges) — keeping them and advancing the claim cursor past them, so the rest
|
|
68
|
-
of the page hydrates around the skipped subtree — then claim the close marker. No scope is built,
|
|
69
|
-
so the inert disposer only clears the kept nodes; the wake clears them and builds the child fresh
|
|
70
|
-
(hydration off) between the same markers, exactly like a create-path mount.
|
|
71
|
-
*/
|
|
72
|
-
function adoptIsland(
|
|
73
|
-
parent: Node,
|
|
74
|
-
start: Comment,
|
|
75
|
-
build: (host: Node, props?: UiProps) => void,
|
|
76
|
-
props: UiProps | undefined,
|
|
77
|
-
label: string | undefined,
|
|
78
|
-
trigger: 'idle' | 'visible',
|
|
79
|
-
hydration: NonNullable<(typeof RENDER)['hydration']>,
|
|
80
|
-
): { start: Comment; end: Comment; dispose: () => void } {
|
|
81
|
-
const firstKept = claimChild(hydration, parent)
|
|
82
|
-
let node = firstKept
|
|
83
|
-
let depth = 0
|
|
84
|
-
while (node !== null) {
|
|
85
|
-
const data = commentData(node)
|
|
86
|
-
if (data !== undefined) {
|
|
87
|
-
const delta = markerDepthDelta(data)
|
|
88
|
-
/* A depth-0 close is THIS range's own; anything nested balances out first. */
|
|
89
|
-
if (delta === -1 && depth === 0) {
|
|
90
|
-
break
|
|
91
|
-
}
|
|
92
|
-
depth += delta
|
|
93
|
-
}
|
|
94
|
-
node = node.nextSibling
|
|
95
|
-
}
|
|
96
|
-
hydration.next.set(parent, node)
|
|
97
|
-
const end = openMarker(parent, RANGE_CLOSE)
|
|
98
|
-
|
|
99
|
-
/* Inert: no scope/effects for the kept nodes, so disposal only evicts the range. The wake
|
|
100
|
-
swaps in a real build whose own disposer replaces this one. */
|
|
101
|
-
let disposeContent = (): void => clearBetween(start, end)
|
|
102
|
-
const wake = (): void => {
|
|
103
|
-
disposeContent()
|
|
104
|
-
const previous = RENDER.hydration
|
|
105
|
-
RENDER.hydration = undefined
|
|
106
|
-
try {
|
|
107
|
-
disposeContent = fillRange(start, end, build, props, label).dispose
|
|
108
|
-
} finally {
|
|
109
|
-
RENDER.hydration = previous
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
const cancelWake = scheduleWake(trigger, firstElementBetween(firstKept, end), wake)
|
|
113
|
-
return {
|
|
114
|
-
start,
|
|
115
|
-
end,
|
|
116
|
-
dispose: () => {
|
|
117
|
-
cancelWake()
|
|
118
|
-
disposeContent()
|
|
119
|
-
},
|
|
120
|
-
}
|
|
121
|
-
}
|
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 {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { snippet } from '../shared/snippet.ts'
|
|
2
|
-
import { deferResume } from './deferResume.ts'
|
|
3
2
|
import { anchorCursor } from './dom/anchorCursor.ts'
|
|
4
3
|
import { appendSnippet } from './dom/appendSnippet.ts'
|
|
5
4
|
import { appendStatic } from './dom/appendStatic.ts'
|
|
@@ -8,6 +7,7 @@ import { appendTextAt } from './dom/appendTextAt.ts'
|
|
|
8
7
|
import { attach } from './dom/attach.ts'
|
|
9
8
|
import { attr } from './dom/attr.ts'
|
|
10
9
|
import { awaitBlock } from './dom/awaitBlock.ts'
|
|
10
|
+
import { bindSelectValue } from './dom/bindSelectValue.ts'
|
|
11
11
|
import { cloneStatic } from './dom/cloneStatic.ts'
|
|
12
12
|
import { each } from './dom/each.ts'
|
|
13
13
|
import { eachAsync } from './dom/eachAsync.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) })
|