@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
|
@@ -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
|
+
}
|
|
@@ -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. */
|
|
@@ -34,10 +34,18 @@ cache() throws at wrap time on throttle+debounce set at once, on ttl: 0 (nothing
|
|
|
34
34
|
retained, nothing to revalidate), and on a non-replayable remote method
|
|
35
35
|
(replaying a write is a state change disguised as a refresh). Producers are
|
|
36
36
|
uncheckable — set `swr` only on a producer that is a pure read.
|
|
37
|
+
|
|
38
|
+
`throttle`/`debounce` are root rate-limit windows for the refetch clock. For the
|
|
39
|
+
smart bare call — where SWR is unconditional for replayable reads, so there is no
|
|
40
|
+
`swr` toggle to hang a window off — they cap how often a background revalidation
|
|
41
|
+
fires (leading-edge-then-coalesce, or fire-after-quiet respectively). They pair
|
|
42
|
+
with the same wrap-time guard as the `swr` window: set one, not both.
|
|
37
43
|
*/
|
|
38
44
|
export type CacheOptions = {
|
|
39
45
|
ttl?: number
|
|
40
46
|
tags?: string[]
|
|
41
47
|
global?: boolean
|
|
42
48
|
swr?: boolean | { throttle?: number; debounce?: number }
|
|
49
|
+
throttle?: number
|
|
50
|
+
debounce?: number
|
|
43
51
|
}
|
|
@@ -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
|
}
|
|
@@ -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,32 @@
|
|
|
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: undefined = retain forever (no auto-refetch),
|
|
9
|
+
N ms = the retained value goes stale after N ms and the next access triggers a
|
|
10
|
+
background revalidation (stale stays visible, `refreshing()` true). The display
|
|
11
|
+
value is never dropped; ttl drives staleness, not eviction.
|
|
12
|
+
- `tags` — free-form invalidation-group labels (see the selector grammar).
|
|
13
|
+
- `throttle` / `debounce` — rate-limit the background refetch clock (set one, not
|
|
14
|
+
both). Leading-edge-then-coalesce, or fire-after-quiet respectively.
|
|
15
|
+
- `global` — store the entry in the process-level store instead of the default
|
|
16
|
+
request-scoped one (server), so a value computed in one request is reused by
|
|
17
|
+
later requests — the memoise-an-external-endpoint case. Omit it for per-user
|
|
18
|
+
data: the default keeps a per-user response from leaking across requests.
|
|
19
|
+
Write only `global: true`; there is no `false` form. On the client there is a
|
|
20
|
+
single tab store, so the flag is a no-op there.
|
|
21
|
+
|
|
22
|
+
Streaming reads (jsonl/sse):
|
|
23
|
+
- `n` — how many retained frames to replay before going live.
|
|
24
|
+
*/
|
|
25
|
+
export type SmartReadOptions = {
|
|
26
|
+
ttl?: number
|
|
27
|
+
tags?: string[]
|
|
28
|
+
throttle?: number
|
|
29
|
+
debounce?: number
|
|
30
|
+
global?: boolean
|
|
31
|
+
n?: number
|
|
32
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CacheSnapshotEntry } from './CacheSnapshotEntry.ts'
|
|
2
|
+
import type { SsrBootState } from './SsrBootState.ts'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
The `window.__SSR__` payload: the single contract between the server's SSR state tag
|
|
6
|
+
(`createUiPageRenderer.stateTag`) and the client entry (`startClient`). Both sides
|
|
7
|
+
import THIS type — the server stamps its object with `satisfies SsrPayload` (an unknown
|
|
8
|
+
or mis-typed field is a compile error) and the client reads through it — so the
|
|
9
|
+
write-set and read-set can't drift apart on a matching-string-key handshake.
|
|
10
|
+
|
|
11
|
+
Three partitions:
|
|
12
|
+
- `SsrBootState` fields seed ambient slots via the exhaustive `seedBootState` map;
|
|
13
|
+
- `cache` is the inline warm-seed partition, drained through `seedResolved`;
|
|
14
|
+
- `route`/`params` are stamped for inspectability and are NOT read by the client
|
|
15
|
+
(the router re-resolves the route from the URL).
|
|
16
|
+
*/
|
|
17
|
+
export type SsrPayload = SsrBootState & {
|
|
18
|
+
route: string
|
|
19
|
+
params: Record<string, string>
|
|
20
|
+
cache?: CacheSnapshotEntry[]
|
|
21
|
+
}
|
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
import type { TailHooks } from './TailHooks.ts'
|
|
2
2
|
|
|
3
3
|
/*
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
stream
|
|
8
|
-
|
|
4
|
+
A named async stream: an AsyncIterable carrying a stable `name` used as the
|
|
5
|
+
subscription registry key. Both `Socket<T>` (the declared broadcast
|
|
6
|
+
primitive) and a streaming rpc's bare call (a `jsonl`/`sse` handler makes
|
|
7
|
+
the call return the stream itself) satisfy this shape, so the consume path
|
|
8
|
+
(`watch` → `cache.on`) and the stream probes (`pending`/`refreshing`/
|
|
9
|
+
`done`/`error`) handle either source through one contract.
|
|
9
10
|
|
|
10
11
|
The name on a Socket comes from the file path under `src/server/sockets/`.
|
|
11
|
-
The name on
|
|
12
|
-
|
|
13
|
-
args dedupe to one underlying fetch.
|
|
12
|
+
The name on a streaming rpc call is `keyForRemoteCall(method, url, args)` —
|
|
13
|
+
the same key the smart call's cache store uses — so two readers of the same
|
|
14
|
+
remote-call args dedupe to one underlying fetch.
|
|
14
15
|
|
|
15
16
|
`tail` is the optional retention capability: a source that keeps a tail of
|
|
16
17
|
recent frames hands back an iterable seeded with at most the last `count`
|
|
17
|
-
before going live. Sockets implement it verbatim
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
readers can commit atomically; see TailHooks.
|
|
18
|
+
before going live. Sockets implement it verbatim (it also backs the socket's
|
|
19
|
+
HTTP/SSE read face); one-shot rpc streams omit it. Implementers must signal
|
|
20
|
+
`hooks.replayed` in-band once the seed portion is delivered (even when
|
|
21
|
+
empty) so a seeded reader can commit its window atomically; see TailHooks.
|
|
22
22
|
*/
|
|
23
23
|
export interface Subscribable<T> extends AsyncIterable<T> {
|
|
24
24
|
readonly name: string
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
|
-
Hooks a tail()
|
|
2
|
+
Hooks a `tail(count, hooks)` caller passes to a Subscribable's retention
|
|
3
|
+
capability.
|
|
3
4
|
`replayed` must be signalled in-band exactly once per iteration — after the
|
|
4
5
|
last replayed frame, before any live frame, and even when nothing was
|
|
5
6
|
replayed — so a window reader can commit its seed atomically instead of
|