@abide/abide 0.46.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 +80 -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/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 +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 +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/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/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 -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
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { cache } from '../shared/cache.ts'
|
|
2
|
+
import { REMOTE_FUNCTION } from '../shared/REMOTE_FUNCTION.ts'
|
|
3
|
+
import type { CacheOnContext } from '../shared/types/CacheOnContext.ts'
|
|
4
|
+
import type { RemoteFunction } from '../shared/types/RemoteFunction.ts'
|
|
5
|
+
import type { Subscribable } from '../shared/types/Subscribable.ts'
|
|
6
|
+
import { effect } from './effect.ts'
|
|
7
|
+
import { generationGuard } from './runtime/generationGuard.ts'
|
|
8
|
+
import type { EffectResult } from './runtime/types/EffectResult.ts'
|
|
9
|
+
import type { State } from './runtime/types/State.ts'
|
|
10
|
+
|
|
11
|
+
/*
|
|
12
|
+
The single reaction primitive: `watch(source, handler)` names its trigger and runs
|
|
13
|
+
`handler` with the source's new value whenever it changes. It unifies three
|
|
14
|
+
previously-separate things — the author `effect`, `socket.on`, and `cache.on` —
|
|
15
|
+
and it is also the compiler's binding primitive (emitted as `$$watch(thunk)` for
|
|
16
|
+
`{expr}` / `class:` / `bind:*`). Client-only: SSR-inert here and stripped by the
|
|
17
|
+
compiler. Returns a scope-tied disposer.
|
|
18
|
+
|
|
19
|
+
Instance sugar mirrors the global for the two subscribable/rpc sources (the other
|
|
20
|
+
sources have no instance home): `socket.watch(handler)` ≡ `watch(socket, handler)`, and
|
|
21
|
+
`getUser.watch(handler)` / `getUser.watch(args, handler)` ≡ `watch(getUser, …)`. The
|
|
22
|
+
method is client-attached (socketProxy / remoteProxy) so this ui primitive never rides
|
|
23
|
+
into a server bundle; server-side it is an inert no-op. Unlike bare `watch(…)` — which
|
|
24
|
+
the SSR back-end strips — a `.watch(…)` member call survives to the server and relies on
|
|
25
|
+
that inert stub.
|
|
26
|
+
|
|
27
|
+
Sources (discriminated at runtime, monomorphic per branch):
|
|
28
|
+
watch(thunk) // compiler binding form — auto-tracked, == effect(thunk)
|
|
29
|
+
watch(count, n => …) // a state cell → handler(newValue)
|
|
30
|
+
watch([a, b], vals => …) // multiple cells → fires on any change
|
|
31
|
+
watch(socket, frame => …) // a subscribable → handler per frame (cache.on loop)
|
|
32
|
+
watch(getUser, user => …) // an rpc → runs the smart read, handler(resolved value)
|
|
33
|
+
watch(getUser, args, user => …) // an rpc with args
|
|
34
|
+
*/
|
|
35
|
+
// @documentation reactive-state
|
|
36
|
+
export function watch(thunk: () => EffectResult): () => void
|
|
37
|
+
export function watch<T>(source: State<T>, handler: (value: T) => void): () => void
|
|
38
|
+
export function watch(
|
|
39
|
+
sources: ReadonlyArray<State<unknown>>,
|
|
40
|
+
handler: (values: unknown[]) => void,
|
|
41
|
+
): () => void
|
|
42
|
+
export function watch<Frame>(
|
|
43
|
+
source: Subscribable<Frame>,
|
|
44
|
+
handler: (frame: Frame, context: CacheOnContext) => void | Promise<void>,
|
|
45
|
+
): () => void
|
|
46
|
+
export function watch<Args, Return>(
|
|
47
|
+
fn: RemoteFunction<Args, Return>,
|
|
48
|
+
handler: (value: Return) => void,
|
|
49
|
+
): () => void
|
|
50
|
+
export function watch<Args, Return>(
|
|
51
|
+
fn: RemoteFunction<Args, Return>,
|
|
52
|
+
args: Args,
|
|
53
|
+
handler: (value: Return) => void,
|
|
54
|
+
): () => void
|
|
55
|
+
export function watch(
|
|
56
|
+
source: unknown,
|
|
57
|
+
argsOrHandler?: unknown,
|
|
58
|
+
maybeHandler?: unknown,
|
|
59
|
+
): () => void {
|
|
60
|
+
/* Client lifecycle, exactly like the effect it wraps: no runtime window guard (that would
|
|
61
|
+
also silence the compiler's generated bindings, which build on the client where the test
|
|
62
|
+
DOM sets no global `window`). SSR-safety comes from the compiler stripping author
|
|
63
|
+
`watch(...)` calls, and the subscribable branch's own server guard (cache.on). */
|
|
64
|
+
/* Compiler binding form watch(thunk): a bare auto-tracked effect (== today's $$effect). */
|
|
65
|
+
if (argsOrHandler === undefined) {
|
|
66
|
+
return effect(source as () => EffectResult)
|
|
67
|
+
}
|
|
68
|
+
/* watch(fn, args, handler): an rpc selector with explicit args. */
|
|
69
|
+
if (maybeHandler !== undefined) {
|
|
70
|
+
return reactToRpc(source, argsOrHandler, maybeHandler as (value: unknown) => void)
|
|
71
|
+
}
|
|
72
|
+
const handler = argsOrHandler as (value: unknown, context?: CacheOnContext) => void
|
|
73
|
+
/* A subscribable (socket / stream): per-frame delivery with reconnect-replay — the
|
|
74
|
+
existing cache.on loop is the single implementation this branch delegates to. */
|
|
75
|
+
if (isSubscribable(source)) {
|
|
76
|
+
return cache.on(
|
|
77
|
+
source as Subscribable<unknown>,
|
|
78
|
+
handler as (frame: unknown, context: CacheOnContext) => void | Promise<void>,
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
/* An rpc without args → run the smart read reactively, pipe its value to the handler. A
|
|
82
|
+
RemoteFunction is a callable, so accept `function` too — a state cell is a branded-free
|
|
83
|
+
object and never matches, and a streaming rpc was already caught by isSubscribable above. */
|
|
84
|
+
if (
|
|
85
|
+
source !== null &&
|
|
86
|
+
(typeof source === 'object' || typeof source === 'function') &&
|
|
87
|
+
REMOTE_FUNCTION in source
|
|
88
|
+
) {
|
|
89
|
+
return reactToRpc(source, undefined, handler)
|
|
90
|
+
}
|
|
91
|
+
/* Multiple cells → fire on any change; hand the handler the current values. */
|
|
92
|
+
if (Array.isArray(source)) {
|
|
93
|
+
const cells = source as ReadonlyArray<State<unknown>>
|
|
94
|
+
return effect(() => {
|
|
95
|
+
const values = cells.map((cell) => cell.value)
|
|
96
|
+
handler(values)
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
/* A single state cell → handler(newValue) on change. */
|
|
100
|
+
const cell = source as State<unknown>
|
|
101
|
+
return effect(() => {
|
|
102
|
+
handler(cell.value)
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* True for a socket / rpc stream — anything async-iterable. */
|
|
107
|
+
function isSubscribable(source: unknown): boolean {
|
|
108
|
+
return (
|
|
109
|
+
source !== null &&
|
|
110
|
+
(typeof source === 'object' || typeof source === 'function') &&
|
|
111
|
+
typeof (source as { [Symbol.asyncIterator]?: unknown })[Symbol.asyncIterator] === 'function'
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/*
|
|
116
|
+
Reacts to an rpc's cached value: an effect that runs the smart read (subscribing
|
|
117
|
+
its key synchronously, so the effect re-runs when the value changes — a refresh,
|
|
118
|
+
a patch, an invalidate) and pipes the resolved value to the handler. Triggers the
|
|
119
|
+
read, unlike peek: `watch` observes a live query, so it keeps it flowing.
|
|
120
|
+
*/
|
|
121
|
+
function reactToRpc(fn: unknown, args: unknown, handler: (value: unknown) => void): () => void {
|
|
122
|
+
const call = fn as (args: unknown) => Promise<unknown>
|
|
123
|
+
/* The bare call routes through cache.read (cache-managed flight, not scope-abortable), so
|
|
124
|
+
a slow flight can settle AFTER a faster re-run's flight OR after the owner tears down.
|
|
125
|
+
Guard the handler on the generation so only the current flight's value lands and a
|
|
126
|
+
post-teardown settle is dropped — a re-run renews, teardown bumps (both via the shared
|
|
127
|
+
generationGuard). */
|
|
128
|
+
const guard = generationGuard()
|
|
129
|
+
return effect(() => {
|
|
130
|
+
const generation = guard.renew()
|
|
131
|
+
void Promise.resolve(call(args)).then(
|
|
132
|
+
(value) => {
|
|
133
|
+
if (guard.live(generation)) {
|
|
134
|
+
handler(value)
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
() => undefined,
|
|
138
|
+
)
|
|
139
|
+
})
|
|
140
|
+
}
|
package/src/serverEntry.ts
CHANGED
|
@@ -29,12 +29,12 @@ import { loadEnvFromBinaryDir } from './lib/cli/loadEnvFromBinaryDir.ts'
|
|
|
29
29
|
import { createServer } from './lib/server/runtime/createServer.ts'
|
|
30
30
|
import { requestContext } from './lib/server/runtime/requestContext.ts'
|
|
31
31
|
import { resolvePageSnapshot } from './lib/server/runtime/resolvePageSnapshot.ts'
|
|
32
|
+
import { cacheStoreSlot } from './lib/shared/cacheStoreSlot.ts'
|
|
32
33
|
import { createCacheStore } from './lib/shared/createCacheStore.ts'
|
|
34
|
+
import { globalCacheStoreSlot } from './lib/shared/globalCacheStoreSlot.ts'
|
|
33
35
|
import { loadEnvFromDataDir } from './lib/shared/loadEnvFromDataDir.ts'
|
|
36
|
+
import { pageSlot } from './lib/shared/pageSlot.ts'
|
|
34
37
|
import { runningAsStandaloneBinary } from './lib/shared/runningAsStandaloneBinary.ts'
|
|
35
|
-
import { setCacheStoreResolver } from './lib/shared/setCacheStoreResolver.ts'
|
|
36
|
-
import { setGlobalCacheStoreResolver } from './lib/shared/setGlobalCacheStoreResolver.ts'
|
|
37
|
-
import { setPageResolver } from './lib/shared/setPageResolver.ts'
|
|
38
38
|
|
|
39
39
|
/*
|
|
40
40
|
Resolve config into process.env before anything reads it (createServer reads
|
|
@@ -65,16 +65,16 @@ await import('./_virtual/config.ts')
|
|
|
65
65
|
// In a bundle, tie this server's life to the launcher's (no-op standalone).
|
|
66
66
|
exitWithParent()
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
cacheStoreSlot.resolver = () => requestContext.getStore()?.cache
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
pageSlot.resolver = resolvePageSnapshot
|
|
71
71
|
|
|
72
72
|
/*
|
|
73
73
|
Process-level store for cache(fn, { global: true }) — one per server process,
|
|
74
74
|
outlives every request so memoised external calls are shared across them.
|
|
75
75
|
*/
|
|
76
76
|
const globalCacheStore = createCacheStore()
|
|
77
|
-
|
|
77
|
+
globalCacheStoreSlot.resolver = () => globalCacheStore
|
|
78
78
|
|
|
79
79
|
await createServer({
|
|
80
80
|
pages,
|
package/template/CLAUDE.md
CHANGED
|
@@ -14,7 +14,7 @@ at `node_modules/@abide/abide/README.md`.
|
|
|
14
14
|
## Conventions (see AGENTS.md for the full list)
|
|
15
15
|
|
|
16
16
|
- One export per file, named after the file. No barrels — import each name by its
|
|
17
|
-
own path (`@abide/abide/server/GET`, `@abide/abide/shared/
|
|
17
|
+
own path (`@abide/abide/server/GET`, `@abide/abide/shared/refresh`, …).
|
|
18
18
|
- RPCs live in `src/server/rpc/<name>.ts`; sockets in `src/server/sockets/`;
|
|
19
19
|
pages are `**/page.abide`, layouts `**/layout.abide`.
|
|
20
20
|
- Generated types land in `src/.abide/` — do not hand-edit them; run `abide dev`/
|
package/template/package.json
CHANGED
|
@@ -25,9 +25,9 @@ dedupe). Other helpers are siblings, one per file: `abide/server/error`,
|
|
|
25
25
|
`abide/server/redirect`, `abide/server/sse`, `abide/server/jsonl`.
|
|
26
26
|
|
|
27
27
|
Every rpc value also exposes `.raw(args?)` (returns the underlying
|
|
28
|
-
`Response`)
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
`Response`) for callers that need headers or status. A streaming rpc — a
|
|
29
|
+
jsonl/sse handler — returns a `Subscribable` over the frames from its bare
|
|
30
|
+
call (`fn(args)`); iterate it with `for await` or react to it with `watch(fn(args), frame => …)`.
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
33
|
import { GET } from '@abide/abide/server/GET'
|
|
@@ -5,15 +5,15 @@ page.abide mounts at that folder's URL; the root layout.abide wraps it.
|
|
|
5
5
|
|
|
6
6
|
The blocking await-block below (the `then` clause sits in the `{#await}` head)
|
|
7
7
|
resolves on the server during SSR and renders inline — no pending placeholder. The
|
|
8
|
+
bare rpc call IS the smart read: cached, coalesced, and reactive by default. The
|
|
8
9
|
decoded body is captured into the per-request cache, serialized into the HTML, and
|
|
9
10
|
replayed on the client during hydration with no second fetch. A `{:then}` *branch*
|
|
10
11
|
instead would stream the resolution in out of order.
|
|
11
12
|
*/
|
|
12
|
-
import { cache } from '@abide/abide/shared/cache'
|
|
13
13
|
import { getHello } from '$server/rpc/getHello.ts'
|
|
14
14
|
</script>
|
|
15
15
|
|
|
16
|
-
{#await
|
|
16
|
+
{#await getHello() then hello}
|
|
17
17
|
<h1>{hello.message}</h1>
|
|
18
18
|
{/await}
|
|
19
19
|
<p>Edit <code>src/ui/pages/page.abide</code> and the page hot-reloads.</p>
|
|
@@ -5,7 +5,7 @@ same wiring as `abide start`, no fixtures) and hands back the whole surface:
|
|
|
5
5
|
app.fetch(path) — pages and raw HTTP, origin + mount base resolved
|
|
6
6
|
app.rpc.<name>(args) — RPCs over the real pipeline (CSRF, cookies, base),
|
|
7
7
|
typed and decoded; .raw(args) for the Response
|
|
8
|
-
app.sockets.<name> — a live Socket: iterate it, .
|
|
8
|
+
app.sockets.<name> — a live Socket: iterate it (for await), .peek(), .broadcast(m)
|
|
9
9
|
app.health() — the /__abide/health payload
|
|
10
10
|
|
|
11
11
|
`app.rpc` / `app.sockets` are typed from your own RPCs and sockets — generated
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { parseBoundedEnvInt } from '../shared/parseBoundedEnvInt.ts'
|
|
2
|
-
import { createReachable } from './runtime/createReachable.ts'
|
|
3
|
-
|
|
4
|
-
/*
|
|
5
|
-
Server-only outbound reachability for an external host. `await reachable(host)`
|
|
6
|
-
HEADs the host's origin: the first call awaits a real probe (faithful — a down
|
|
7
|
-
host costs the full timeout, an up host one handshake) and starts a background
|
|
8
|
-
poll that re-probes every TTL, so every later call resolves instantly off the
|
|
9
|
-
warm value, fresh within one TTL. A down host going down is caught within
|
|
10
|
-
~failureLimit polls; recovery flips it back automatically.
|
|
11
|
-
|
|
12
|
-
if (!(await reachable('api.example.com'))) return error(503)
|
|
13
|
-
|
|
14
|
-
A bare host defaults to https; pass an explicit http://… for a non-TLS host.
|
|
15
|
-
Answers "can I connect to this host," NOT "is my endpoint healthy": any
|
|
16
|
-
completed HTTP response (even 4xx/5xx, even a 405 to HEAD) counts as reachable;
|
|
17
|
-
only a connection failure or timeout reads as unreachable. There is no ambient
|
|
18
|
-
server-side connectivity signal, so this is the honest way to fail a doomed
|
|
19
|
-
outbound call fast — see online() for the inbound/client-reported counterpart.
|
|
20
|
-
|
|
21
|
-
ABIDE_REACHABLE_TTL (poll cadence / freshness, ms) and ABIDE_REACHABLE_TIMEOUT
|
|
22
|
-
(per-HEAD bound, ms) tune the defaults; the timeout is deliberately generous so
|
|
23
|
-
a healthy-but-distant host over a slow link is not mis-read as down.
|
|
24
|
-
*/
|
|
25
|
-
const TTL_MS = parseBoundedEnvInt(process.env.ABIDE_REACHABLE_TTL, 1_000, 600_000) ?? 30_000
|
|
26
|
-
const TIMEOUT_MS = parseBoundedEnvInt(process.env.ABIDE_REACHABLE_TIMEOUT, 100, 60_000) ?? 3_000
|
|
27
|
-
/* Stop polling a host nobody has read in a few TTLs; the next read restarts it cold. */
|
|
28
|
-
const IDLE_MS = TTL_MS * 3
|
|
29
|
-
|
|
30
|
-
/* Status-agnostic HEAD: a completed response proves connectivity; reject/timeout does not. */
|
|
31
|
-
async function probeOrigin(origin: string): Promise<boolean> {
|
|
32
|
-
try {
|
|
33
|
-
await fetch(origin, { method: 'HEAD', signal: AbortSignal.timeout(TIMEOUT_MS) })
|
|
34
|
-
return true
|
|
35
|
-
} catch {
|
|
36
|
-
return false
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// @documentation observability
|
|
41
|
-
export const reachable = createReachable({
|
|
42
|
-
probe: probeOrigin,
|
|
43
|
-
intervalMs: TTL_MS,
|
|
44
|
-
idleMs: IDLE_MS,
|
|
45
|
-
}).reachable
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import type { ClientFlags } from '../../../shared/types/ClientFlags.ts'
|
|
2
|
-
import type { TailHooks } from '../../../shared/types/TailHooks.ts'
|
|
3
|
-
|
|
4
|
-
/*
|
|
5
|
-
Bidirectional named broadcast primitive. Declared once with `socket<T>()`
|
|
6
|
-
inside a file under `src/server/sockets/`; the same import resolves to a server-side
|
|
7
|
-
fan-out and a client-side ws proxy by build target. Iterating the socket
|
|
8
|
-
is the live stream — no replay. `.tail(count)` opens a subscription seeded
|
|
9
|
-
with the last `count` frames of the retained tail (declared via
|
|
10
|
-
`{ tail: n }`; no-arg = the whole retained tail) before going live — the
|
|
11
|
-
optional Subscribable retention capability the reactive `tail()` consumer
|
|
12
|
-
seeds from. `publish` is isomorphic: server code publishes in-process
|
|
13
|
-
and fans out to remote subscribers; client code sends a `pub` frame the
|
|
14
|
-
dispatcher validates against the topic's `clientPublish` flag. `clients`
|
|
15
|
-
exposes which adapter surfaces (browser / mcp / cli) advertise this
|
|
16
|
-
socket.
|
|
17
|
-
*/
|
|
18
|
-
export interface Socket<T> extends AsyncIterable<T> {
|
|
19
|
-
readonly name: string
|
|
20
|
-
readonly clients: ClientFlags
|
|
21
|
-
publish(message: T): void
|
|
22
|
-
tail(count?: number, hooks?: TailHooks): AsyncIterable<T>
|
|
23
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
Brand on the invoker cache() returns; its property value is the wrapped fn,
|
|
3
|
-
so inspection shows what a wrapper wraps. Detection is certain (set by us,
|
|
4
|
-
never heuristic), which is why misuse throws instead of warning: a wrapper
|
|
5
|
-
used as a selector matches nothing (no url/method, no producer id), and
|
|
6
|
-
without detection a re-wrapped wrapper would silently downgrade a remote to
|
|
7
|
-
an anonymous producer (no url/method, no shared key, no SSR snapshot).
|
|
8
|
-
*/
|
|
9
|
-
export const CACHE_WRAPPED: unique symbol = Symbol('abide.cacheWrapped')
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
The array-length above which a blocking `{#await cache()}` value is worth deferring (shipped
|
|
3
|
-
inert, materialized on idle) rather than inlined and hydrated eagerly at boot. Deferral trades
|
|
4
|
-
a live boot for a cheaper one; below this many rows the boot decode is cheap enough that the
|
|
5
|
-
trade doesn't pay, so the block stays eager — fully interactive from the first frame, no wake
|
|
6
|
-
needed. Only genuinely large grids cross it and take the inert-then-idle-wake path.
|
|
7
|
-
|
|
8
|
-
An O(1) `.length` heuristic (measured on the resolved value, before serialization): array-shaped
|
|
9
|
-
cache reads — lists, grids, tables — are the payloads big enough to matter, and the ones the
|
|
10
|
-
inert path was built for. Non-array values never defer. A deliberately generous default: with
|
|
11
|
-
idle-wake, a deferred block is interactive anyway (live before a human acts), so this is a
|
|
12
|
-
boot-performance knob, not a correctness one — set high so eager-and-simple is the common case.
|
|
13
|
-
*/
|
|
14
|
-
export const DEFER_MIN_ARRAY_LENGTH = 500
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { createResolverSlot } from './createResolverSlot.ts'
|
|
2
|
-
|
|
3
|
-
/*
|
|
4
|
-
The mount-base slot/resolver/reader bundle. The server entry installs an
|
|
5
|
-
APP_URL-derived resolver at boot; the client entry one reading
|
|
6
|
-
window.__SSR__.base. No lazy fallback creator — `fallback` is a plain string set
|
|
7
|
-
directly by isolated tests, and basePath() supplies the '' default. baseSlot /
|
|
8
|
-
basePath re-export the slot and reader; setBaseResolver the setter.
|
|
9
|
-
*/
|
|
10
|
-
export const baseResolver = createResolverSlot<string>()
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { cacheKeyStore } from './cacheKeyStore.ts'
|
|
2
|
-
|
|
3
|
-
/* The store key behind a cache() read's promise, or undefined if it carries none
|
|
4
|
-
(a producer read, a raw Response read, or a non-cache promise). */
|
|
5
|
-
export function cacheKeyOf(promise: Promise<unknown>): string | undefined {
|
|
6
|
-
return cacheKeyStore.get(promise)
|
|
7
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
WeakMap recording the cache key behind a `cache()` read's returned promise, so a
|
|
3
|
-
consumer holding only the promise can recover its key. Mirrors remoteMetaStore (which
|
|
4
|
-
records the synthesized Request the same way). The SSR resume path uses it to decide
|
|
5
|
-
whether a `{#await cache()}` value is a large cache-backed read it can defer — shipping a
|
|
6
|
-
`{ defer, key }` marker instead of the value. Collected with the promise.
|
|
7
|
-
*/
|
|
8
|
-
export const cacheKeyStore = new WeakMap<Promise<unknown>, string>()
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { createCacheStore } from './createCacheStore.ts'
|
|
2
|
-
import { createResolverSlot } from './createResolverSlot.ts'
|
|
3
|
-
import type { CacheStore } from './types/CacheStore.ts'
|
|
4
|
-
|
|
5
|
-
/*
|
|
6
|
-
The active-CacheStore slot/resolver/reader bundle. The server entry installs an
|
|
7
|
-
ALS-backed resolver (request-scoped); the client entry a module-singleton one.
|
|
8
|
-
With no resolver registered, a single fallback store is created lazily so
|
|
9
|
-
isolated tests work without booting the runtime. cacheStoreSlot / activeCacheStore
|
|
10
|
-
re-export the slot and reader; setCacheStoreResolver the setter.
|
|
11
|
-
*/
|
|
12
|
-
export const cacheStoreResolver = createResolverSlot<CacheStore>(createCacheStore)
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { createResolverSlot } from './createResolverSlot.ts'
|
|
2
|
-
import type { CacheStore } from './types/CacheStore.ts'
|
|
3
|
-
|
|
4
|
-
/*
|
|
5
|
-
Slot + setter for the process-level cache store resolver used by cache() entries
|
|
6
|
-
opting into `global: true`. The server entry registers a module-singleton store
|
|
7
|
-
outliving any one request; the client entry points it at its single tab store so
|
|
8
|
-
`global` is a no-op there. No fallback creator — when unset, globalCacheStore()
|
|
9
|
-
falls through to the active store rather than minting an isolated one.
|
|
10
|
-
globalCacheStoreSlot / setGlobalCacheStoreResolver re-export the slot and setter.
|
|
11
|
-
*/
|
|
12
|
-
export const globalCacheStoreResolver = createResolverSlot<CacheStore>()
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { createResolverSlot } from './createResolverSlot.ts'
|
|
2
|
-
import type { PageSnapshot } from './types/PageSnapshot.ts'
|
|
3
|
-
|
|
4
|
-
/*
|
|
5
|
-
The active-page slot/resolver/reader bundle. The server entry installs an
|
|
6
|
-
ALS-backed resolver (request-scoped, so concurrent and streaming renders never
|
|
7
|
-
share state); the client entry a module-singleton one. With no resolver
|
|
8
|
-
registered, a single empty snapshot is created lazily so isolated tests work.
|
|
9
|
-
pageSlot / activePage re-export the slot and reader; setPageResolver the setter.
|
|
10
|
-
*/
|
|
11
|
-
export const pageResolver = createResolverSlot<PageSnapshot>(() => ({
|
|
12
|
-
route: '',
|
|
13
|
-
params: {},
|
|
14
|
-
url: new URL('http://localhost/'),
|
|
15
|
-
navigating: false,
|
|
16
|
-
}))
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { createResolverSlot } from './createResolverSlot.ts'
|
|
2
|
-
import type { RequestScopeInfo } from './types/RequestScopeInfo.ts'
|
|
3
|
-
|
|
4
|
-
/*
|
|
5
|
-
Slot + setter for the request-scope resolver. The server installs an ALS-backed
|
|
6
|
-
resolver (createServer, reading the RequestStore); the client a module-singleton
|
|
7
|
-
seeded from __SSR__ (startClient). No fallback creator — an unset resolver (or one
|
|
8
|
-
returning undefined outside any request) means "no scope": callers read
|
|
9
|
-
`.resolver?.()` and treat undefined as absent. requestScopeSlot /
|
|
10
|
-
setRequestScopeResolver re-export the slot and setter.
|
|
11
|
-
*/
|
|
12
|
-
export const requestScopeResolver = createResolverSlot<RequestScopeInfo>()
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { parseRouteSegments } from './parseRouteSegments.ts'
|
|
2
|
-
|
|
3
|
-
/*
|
|
4
|
-
Translates a abide route URL (`/media/[id]/[...rest]`) into the pattern Bun
|
|
5
|
-
needs (`/media/:id/*`) for `Bun.serve({ routes })`. Returns the catch-all
|
|
6
|
-
segment's original name alongside so the server can rename Bun's `*` param
|
|
7
|
-
back to that name on the way out, keeping page-prop destructuring consistent
|
|
8
|
-
with the route file path.
|
|
9
|
-
*/
|
|
10
|
-
export function toBunRoutePattern(routeUrl: string): {
|
|
11
|
-
pattern: string
|
|
12
|
-
catchAllName: string | undefined
|
|
13
|
-
} {
|
|
14
|
-
let catchAllName: string | undefined
|
|
15
|
-
const pattern = parseRouteSegments(routeUrl)
|
|
16
|
-
.map((segment) => {
|
|
17
|
-
if (segment.kind === 'literal') {
|
|
18
|
-
return segment.value
|
|
19
|
-
}
|
|
20
|
-
if (segment.catchAll) {
|
|
21
|
-
catchAllName = segment.name
|
|
22
|
-
return '*'
|
|
23
|
-
}
|
|
24
|
-
return `:${segment.name}`
|
|
25
|
-
})
|
|
26
|
-
.join('/')
|
|
27
|
-
return { pattern, catchAllName }
|
|
28
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
Window form of `tail()`: `last` is the rolling window size — the reader holds
|
|
3
|
-
the last ≤`last` frames, however they arrived. A source that retains a tail
|
|
4
|
-
(a socket declared with `{ tail: n }`) seeds the window by replaying up to
|
|
5
|
-
`last` retained frames; a source with no retention (an rpc stream, an
|
|
6
|
-
undeclared socket) fills it from live frames only. Integer ≥ 1.
|
|
7
|
-
*/
|
|
8
|
-
export interface TailOptions {
|
|
9
|
-
readonly last: number
|
|
10
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { activeCacheStore } from '../shared/activeCacheStore.ts'
|
|
2
|
-
import { cacheKeyOf } from '../shared/cacheKeyOf.ts'
|
|
3
|
-
import { DEFER_MIN_ARRAY_LENGTH } from '../shared/DEFER_MIN_ARRAY_LENGTH.ts'
|
|
4
|
-
import { snapshotShippable } from '../shared/snapshotShippable.ts'
|
|
5
|
-
import type { DeferMarker, ResumeEntry } from './runtime/RESUME.ts'
|
|
6
|
-
|
|
7
|
-
/*
|
|
8
|
-
The resume-manifest entry for a blocking `{#await expr then value}`. When the awaited value
|
|
9
|
-
is a shippable cache-backed read, DEFER it: flag the store entry so its SSR snapshot seeds
|
|
10
|
-
lazily (the body is decoded on first read, not at hydration) and return a `{ defer, key }`
|
|
11
|
-
marker in place of the value. Hydration then adopts the server branch inert and decodes
|
|
12
|
-
neither copy — the blocking form's contract: "render it on the server, keep the page, refetch
|
|
13
|
-
only when I read or invalidate it." A non-cache value (a plain promise, a computation) or a
|
|
14
|
-
cache read whose entry can't ship inlines its value as before. Server-only in practice — the
|
|
15
|
-
client codegen never calls it. */
|
|
16
|
-
// @documentation plumbing
|
|
17
|
-
export function deferResume(promise: unknown, value: unknown): ResumeEntry | DeferMarker {
|
|
18
|
-
const isPromise =
|
|
19
|
-
promise !== null && typeof (promise as { then?: unknown })?.then === 'function'
|
|
20
|
-
const key = isPromise ? cacheKeyOf(promise as Promise<unknown>) : undefined
|
|
21
|
-
if (key === undefined) {
|
|
22
|
-
return { ok: true, value }
|
|
23
|
-
}
|
|
24
|
-
const entry = activeCacheStore().entries.get(key)
|
|
25
|
-
if (entry === undefined || !snapshotShippable(entry)) {
|
|
26
|
-
return { ok: true, value }
|
|
27
|
-
}
|
|
28
|
-
/* Only defer a genuinely large grid: below the threshold, inline the value and hydrate
|
|
29
|
-
eagerly — the block is interactive from the first frame, no inert phase, no wake. This is
|
|
30
|
-
what keeps a modest searchable list live instead of frozen-until-idle (the reported gap). */
|
|
31
|
-
if (!Array.isArray(value) || value.length < DEFER_MIN_ARRAY_LENGTH) {
|
|
32
|
-
return { ok: true, value }
|
|
33
|
-
}
|
|
34
|
-
entry.deferred = true
|
|
35
|
-
return { defer: true, key }
|
|
36
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { isElement } from './isElement.ts'
|
|
2
|
-
|
|
3
|
-
/* The first Element in the sibling run `[start, end)` — the node a visible-wake observes for a
|
|
4
|
-
deferred region (await branch or component island). Undefined when the run holds no element
|
|
5
|
-
(text/comment only), so the caller falls back to an idle wake. Element detection is
|
|
6
|
-
method-based (`isElement`), not `nodeType`, so the walk runs under the test mini-dom too. */
|
|
7
|
-
export function firstElementBetween(start: Node | null, end: Node | null): Element | undefined {
|
|
8
|
-
for (let node = start; node !== null && node !== end; node = node.nextSibling) {
|
|
9
|
-
if (isElement(node)) {
|
|
10
|
-
return node
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
return undefined
|
|
14
|
-
}
|