@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
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { CACHE_WRAPPED } from './CACHE_WRAPPED.ts'
|
|
2
1
|
import { keyForRemoteCall } from './keyForRemoteCall.ts'
|
|
2
|
+
import { keyPrefixForRemote } from './keyPrefixForRemote.ts'
|
|
3
3
|
import { producerKey } from './producerKey.ts'
|
|
4
4
|
import { REMOTE_FUNCTION } from './REMOTE_FUNCTION.ts'
|
|
5
5
|
import type { CacheSelector } from './types/CacheSelector.ts'
|
|
@@ -13,9 +13,7 @@ one call instead — derived through the same encoders the read path uses
|
|
|
13
13
|
(keyForRemoteCall / producerKey format), so selector and entry can't
|
|
14
14
|
disagree. Undefined when no prefix exists — bare and tag selectors (they
|
|
15
15
|
scan, not prefix-match) and producers never cached (no id was minted;
|
|
16
|
-
minting one here would leak identities for probe-only reads).
|
|
17
|
-
cache()-wrapper throw lives here so both consumers — the matcher and the
|
|
18
|
-
probes' channel tap — reject it before subscribing to anything.
|
|
16
|
+
minting one here would leak identities for probe-only reads).
|
|
19
17
|
*/
|
|
20
18
|
export function selectorPrefix<Args, Return>(
|
|
21
19
|
arg: CacheSelector<Args, Return> | undefined,
|
|
@@ -24,15 +22,10 @@ export function selectorPrefix<Args, Return>(
|
|
|
24
22
|
if (typeof arg !== 'function') {
|
|
25
23
|
return undefined
|
|
26
24
|
}
|
|
27
|
-
if (CACHE_WRAPPED in arg) {
|
|
28
|
-
throw new Error(
|
|
29
|
-
'[abide] a cache() wrapper is not a selector — pass the function it wraps, e.g. pending(getPost), not pending(cache(getPost))',
|
|
30
|
-
)
|
|
31
|
-
}
|
|
32
25
|
const remote = REMOTE_FUNCTION in arg ? (arg as RawRemoteFunction<Args>) : undefined
|
|
33
26
|
if (remote) {
|
|
34
27
|
return args === undefined
|
|
35
|
-
?
|
|
28
|
+
? keyPrefixForRemote(remote.method, remote.url)
|
|
36
29
|
: keyForRemoteCall(remote.method, remote.url, args)
|
|
37
30
|
}
|
|
38
31
|
return producerKey.existing(arg, args)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Assignment can't create an own `__proto__` key — it invokes the
|
|
3
|
+
Object.prototype accessor instead, silently dropping the key or (on decode of
|
|
4
|
+
attacker-controlled wire data) swapping the object's prototype. defineProperty
|
|
5
|
+
always writes an own enumerable data property, so `__proto__` round-trips as a
|
|
6
|
+
plain key. Plain assignment stays on the fast path for every other key.
|
|
7
|
+
*/
|
|
8
|
+
export function setOwnProperty(target: Record<string, unknown>, key: string, value: unknown): void {
|
|
9
|
+
if (key === '__proto__') {
|
|
10
|
+
Object.defineProperty(target, key, {
|
|
11
|
+
value,
|
|
12
|
+
writable: true,
|
|
13
|
+
enumerable: true,
|
|
14
|
+
configurable: true,
|
|
15
|
+
})
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
target[key] = value
|
|
19
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { activeCacheStore } from './activeCacheStore.ts'
|
|
2
|
+
import { sharedCacheStoreSlot } from './sharedCacheStoreSlot.ts'
|
|
3
|
+
import type { CacheStore } from './types/CacheStore.ts'
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
Resolves the process-level ("shared") CacheStore that `cache(fn, { shared: true })`
|
|
7
|
+
entries live in. The server entry registers a module-singleton resolver so the
|
|
8
|
+
store survives across requests; the client points it at the active tab store.
|
|
9
|
+
When no resolver is registered it falls back to the active store, so `shared`
|
|
10
|
+
degrades to request/tab-scoped rather than throwing.
|
|
11
|
+
*/
|
|
12
|
+
export function sharedCacheStore(): CacheStore {
|
|
13
|
+
return sharedCacheStoreSlot.resolver?.() ?? activeCacheStore()
|
|
14
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createResolverSlot } from './createResolverSlot.ts'
|
|
2
|
+
import type { CacheStore } from './types/CacheStore.ts'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
The process-level ("shared") CacheStore slot that `cache(fn, { shared: true })`
|
|
6
|
+
entries live in. The server entry registers a module-singleton store outliving
|
|
7
|
+
every request; the client points it at the active tab store. When no resolver
|
|
8
|
+
is registered, sharedCacheStore() falls back to the active store.
|
|
9
|
+
*/
|
|
10
|
+
export const sharedCacheStoreSlot = createResolverSlot<CacheStore>()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const SNIPPET = Symbol.for('abide.snippet')
|
|
2
2
|
|
|
3
|
-
/* A
|
|
3
|
+
/* A `{#snippet name(args)}` snippet, called like a function: it carries a payload a
|
|
4
4
|
`{expr}` interpolation mounts in place — a DOM builder `(host) => void` on the
|
|
5
5
|
client, the pre-rendered HTML string on the server. The brand is a registered
|
|
6
6
|
Symbol so it survives across module/bundle copies (same idiom as `html\`\``). */
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { createLifecycleChannel } from './createLifecycleChannel.ts'
|
|
2
|
+
import { tailProbeSlot } from './tailProbeSlot.ts'
|
|
3
|
+
|
|
4
|
+
type StreamStatus = 'pending' | 'open' | 'done' | 'error'
|
|
5
|
+
|
|
6
|
+
/* One registered consumption of a named subscribable — the stream-side analog of a
|
|
7
|
+
cache-store entry. Populated by the consume path (cache.on / watch(socket)), read
|
|
8
|
+
by the pending()/refreshing()/done()/error() probes. No window/value here: the
|
|
9
|
+
value flows through the consumer (watch handler / for-await), the registry only
|
|
10
|
+
holds probe state. */
|
|
11
|
+
type StreamEntry = {
|
|
12
|
+
source: string
|
|
13
|
+
status: StreamStatus
|
|
14
|
+
refreshing: boolean
|
|
15
|
+
error: Error | undefined
|
|
16
|
+
/* Reconnect only counts as refreshing once a value has been seen (probe contract:
|
|
17
|
+
value held, fresher source in flight — never a first-ever open). */
|
|
18
|
+
everFramed: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const registry = new Set<StreamEntry>()
|
|
22
|
+
|
|
23
|
+
/*
|
|
24
|
+
Registry-wide lifecycle channel for the subscribable probes — the stream-side
|
|
25
|
+
counterpart of the cache store's. Probes match entries by source name (or all)
|
|
26
|
+
without creating them, so they tap one "membership or state changed" signal and
|
|
27
|
+
re-derive by scanning.
|
|
28
|
+
*/
|
|
29
|
+
const lifecycle = createLifecycleChannel()
|
|
30
|
+
|
|
31
|
+
/*
|
|
32
|
+
Installs the prober the shared probes call for a Subscribable (probeRegistries →
|
|
33
|
+
tailProbeSlot). Register-on-consume: an entry exists only while something is
|
|
34
|
+
consuming the stream, exactly as a cache entry exists only while a call is
|
|
35
|
+
retained. A name with no entry reads as "no value yet" (pending) without opening
|
|
36
|
+
anything — probes report, never act. This install replaces tail()'s: the registrar
|
|
37
|
+
moved from the (removed) tail() reader to the consume path.
|
|
38
|
+
*/
|
|
39
|
+
tailProbeSlot.probe = (name) => {
|
|
40
|
+
lifecycle.track()
|
|
41
|
+
const entries = [...registry].filter((entry) => name === undefined || entry.source === name)
|
|
42
|
+
return {
|
|
43
|
+
pending:
|
|
44
|
+
(name !== undefined && entries.length === 0) ||
|
|
45
|
+
entries.some((entry) => entry.status === 'pending'),
|
|
46
|
+
refreshing: entries.some((entry) => entry.refreshing),
|
|
47
|
+
/* Terminal only when there is at least one entry and all of them have closed. */
|
|
48
|
+
done: entries.length > 0 && entries.every((entry) => entry.status === 'done'),
|
|
49
|
+
error: entries.find((entry) => entry.error !== undefined)?.error,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type StreamProbe = {
|
|
54
|
+
frame(): void
|
|
55
|
+
reconnecting(): void
|
|
56
|
+
done(): void
|
|
57
|
+
errored(error: Error): void
|
|
58
|
+
close(): void
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/*
|
|
62
|
+
Opens a probe entry for a consumed named subscribable and returns the handle the
|
|
63
|
+
consume loop drives: `frame()` on each delivered value (pending → open), plus the
|
|
64
|
+
terminal/lifecycle transitions. `close()` evicts on teardown. Reactivity is the
|
|
65
|
+
registry lifecycle channel, marked on every transition.
|
|
66
|
+
*/
|
|
67
|
+
export function openStreamProbe(source: string): StreamProbe {
|
|
68
|
+
const entry: StreamEntry = {
|
|
69
|
+
source,
|
|
70
|
+
status: 'pending',
|
|
71
|
+
refreshing: false,
|
|
72
|
+
error: undefined,
|
|
73
|
+
everFramed: false,
|
|
74
|
+
}
|
|
75
|
+
registry.add(entry)
|
|
76
|
+
lifecycle.mark()
|
|
77
|
+
return {
|
|
78
|
+
frame() {
|
|
79
|
+
/* Only ping on a real transition (pending→open, or refreshing clearing) so a
|
|
80
|
+
chatty stream doesn't re-derive every bare probe reader on each value. */
|
|
81
|
+
const transitioned = entry.status !== 'open' || entry.refreshing
|
|
82
|
+
entry.status = 'open'
|
|
83
|
+
entry.refreshing = false
|
|
84
|
+
entry.everFramed = true
|
|
85
|
+
if (transitioned) {
|
|
86
|
+
lifecycle.mark()
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
reconnecting() {
|
|
90
|
+
entry.refreshing = entry.everFramed
|
|
91
|
+
lifecycle.mark()
|
|
92
|
+
},
|
|
93
|
+
done() {
|
|
94
|
+
if (entry.status !== 'error') {
|
|
95
|
+
entry.status = 'done'
|
|
96
|
+
}
|
|
97
|
+
entry.refreshing = false
|
|
98
|
+
lifecycle.mark()
|
|
99
|
+
},
|
|
100
|
+
errored(error: Error) {
|
|
101
|
+
entry.error = error
|
|
102
|
+
entry.status = 'error'
|
|
103
|
+
entry.refreshing = false
|
|
104
|
+
lifecycle.mark()
|
|
105
|
+
},
|
|
106
|
+
close() {
|
|
107
|
+
registry.delete(entry)
|
|
108
|
+
lifecycle.mark()
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -10,7 +10,14 @@ itself gives on the server: a named stream has no value yet (pending true),
|
|
|
10
10
|
and nothing is reconnecting (refreshing false).
|
|
11
11
|
*/
|
|
12
12
|
export const tailProbeSlot: {
|
|
13
|
-
probe:
|
|
13
|
+
probe:
|
|
14
|
+
| ((name?: string) => {
|
|
15
|
+
pending: boolean
|
|
16
|
+
refreshing: boolean
|
|
17
|
+
done: boolean
|
|
18
|
+
error: Error | undefined
|
|
19
|
+
})
|
|
20
|
+
| undefined
|
|
14
21
|
} = {
|
|
15
22
|
probe: undefined,
|
|
16
23
|
}
|
|
@@ -59,26 +59,20 @@ export type CacheEntry = {
|
|
|
59
59
|
ttl: number | undefined
|
|
60
60
|
expiresAt: number | undefined
|
|
61
61
|
value?: unknown
|
|
62
|
-
/*
|
|
63
|
-
Lazy warm materializer (Tier 2 deferred seed): a memoized thunk that decodes the
|
|
64
|
-
shipped body into the warm value on first call, so a deferred snapshot pays no decode
|
|
65
|
-
at hydration — only when a read actually needs the value. `value` stays undefined until
|
|
66
|
-
then; the read gate treats an entry with `warm` set as warm without invoking it. Eager
|
|
67
|
-
snapshots set `value` and leave this undefined.
|
|
68
|
-
*/
|
|
69
|
-
warm?: () => unknown
|
|
70
62
|
tags?: Set<string>
|
|
71
63
|
settled?: boolean
|
|
72
64
|
hydrated?: boolean
|
|
73
|
-
/*
|
|
74
|
-
Marks that the SSR resume path deferred this entry's `{#await cache()}` value — shipped a
|
|
75
|
-
`{ defer, key }` marker instead of inlining it. The post-stream snapshot drain reads it to
|
|
76
|
-
ship the body as a LAZY seed (decoded on first read, not at hydration). Set server-side
|
|
77
|
-
only, on the request-scoped entry, during the stream.
|
|
78
|
-
*/
|
|
79
|
-
deferred?: boolean
|
|
80
65
|
refreshing?: boolean
|
|
81
66
|
invalidation?: InvalidationState
|
|
67
|
+
/*
|
|
68
|
+
Set by the smart bare call (a replayable read routed through cache.read): the
|
|
69
|
+
display value is retained unconditionally and `ttl` marks a staleness deadline
|
|
70
|
+
(the next read past it revalidates in the background, stale stays visible) rather
|
|
71
|
+
than driving the hard eviction an explicit cache() ttl triggers. Distinguishes the
|
|
72
|
+
smart-read lifecycle from the explicit cache()/invalidate old surface, which keeps
|
|
73
|
+
its drop-on-ttl / drop-on-invalidate behaviour.
|
|
74
|
+
*/
|
|
75
|
+
retain?: boolean
|
|
82
76
|
}
|
|
83
77
|
|
|
84
78
|
/* Per-key invalidate coalescing: the throttle/debounce policy plus the timer/in-flight state. */
|
|
@@ -2,21 +2,25 @@
|
|
|
2
2
|
Options for cache(). The key is always auto-derived (method+url+args for a remote
|
|
3
3
|
function, producer-reference+args for a plain producer): hoist a producer to a
|
|
4
4
|
stable reference to share its entry across calls. `ttl` is the
|
|
5
|
-
milliseconds-past-resolve that the entry stays live:
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
milliseconds-past-resolve that the entry stays live: 0 = dedupe only (entry
|
|
6
|
+
dropped once the promise settles — the mutation idiom: in-flight coalescing and
|
|
7
|
+
pending() visibility, nothing retained), any other number = TTL. Omitted = forever
|
|
8
|
+
for a producer and for a remote call on the client; a remote call on the SERVER
|
|
9
|
+
with neither `ttl` nor `swr` stated defaults to 0 (coalesce-only — the request is
|
|
10
|
+
the atomic unit, nothing is retained past it; pair with `shared` + an explicit
|
|
11
|
+
`ttl` to memoise across requests).
|
|
9
12
|
`tags` is an array of free-form labels grouping unrelated calls so one
|
|
10
13
|
`cache.invalidate({ tags })` drops every entry sharing any of them — list
|
|
11
14
|
multiple when a call belongs to multiple invalidation groups. A unique tag (e.g.
|
|
12
15
|
a uuid) shared by a set of calls gives them their own private invalidation group.
|
|
13
16
|
|
|
14
|
-
`
|
|
15
|
-
request-scoped one (server) —
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
`shared` opts the entry into the process-level store instead of the default
|
|
18
|
+
request-scoped one (server) — a store that outlives every request. It selects
|
|
19
|
+
the store only; it does NOT retain (pair it with `ttl` to memoise across
|
|
20
|
+
requests). The shared store is keyed by method+url+args, never by user, so do
|
|
21
|
+
not put per-user data in it — it would be served to other users. Omit `shared`
|
|
22
|
+
for per-request data. Write only `shared: true`; there is no `false` form. On
|
|
23
|
+
the client there is a single tab store, so the flag is a no-op there.
|
|
20
24
|
|
|
21
25
|
`swr` is stale-while-revalidate: it changes what a `cache.invalidate` hit does
|
|
22
26
|
to this key. Without it, an invalidate drops the entry and the next read shows
|
|
@@ -34,10 +38,18 @@ cache() throws at wrap time on throttle+debounce set at once, on ttl: 0 (nothing
|
|
|
34
38
|
retained, nothing to revalidate), and on a non-replayable remote method
|
|
35
39
|
(replaying a write is a state change disguised as a refresh). Producers are
|
|
36
40
|
uncheckable — set `swr` only on a producer that is a pure read.
|
|
41
|
+
|
|
42
|
+
`throttle`/`debounce` are root rate-limit windows for the refetch clock. For the
|
|
43
|
+
smart bare call — where SWR is unconditional for replayable reads, so there is no
|
|
44
|
+
`swr` toggle to hang a window off — they cap how often a background revalidation
|
|
45
|
+
fires (leading-edge-then-coalesce, or fire-after-quiet respectively). They pair
|
|
46
|
+
with the same wrap-time guard as the `swr` window: set one, not both.
|
|
37
47
|
*/
|
|
38
48
|
export type CacheOptions = {
|
|
39
49
|
ttl?: number
|
|
40
50
|
tags?: string[]
|
|
41
|
-
|
|
51
|
+
shared?: boolean
|
|
42
52
|
swr?: boolean | { throttle?: number; debounce?: number }
|
|
53
|
+
throttle?: number
|
|
54
|
+
debounce?: number
|
|
43
55
|
}
|
|
@@ -14,9 +14,4 @@ export type CacheSnapshotEntry = {
|
|
|
14
14
|
statusText: string
|
|
15
15
|
headers: Array<[string, string]>
|
|
16
16
|
body: string
|
|
17
|
-
/* Deferred seed (Tier 2): the client stores the body but does NOT decode it at boot —
|
|
18
|
-
the warm value is materialized lazily on the first read, off the hydration path. Set
|
|
19
|
-
by the server for a deferred `{#await cache()}` whose value ships via a `{defer,key}`
|
|
20
|
-
resume marker instead of inline, so hydration touches neither copy's decode. */
|
|
21
|
-
lazy?: boolean
|
|
22
17
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Per-store cache read tallies, frozen into the request's closing log record at
|
|
3
3
|
settle. hit = a read served from a settled retained entry (including the warm
|
|
4
4
|
SSR sync path); coalesced = a read that joined an in-flight call; miss = a
|
|
5
|
-
read that invoked its producer/remote. Reads against the process-level
|
|
5
|
+
read that invoked its producer/remote. Reads against the process-level shared
|
|
6
6
|
store count into the requesting scope's store, so a request's record reflects
|
|
7
7
|
everything that request asked the cache for.
|
|
8
8
|
*/
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { RpcOptions } from './RpcOptions.ts'
|
|
2
|
+
import type { Subscribable } from './Subscribable.ts'
|
|
2
3
|
|
|
3
4
|
/*
|
|
4
5
|
Call signature shared by RemoteFunction and RawRemoteFunction. The base
|
|
@@ -8,14 +9,31 @@ optional-arg signature lets call sites write `fn()` instead of
|
|
|
8
9
|
`fn(undefined)`. Intersection rather than a bare conditional so the type
|
|
9
10
|
stays callable while `Args` is still generic (cache() invokes producers
|
|
10
11
|
before `Args` resolves). FormData is the multipart upload escape hatch —
|
|
11
|
-
see RemoteFunction. The
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
see RemoteFunction. The trailing `Opts` differs by variant: the smart bare
|
|
13
|
+
call (RemoteFunction) carries cache/stream options (SmartReadOptions), while
|
|
14
|
+
`.raw` (RawRemoteFunction) carries per-call transport options (RpcOptions:
|
|
15
|
+
signal/keepalive/priority/cache/headers). The server ignores either, so the
|
|
16
|
+
callable stays isomorphic.
|
|
17
|
+
|
|
18
|
+
A streaming `Resolved` (an `AsyncIterable<Frame>`, branded by jsonl()/sse()) makes
|
|
19
|
+
the bare call return a `Subscribable<Frame>` synchronously — the iterable IS the
|
|
20
|
+
value, consumed by `for await` or `state(fn(args))`, no `.stream()`. Every other
|
|
21
|
+
`Resolved` stays `Promise<Resolved>`. `await`-ing a streaming call is a compile
|
|
22
|
+
error (a Subscribable is not awaitable), which replaces the old runtime guard.
|
|
14
23
|
*/
|
|
15
|
-
|
|
24
|
+
/* `[Resolved] extends [never]` first: a handler that only ever returns error() resolves to
|
|
25
|
+
`never`, and `never extends AsyncIterable` is vacuously true — without this guard its call
|
|
26
|
+
would mistype as a Subscribable. A real streaming rpc's `Resolved` is `AsyncIterable<Frame>`. */
|
|
27
|
+
type CallResult<Resolved> = [Resolved] extends [never]
|
|
28
|
+
? Promise<Resolved>
|
|
29
|
+
: Resolved extends AsyncIterable<infer Frame>
|
|
30
|
+
? Subscribable<Frame>
|
|
31
|
+
: Promise<Resolved>
|
|
32
|
+
|
|
33
|
+
export type RemoteCallable<Args, Resolved, Opts = RpcOptions> = ((
|
|
16
34
|
args: Args | FormData,
|
|
17
|
-
opts?:
|
|
18
|
-
) =>
|
|
35
|
+
opts?: Opts,
|
|
36
|
+
) => CallResult<Resolved>) &
|
|
19
37
|
(undefined extends Args
|
|
20
|
-
? (args?: Args | FormData, opts?:
|
|
38
|
+
? (args?: Args | FormData, opts?: Opts) => CallResult<Resolved>
|
|
21
39
|
: unknown)
|
|
@@ -4,8 +4,9 @@ import type { HttpMethod } from './HttpMethod.ts'
|
|
|
4
4
|
import type { Outbox } from './Outbox.ts'
|
|
5
5
|
import type { RawRemoteFunction } from './RawRemoteFunction.ts'
|
|
6
6
|
import type { RemoteCallable } from './RemoteCallable.ts'
|
|
7
|
+
import type { RpcError } from './RpcError.ts'
|
|
7
8
|
import type { RpcErrorGuard } from './RpcErrorGuard.ts'
|
|
8
|
-
import type {
|
|
9
|
+
import type { SmartReadOptions } from './SmartReadOptions.ts'
|
|
9
10
|
|
|
10
11
|
/*
|
|
11
12
|
Remote function reference produced by GET/POST/... inside an `$rpc/**`
|
|
@@ -19,12 +20,12 @@ RemoteFunction whose call resolves to the underlying Response — same
|
|
|
19
20
|
method, same url, same args, no decode. Pass `fn.raw` to cache() to
|
|
20
21
|
memoise raw Responses against the same cache key as `fn` (both share one
|
|
21
22
|
stored entry — the decode just happens on the way out for callers of
|
|
22
|
-
`fn`).
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
`fn`). A streaming handler (jsonl()/sse()) makes the bare call return a
|
|
24
|
+
`Subscribable<Frame>` directly (the iterable IS the value) — consume it with
|
|
25
|
+
`for await (… of fn(args))` or `state(fn(args))`; there is no `.stream()`, and
|
|
26
|
+
`await`-ing a streaming call is a compile error. For sustained broadcast /
|
|
27
|
+
pub-sub use the `abide/server/socket` primitive — HTTP rpc isn't the place for
|
|
28
|
+
long-lived multi-publisher subscriptions.
|
|
28
29
|
`.fetch(req)` is the framework's request-dispatch entry point — used by
|
|
29
30
|
the router to invoke the handler from an incoming HTTP request, not
|
|
30
31
|
for user code.
|
|
@@ -44,23 +45,57 @@ export type RemoteFunction<
|
|
|
44
45
|
Return,
|
|
45
46
|
Errors extends ErrorSpec = Record<never, never>,
|
|
46
47
|
Durable extends boolean = false,
|
|
47
|
-
> = RemoteCallable<Args, Return> & {
|
|
48
|
+
> = RemoteCallable<Args, Return, SmartReadOptions> & {
|
|
48
49
|
readonly method: HttpMethod
|
|
49
50
|
readonly url: string
|
|
50
51
|
readonly clients: ClientFlags
|
|
51
52
|
readonly crossOrigin?: boolean
|
|
52
53
|
readonly raw: RawRemoteFunction<Args>
|
|
53
|
-
stream(args?: Args | FormData): Subscribable<Return>
|
|
54
54
|
fetch(request: Request): Promise<Response>
|
|
55
55
|
/* Type-guard a caught error against this rpc's declared `errors` (plus the framework
|
|
56
56
|
`'validation'` / `'queued'`): narrows `.kind` and, for a known kind, `.data` — the
|
|
57
57
|
per-rpc replacement for a global guard, since the error name → data type mapping
|
|
58
58
|
lives in the rpc's own spec. */
|
|
59
59
|
readonly isError: RpcErrorGuard<Errors>
|
|
60
|
-
|
|
60
|
+
/* Pre-bound selector sugar: `fn.pending(args?)` ≡ `pending(fn, args?)`, and likewise for
|
|
61
|
+
refreshing / refresh / peek — the rpc is the leading selector, bound in. The argument is
|
|
62
|
+
this rpc's typed `Args` (the by-args refinement); tags / cross-cutting selection stay on
|
|
63
|
+
the globals. The bare call `fn(args, opts)` IS the cached read (was `fn.cache`), and
|
|
64
|
+
`refresh(args?)` refetches keeping the stale value visible (was `invalidate`, which
|
|
65
|
+
dropped). `peek(args?)` reads the retained value synchronously (a streaming rpc peeks its
|
|
66
|
+
latest frame). `patch` is fetch-only, so it is omitted for a streaming rpc (a stream isn't
|
|
67
|
+
a memoized value) via the intersection below. */
|
|
68
|
+
pending(args?: Args): boolean
|
|
69
|
+
refreshing(args?: Args): boolean
|
|
70
|
+
refresh(args?: Args): void
|
|
71
|
+
peek(args?: Args): ([Return] extends [AsyncIterable<infer Frame>] ? Frame : Return) | undefined
|
|
72
|
+
/* This rpc's last error, typed off `Errors` (a discriminated union already narrowed on
|
|
73
|
+
`.kind`/`.data`). `args` scopes to one call; omitted aggregates the most-recent across
|
|
74
|
+
this rpc. Truthiness is the "isError" check. Instance-only — no bare global (it would
|
|
75
|
+
shadow the server `error()` thrower). Reads the rpc error registry captured at the call
|
|
76
|
+
boundary; the cache is untouched. */
|
|
77
|
+
error(args?: Args): RpcError<Errors> | undefined
|
|
78
|
+
/* Client-only reaction sugar: `fn.watch(handler)` / `fn.watch(args, handler)` ≡
|
|
79
|
+
`watch(fn, …)` — runs the smart read reactively and pipes each resolved value to the
|
|
80
|
+
handler, returning a scope-tied disposer. Reaction is a client concern (`watch` is a ui
|
|
81
|
+
primitive), so this is an inert no-op server-side; an author `fn.watch(…)` surviving the
|
|
82
|
+
SSR effect-strip (which leaves member calls intact) is a safe no-op there. The real method
|
|
83
|
+
is attached client-side by remoteProxy. */
|
|
84
|
+
watch(handler: (value: Return) => void): () => void
|
|
85
|
+
watch(args: Args, handler: (value: Return) => void): () => void
|
|
86
|
+
} /* `patch` is fetch-only: a streaming rpc has no single memoized value to mutate, so the
|
|
87
|
+
method is present only when `Return` is not an AsyncIterable. Two signatures: with args
|
|
88
|
+
(`patch(args, updater)`) and without (`patch(updater)` — every args-variant). Tuple-wrapped
|
|
89
|
+
so the conditional doesn't distribute over a `never` Return (an error-only rpc). */ & ([
|
|
90
|
+
Return,
|
|
91
|
+
] extends [AsyncIterable<unknown>]
|
|
92
|
+
? Record<never, never>
|
|
93
|
+
: {
|
|
94
|
+
patch(args: Args | undefined, updater: (current: Return) => Return): void
|
|
95
|
+
patch(updater: (current: Return) => Return): void
|
|
96
|
+
}) /* `outbox` presence follows the `outbox: true` opt: the mutating helper threads `Durable`
|
|
61
97
|
into the return type so a DURABLE rpc's `.outbox` is the required queue face (no optional
|
|
62
98
|
chain). A non-durable rpc keeps it optional — assignable to a durable one everywhere a
|
|
63
99
|
bare `RemoteFunction<Args, Return>` slot (cache selectors, registries) is expected, since
|
|
64
|
-
required→optional widens cleanly and no call site had to learn the `Durable` bit. */ &
|
|
65
|
-
|
|
66
|
-
: { readonly outbox?: Outbox<Args> })
|
|
100
|
+
required→optional widens cleanly and no call site had to learn the `Durable` bit. */ &
|
|
101
|
+
(Durable extends true ? { readonly outbox: Outbox<Args> } : { readonly outbox?: Outbox<Args> })
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/*
|
|
2
|
-
|
|
3
|
-
lazy/value fallback used when no
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
One resolved runtime value's whole seam: the mutable cell a runtime entry
|
|
3
|
+
registers its resolver into, the single lazy/value fallback used when no
|
|
4
|
+
resolver is registered, and the read. createServer and startClient install
|
|
5
|
+
side-specific resolvers by assigning `.resolver`; isolated tests poke
|
|
6
|
+
`.resolver` / `.fallback` directly, so both fields stay public and mutable.
|
|
6
7
|
*/
|
|
7
8
|
export type ResolverSlot<T> = {
|
|
8
9
|
resolver: (() => T | undefined) | undefined
|
|
9
10
|
fallback: T | undefined
|
|
11
|
+
// The resolved value: the registered resolver's, else the (lazy) fallback.
|
|
12
|
+
get(): T | undefined
|
|
10
13
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { HttpError } from '../HttpError.ts'
|
|
2
|
+
import type { ErrorSpec } from './ErrorSpec.ts'
|
|
3
|
+
import type { StandardSchemaV1 } from './StandardSchemaV1.ts'
|
|
4
|
+
|
|
5
|
+
/* Payload a declared error name carries: its data schema's inferred input, or `unknown`
|
|
6
|
+
for a nullary error (no data schema) — mirrors RpcErrorGuard's DeclaredErrorData. */
|
|
7
|
+
type DeclaredData<
|
|
8
|
+
Errors extends ErrorSpec,
|
|
9
|
+
Name extends keyof Errors,
|
|
10
|
+
> = Errors[Name]['data'] extends StandardSchemaV1
|
|
11
|
+
? StandardSchemaV1.InferInput<Errors[Name]['data']>
|
|
12
|
+
: unknown
|
|
13
|
+
|
|
14
|
+
/*
|
|
15
|
+
The typed error value `fn.error()` returns: the discriminated union of this rpc's declared
|
|
16
|
+
errors (name → data), each an HttpError narrowed on `.kind` / `.data`. The read-side mirror
|
|
17
|
+
of RpcErrorGuard's narrowing, as a value rather than a guard — so `getUser.error()?.name`
|
|
18
|
+
is already narrowed with no guard call. `never` (collapses to `undefined`-only at the call
|
|
19
|
+
site) for an rpc that declares no errors.
|
|
20
|
+
*/
|
|
21
|
+
export type RpcError<Errors extends ErrorSpec> = {
|
|
22
|
+
[Name in keyof Errors & string]: HttpError & {
|
|
23
|
+
kind: Name
|
|
24
|
+
data: DeclaredData<Errors, Name>
|
|
25
|
+
}
|
|
26
|
+
}[keyof Errors & string]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Second-argument options for the smart bare rpc call (`getFoo(args, opts)`). The
|
|
3
|
+
call is cached/coalesced/reactive with SWR always on for replayable reads, so
|
|
4
|
+
these govern retention and the refetch clock — NOT transport. Per-call transport
|
|
5
|
+
options (signal/keepalive/priority/cache/headers) live on `.raw(args, init)`.
|
|
6
|
+
|
|
7
|
+
Fetch reads:
|
|
8
|
+
- `ttl` — retention/staleness in ms. Default: 0 on the server (coalesce-only —
|
|
9
|
+
the request is the atomic unit, nothing is retained past it), Infinity on the
|
|
10
|
+
client (retain until invalidate/refresh — the tab is the atomic unit). On the
|
|
11
|
+
client, N ms marks a staleness deadline: the retained value goes stale after N
|
|
12
|
+
ms and the next access triggers a background revalidation (stale stays visible,
|
|
13
|
+
`refreshing()` true); the display value is never dropped. On the server, N ms
|
|
14
|
+
is a plain expiry and only takes effect in the shared store (pair with `shared`).
|
|
15
|
+
- `tags` — free-form invalidation-group labels (see the selector grammar).
|
|
16
|
+
- `throttle` / `debounce` — rate-limit the background refetch clock (set one, not
|
|
17
|
+
both). Leading-edge-then-coalesce, or fire-after-quiet respectively.
|
|
18
|
+
- `shared` — opts the entry into the process-level store instead of the default
|
|
19
|
+
request-scoped one (server) — a store that outlives every request. It selects
|
|
20
|
+
the store only; it does NOT retain (pair it with `ttl` to memoise across
|
|
21
|
+
requests). The shared store is keyed by method+url+args, never by user, so do
|
|
22
|
+
not put per-user data in it — it would be served to other users. Omit `shared`
|
|
23
|
+
for per-request data. Write only `shared: true`; there is no `false` form. On
|
|
24
|
+
the client there is a single tab store, so the flag is a no-op there.
|
|
25
|
+
|
|
26
|
+
Streaming reads (jsonl/sse):
|
|
27
|
+
- `n` — how many retained frames to replay before going live.
|
|
28
|
+
*/
|
|
29
|
+
export type SmartReadOptions = {
|
|
30
|
+
ttl?: number
|
|
31
|
+
tags?: string[]
|
|
32
|
+
throttle?: number
|
|
33
|
+
debounce?: number
|
|
34
|
+
shared?: boolean
|
|
35
|
+
n?: number
|
|
36
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { CacheOnContext } from './CacheOnContext.ts'
|
|
2
|
+
import type { ClientFlags } from './ClientFlags.ts'
|
|
3
|
+
import type { TailHooks } from './TailHooks.ts'
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
Bidirectional named broadcast primitive. Declared once with `socket<T>()`
|
|
7
|
+
inside a file under `src/server/sockets/`; the same import resolves to a server-side
|
|
8
|
+
fan-out and a client-side ws proxy by build target. Iterating the socket
|
|
9
|
+
is the live stream — no replay. `.tail(count)` opens a subscription seeded
|
|
10
|
+
with the last `count` frames of the retained tail (declared via
|
|
11
|
+
`{ tail: n }`; no-arg = the whole retained tail) before going live — the
|
|
12
|
+
optional Subscribable retention capability a reactive `watch(socket, …)`
|
|
13
|
+
seeds from. `broadcast` is isomorphic: server code broadcasts in-process
|
|
14
|
+
and fans out to remote subscribers; client code sends a `pub` frame the
|
|
15
|
+
dispatcher validates against the topic's `clientPublish` flag. `clients`
|
|
16
|
+
exposes which adapter surfaces (browser / mcp / cli) advertise this
|
|
17
|
+
socket.
|
|
18
|
+
*/
|
|
19
|
+
export interface Socket<T> extends AsyncIterable<T> {
|
|
20
|
+
readonly name: string
|
|
21
|
+
readonly clients: ClientFlags
|
|
22
|
+
/* Send a frame to every subscriber (server always; client only when `clientPublish` is
|
|
23
|
+
set). Isomorphic: server code fans out in-process + to remote subscribers; client code
|
|
24
|
+
sends a validated `pub` frame. */
|
|
25
|
+
broadcast(message: T): void
|
|
26
|
+
tail(count?: number, hooks?: TailHooks): AsyncIterable<T>
|
|
27
|
+
/* The latest retained frame, synchronously — `T | undefined` when none. The value
|
|
28
|
+
member of the probe family for a stream; `peek(socket)` routes here. */
|
|
29
|
+
peek(): T | undefined
|
|
30
|
+
/* Reactive probe sugar mirroring the rpc instance — the socket is the pre-bound stream
|
|
31
|
+
selector: `socket.pending()` ≡ `pending(socket)` (no frame yet), `socket.refreshing()` ≡
|
|
32
|
+
`refreshing(socket)` (a held frame revalidating, e.g. across a reconnect gap),
|
|
33
|
+
`socket.done()` ≡ `done(socket)` (the stream closed). Client-reactive; on the server (and
|
|
34
|
+
the raw test builder) they read the same fallbacks the globals give with no tail prober
|
|
35
|
+
registered (pending true until a frame, refreshing/done false). */
|
|
36
|
+
pending(): boolean
|
|
37
|
+
refreshing(): boolean
|
|
38
|
+
done(): boolean
|
|
39
|
+
/* The stream's terminal error, or undefined. Instance-only — like the rpc's `.error`, since a
|
|
40
|
+
bare `error` global would shadow the server `error()` thrower. Reactive off the tail prober's
|
|
41
|
+
error field; undefined on the server. */
|
|
42
|
+
error(): Error | undefined
|
|
43
|
+
/* Drop local frames and re-pull the server's retained tail — the socket analog of
|
|
44
|
+
getFoo.refresh(), for a ttl-expired frame or a reconnect resync. Server-side it is a
|
|
45
|
+
no-op (the server IS the source). */
|
|
46
|
+
refresh(): void
|
|
47
|
+
/* Client-only reaction sugar: `socket.watch(handler)` ≡ `watch(socket, handler)` — the
|
|
48
|
+
handler runs per delivered frame with reconnect-replay and returns a scope-tied disposer.
|
|
49
|
+
Reaction is a client concern (`watch` is a ui primitive that never rides into a server
|
|
50
|
+
bundle), so server-side — and in the raw test builder — this is an inert no-op like
|
|
51
|
+
`.refresh`; the SSR effect-strip leaves member calls intact, so an author `socket.watch(…)`
|
|
52
|
+
reaching the server is a safe no-op. The real method is attached client-side by socketProxy. */
|
|
53
|
+
watch(handler: (frame: T, context: CacheOnContext) => void | Promise<void>): () => void
|
|
54
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/*
|
|
2
|
+
The subset of the `__SSR__` payload the client seeds into ambient slots at boot: the
|
|
3
|
+
mount base, the default log channel (app name), the warm health payload, and the
|
|
4
|
+
env-configured RPC client timeout. Every field here is stamped by the server's SSR
|
|
5
|
+
state tag and MUST be seeded client-side by `seedBootState`, whose seed map is keyed
|
|
6
|
+
EXHAUSTIVELY off this type — so a field added here fails to compile until both sides
|
|
7
|
+
wire it. That compile error is the guard closing the silent stamped-but-unseeded gap
|
|
8
|
+
(the bug that left `ABIDE_CLIENT_TIMEOUT` inoperative in the browser while every test
|
|
9
|
+
still passed).
|
|
10
|
+
*/
|
|
11
|
+
export type SsrBootState = {
|
|
12
|
+
/* The mount base (`base || undefined`); seeds the base resolver so call keys and
|
|
13
|
+
navigation resolve rooted paths. Seeded first — the cache seed that follows keys
|
|
14
|
+
through it. */
|
|
15
|
+
base?: string
|
|
16
|
+
/* The app name — the default log channel; falls back to `'app'` when absent. */
|
|
17
|
+
app?: string
|
|
18
|
+
/* The health payload, so `health()`'s first client probe is warm rather than cold. */
|
|
19
|
+
health?: Record<string, unknown>
|
|
20
|
+
/* The env-configured RPC client timeout (`ABIDE_CLIENT_TIMEOUT`), shipped per request;
|
|
21
|
+
absent → unbounded client fetches. */
|
|
22
|
+
clientTimeout?: number
|
|
23
|
+
/* The SSR request's W3C traceparent; seeds the browser request scope so trace(), log
|
|
24
|
+
prefixes, and RPC traceparent headers continue the document's trace. Absent → a
|
|
25
|
+
fresh sampled trace is minted at boot. */
|
|
26
|
+
trace?: string
|
|
27
|
+
}
|