@abide/abide 0.43.1 → 0.44.1
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 +293 -219
- package/CHANGELOG.md +63 -0
- package/README.md +191 -67
- package/package.json +2 -2
- package/src/abideLsp.ts +8 -4
- package/src/abideResolverPlugin.ts +17 -2
- package/src/build.ts +9 -0
- package/src/buildDisconnected.ts +2 -0
- package/src/devEntry.ts +44 -9
- package/src/lib/bundle/probeAbideServer.ts +1 -1
- package/src/lib/cli/parseArgvForRpc.ts +25 -26
- package/src/lib/cli/runCli.ts +9 -24
- package/src/lib/cli/tokenizeArgvFlags.ts +84 -0
- package/src/lib/cli/types/CliManifest.ts +6 -4
- package/src/lib/mcp/annotationsForMethod.ts +1 -1
- package/src/lib/mcp/toolResultFromResponse.ts +1 -1
- package/src/lib/mcp/types/JsonRpcRequest.ts +3 -3
- package/src/lib/server/AppModule.ts +1 -1
- package/src/lib/server/cli/handleCliDownload.ts +7 -5
- package/src/lib/server/cli/installScript.ts +3 -3
- package/src/lib/server/prompts/promptRegistry.ts +1 -1
- package/src/lib/server/prompts/types/Prompt.ts +2 -1
- package/src/lib/server/rpc/defineRpc.ts +9 -1
- package/src/lib/server/rpc/dispatchRpcInProcess.ts +2 -2
- package/src/lib/server/rpc/types/RpcHelper.ts +1 -1
- package/src/lib/server/runtime/createPublicAssetServer.ts +2 -2
- package/src/lib/server/runtime/createRouteDispatcher.ts +1 -1
- package/src/lib/server/runtime/createServer.ts +50 -12
- package/src/lib/server/runtime/createUiPageRenderer.ts +7 -11
- package/src/lib/server/runtime/crossOriginForbidden.ts +2 -2
- package/src/lib/server/runtime/crossOriginGate.ts +1 -1
- package/src/lib/server/runtime/devHotModuleResponse.ts +3 -3
- package/src/lib/server/runtime/disableIdleTimeoutForStream.ts +1 -1
- package/src/lib/server/runtime/gzipResponse.ts +14 -16
- package/src/lib/server/runtime/logExposedSurfaces.ts +3 -4
- package/src/lib/server/runtime/runWithRequestScope.ts +4 -1
- package/src/lib/server/runtime/serializeCacheSnapshot.ts +3 -8
- package/src/lib/server/runtime/snapshotEntryFromCache.ts +10 -7
- package/src/lib/server/runtime/streamFromIterator.ts +2 -2
- package/src/lib/server/runtime/types/InspectorRpc.ts +2 -2
- package/src/lib/server/runtime/types/RequestStore.ts +16 -0
- package/src/lib/server/runtime/withResponseDefaults.ts +8 -3
- package/src/lib/server/socket.ts +1 -1
- package/src/lib/server/sockets/createSocketDispatcher.ts +2 -2
- package/src/lib/server/sockets/defineSocket.ts +5 -1
- package/src/lib/server/sockets/socketRegistry.ts +2 -2
- package/src/lib/server/sockets/types/SocketClientFrame.ts +3 -3
- package/src/lib/shared/CACHE_WRAPPED.ts +3 -2
- package/src/lib/shared/REMOTE_FUNCTION.ts +1 -1
- package/src/lib/shared/RPC_ARGS_TYPE.ts +7 -0
- package/src/lib/shared/STREAMING_CONTENT_TYPES.ts +3 -2
- package/src/lib/shared/activeCacheStore.ts +5 -14
- package/src/lib/shared/activePage.ts +5 -19
- package/src/lib/shared/augmentModule.ts +18 -0
- package/src/lib/shared/basePath.ts +6 -5
- package/src/lib/shared/baseResolver.ts +10 -0
- package/src/lib/shared/baseSlot.ts +6 -12
- package/src/lib/shared/bodyValueForKind.ts +35 -0
- package/src/lib/shared/cache.ts +22 -2
- package/src/lib/shared/cacheEntryFromSnapshot.ts +23 -18
- package/src/lib/shared/cacheStoreResolver.ts +12 -0
- package/src/lib/shared/cacheStoreSlot.ts +5 -13
- package/src/lib/shared/changeAffectsClient.ts +35 -0
- package/src/lib/shared/createPushIterator.ts +1 -1
- package/src/lib/shared/createResolverSlot.ts +37 -0
- package/src/lib/shared/debugGate.ts +90 -0
- package/src/lib/shared/decodeResponse.ts +11 -8
- package/src/lib/shared/emitLogRecord.ts +18 -4
- package/src/lib/shared/globalCacheStoreResolver.ts +12 -0
- package/src/lib/shared/globalCacheStoreSlot.ts +6 -11
- package/src/lib/shared/hasReplayableRequest.ts +17 -0
- package/src/lib/shared/html.ts +7 -26
- package/src/lib/shared/isDebugEnabled.ts +3 -10
- package/src/lib/shared/isDebugNegated.ts +4 -6
- package/src/lib/shared/manifestModule.ts +2 -2
- package/src/lib/shared/online.ts +1 -1
- package/src/lib/shared/outboxProbeSlot.ts +1 -1
- package/src/lib/shared/pageResolver.ts +16 -0
- package/src/lib/shared/pageSlot.ts +5 -14
- package/src/lib/shared/pageUrlForFile.ts +1 -1
- package/src/lib/shared/requestScopeResolver.ts +12 -0
- package/src/lib/shared/requestScopeSlot.ts +7 -12
- package/src/lib/shared/responseBodyKind.ts +35 -0
- package/src/lib/shared/routeParamsShape.ts +20 -0
- package/src/lib/shared/setBaseResolver.ts +2 -4
- package/src/lib/shared/setCacheStoreResolver.ts +3 -5
- package/src/lib/shared/setGlobalCacheStoreResolver.ts +3 -5
- package/src/lib/shared/setPageResolver.ts +2 -5
- package/src/lib/shared/setRequestScopeResolver.ts +3 -5
- package/src/lib/shared/snapshotShippable.ts +22 -0
- package/src/lib/shared/types/CacheEntry.ts +1 -1
- package/src/lib/shared/types/CacheInvalidation.ts +3 -3
- package/src/lib/shared/types/ClientFlags.ts +5 -3
- package/src/lib/shared/types/DebugGate.ts +10 -0
- package/src/lib/shared/types/LastConnection.ts +1 -1
- package/src/lib/shared/types/ResolverSlot.ts +10 -0
- package/src/lib/shared/url.ts +4 -1
- package/src/lib/shared/withBase.ts +1 -2
- package/src/lib/shared/writeDts.ts +10 -3
- package/src/lib/shared/writePublicAssetsDts.ts +5 -9
- package/src/lib/shared/writeRoutesDts.ts +14 -46
- package/src/lib/shared/writeRpcDts.ts +13 -12
- package/src/lib/shared/writeTestRpcDts.ts +11 -11
- package/src/lib/shared/writeTestSocketsDts.ts +9 -9
- package/src/lib/test/createTestSocketChannel.ts +3 -3
- package/src/lib/ui/COMPONENT_WRAPPER_PREFIX.ts +1 -1
- package/src/lib/ui/compile/ABIDE_SEMANTIC_TOKENS_LEGEND.ts +40 -6
- package/src/lib/ui/compile/REACTIVE_CALLEES.ts +1 -1
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +49 -37
- package/src/lib/ui/compile/abideUiPlugin.ts +10 -8
- package/src/lib/ui/compile/assertRuntimeHelpersBound.ts +27 -13
- package/src/lib/ui/compile/awaitPlan.ts +14 -2
- package/src/lib/ui/compile/catchBinding.ts +8 -0
- package/src/lib/ui/compile/classStyleMergePlan.ts +108 -0
- package/src/lib/ui/compile/collectAbideDiagnostics.ts +11 -0
- package/src/lib/ui/compile/compileComponent.ts +4 -4
- package/src/lib/ui/compile/compileModule.ts +56 -21
- package/src/lib/ui/compile/compileSSR.ts +12 -10
- package/src/lib/ui/compile/compileShadow.ts +163 -12
- package/src/lib/ui/compile/composeProps.ts +3 -3
- package/src/lib/ui/compile/createShadowLanguageService.ts +58 -4
- package/src/lib/ui/compile/createShadowProgram.ts +2 -1
- package/src/lib/ui/compile/createShadowScope.ts +42 -0
- package/src/lib/ui/compile/decodeHtmlEntities.ts +4 -0
- package/src/lib/ui/compile/desugarSignals.ts +12 -7
- package/src/lib/ui/compile/eachPlan.ts +61 -0
- package/src/lib/ui/compile/elementPlan.ts +55 -0
- package/src/lib/ui/compile/encodeSemanticTokens.ts +24 -1
- package/src/lib/ui/compile/generateBuild.ts +156 -127
- package/src/lib/ui/compile/generateSSR.ts +112 -73
- package/src/lib/ui/compile/ifPlan.ts +7 -1
- package/src/lib/ui/compile/interpolatedTemplateLiteral.ts +25 -0
- package/src/lib/ui/compile/isPlainIdentifier.ts +7 -0
- package/src/lib/ui/compile/isTextLeaf.ts +8 -7
- package/src/lib/ui/compile/lowerContext.ts +36 -46
- package/src/lib/ui/compile/lowerDocAccess.ts +2 -2
- package/src/lib/ui/compile/lowerScript.ts +1 -1
- package/src/lib/ui/compile/markupTokens.ts +313 -0
- package/src/lib/ui/compile/pagePropsType.ts +23 -0
- package/src/lib/ui/compile/parseTemplate.ts +135 -62
- package/src/lib/ui/compile/reactiveBinding.ts +47 -0
- package/src/lib/ui/compile/renameSignalRefs.ts +44 -10
- package/src/lib/ui/compile/skeletonable.ts +5 -5
- package/src/lib/ui/compile/snippetPlan.ts +28 -0
- package/src/lib/ui/compile/spreadExcludedNames.ts +4 -4
- package/src/lib/ui/compile/structuralBlockTokens.ts +3 -4
- package/src/lib/ui/compile/switchPlan.ts +7 -1
- package/src/lib/ui/compile/tryPlan.ts +8 -1
- package/src/lib/ui/compile/types/AnalyzedComponent.ts +2 -2
- package/src/lib/ui/compile/types/Binding.ts +24 -0
- package/src/lib/ui/compile/types/CompiledShadow.ts +13 -1
- package/src/lib/ui/compile/types/ElementPlan.ts +43 -0
- package/src/lib/ui/compile/types/ShadowKind.ts +9 -0
- package/src/lib/ui/compile/types/ShadowScope.ts +19 -0
- package/src/lib/ui/compile/types/TemplateAttr.ts +7 -0
- package/src/lib/ui/compile/types/TemplateNode.ts +9 -9
- package/src/lib/ui/compile/withBindings.ts +45 -0
- package/src/lib/ui/createScope.ts +2 -2
- package/src/lib/ui/dom/MATHML_NAMESPACE.ts +2 -2
- package/src/lib/ui/dom/SVG_NAMESPACE.ts +3 -2
- package/src/lib/ui/dom/appendText.ts +1 -1
- package/src/lib/ui/dom/applyResolved.ts +1 -1
- package/src/lib/ui/dom/buildDetachedRange.ts +4 -3
- package/src/lib/ui/dom/cloneStatic.ts +3 -2
- package/src/lib/ui/dom/each.ts +26 -7
- package/src/lib/ui/dom/foreignWrapperTag.ts +2 -2
- package/src/lib/ui/dom/mountChild.ts +8 -1
- package/src/lib/ui/dom/mountSwappableRange.ts +79 -0
- package/src/lib/ui/dom/readCall.ts +6 -4
- package/src/lib/ui/dom/scopeLabel.ts +3 -3
- package/src/lib/ui/dom/skeleton.ts +10 -1
- package/src/lib/ui/dom/switchBlock.ts +17 -63
- package/src/lib/ui/dom/types/EachRow.ts +4 -0
- package/src/lib/ui/dom/types/SkeletonHoles.ts +4 -2
- package/src/lib/ui/dom/when.ts +11 -64
- package/src/lib/ui/history.ts +1 -2
- package/src/lib/ui/html.ts +28 -0
- package/src/lib/ui/installHotBridge.ts +0 -2
- package/src/lib/ui/renderChain.ts +16 -3
- package/src/lib/ui/router.ts +24 -3
- package/src/lib/ui/rpcOutbox/createOutboxQueue.ts +1 -1
- package/src/lib/ui/runtime/CHILD_PRESENT.ts +8 -0
- package/src/lib/ui/runtime/OUTLET_MARKER.ts +2 -2
- package/src/lib/ui/runtime/RANGE_MARKER.ts +1 -1
- package/src/lib/ui/runtime/REACTIVE_CONTEXT.ts +9 -3
- package/src/lib/ui/runtime/captureModelDoc.ts +1 -1
- package/src/lib/ui/runtime/createDoc.ts +107 -13
- package/src/lib/ui/runtime/createEffectNode.ts +5 -5
- package/src/lib/ui/runtime/flushEffects.ts +22 -9
- package/src/lib/ui/runtime/historyEntries.ts +1 -1
- package/src/lib/ui/runtime/track.ts +4 -2
- package/src/lib/ui/runtime/trigger.ts +34 -20
- package/src/lib/ui/runtime/types/Route.ts +2 -2
- package/src/lib/ui/runtime/types/UiComponent.ts +3 -2
- package/src/lib/ui/runtime/types/UiProps.ts +9 -12
- package/src/lib/ui/runtime/walkPath.ts +15 -3
- package/src/lib/ui/startClient.ts +41 -14
- package/src/lib/ui/state.ts +3 -3
- package/src/lib/ui/types/Scope.ts +3 -3
- package/src/serverBuildPlugins.ts +11 -5
- package/src/zodCjsPlugin.ts +51 -0
- package/template/.zed/settings.json +7 -1
- package/template/package.json +3 -0
- package/template/src/ui/pages/layout.abide +3 -5
- package/src/lib/shared/matchesDebugPattern.ts +0 -16
- package/src/lib/shared/parseDebugPatterns.ts +0 -21
|
@@ -1,17 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import type { ResponseBodyKind } from '../../shared/responseBodyKind.ts'
|
|
2
|
+
import { responseBodyKind } from '../../shared/responseBodyKind.ts'
|
|
3
3
|
import { acceptsGzip } from './acceptsGzip.ts'
|
|
4
4
|
import { flushingGzipStream } from './flushingGzipStream.ts'
|
|
5
5
|
import { STREAMED_HTML_HEADER } from './STREAMED_HTML_HEADER.ts'
|
|
6
6
|
|
|
7
|
-
/*
|
|
8
|
-
Compressible Content-Types — text and structured-text payloads. Binary or
|
|
9
|
-
already-compressed bodies (images, fonts, archives, zstd/gzip blobs) gain
|
|
10
|
-
nothing from a second pass and only burn CPU.
|
|
11
|
-
*/
|
|
12
|
-
const COMPRESSIBLE_TYPE =
|
|
13
|
-
/^(?:text\/|application\/(?:json|javascript|xml|[\w.-]+\+(?:json|xml))|image\/svg)/
|
|
14
|
-
|
|
15
7
|
/*
|
|
16
8
|
Gzips a dynamic response (SSR HTML, rpc/json replies, the plain 404) when the
|
|
17
9
|
client accepts it. Static assets never reach here — they already carry a
|
|
@@ -23,6 +15,12 @@ expose a string body's length before send, and measuring would mean buffering th
|
|
|
23
15
|
body — the framing overhead on the rare tiny body is negligible against
|
|
24
16
|
compressing every page and rpc payload.
|
|
25
17
|
|
|
18
|
+
`kind` is the body class the dispatch pipeline already computed (S2: classify
|
|
19
|
+
once, thread it in) — only `compressible` bodies gzip; `streaming` and `opaque`
|
|
20
|
+
pass through. Standalone callers (the health probe) omit it and it's derived
|
|
21
|
+
here. The streamed-HTML marker is handled independent of `kind` since it's
|
|
22
|
+
stripped even on a skip path.
|
|
23
|
+
|
|
26
24
|
Buffered bodies take the web CompressionStream (best ratio, one flush at close).
|
|
27
25
|
The streamed SSR document self-marks (STREAMED_HTML_HEADER) and takes a
|
|
28
26
|
per-chunk-flushing gzip instead: the plain CompressionStream buffers the head
|
|
@@ -30,7 +28,7 @@ until its deflate window fills, which defeats streaming (the browser can't
|
|
|
30
28
|
preload-scan the head or paint the pending shell until the stream nearly closes).
|
|
31
29
|
The marker is stripped so it never reaches the client.
|
|
32
30
|
*/
|
|
33
|
-
export function gzipResponse(req: Request, response: Response): Response {
|
|
31
|
+
export function gzipResponse(req: Request, response: Response, kind?: ResponseBodyKind): Response {
|
|
34
32
|
const streamedHtml = response.headers.has(STREAMED_HTML_HEADER)
|
|
35
33
|
if (streamedHtml) {
|
|
36
34
|
response.headers.delete(STREAMED_HTML_HEADER)
|
|
@@ -38,11 +36,11 @@ export function gzipResponse(req: Request, response: Response): Response {
|
|
|
38
36
|
if (!response.body || response.headers.has('Content-Encoding')) {
|
|
39
37
|
return response
|
|
40
38
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
if (!
|
|
39
|
+
/* A streamed-HTML document classifies as `compressible` (text/html) but the
|
|
40
|
+
marker was already stripped above, so re-derive after the strip when the
|
|
41
|
+
pipeline didn't hand a kind in. */
|
|
42
|
+
const bodyKind = kind ?? responseBodyKind(response)
|
|
43
|
+
if (!acceptsGzip(req) || bodyKind !== 'compressible') {
|
|
46
44
|
return response
|
|
47
45
|
}
|
|
48
46
|
const headers = new Headers(response.headers)
|
|
@@ -83,7 +83,7 @@ exposed on, so abide's routing and multimodal-by-default exposure are auditable
|
|
|
83
83
|
rather than implicit. Three aligned tables — scan a column to spot a missing
|
|
84
84
|
surface, a row to see one declaration's reach:
|
|
85
85
|
|
|
86
|
-
- pages: each route
|
|
86
|
+
- pages: each route key.
|
|
87
87
|
- sockets: name + client surfaces (schema/browser/mcp/cli) and whether clients
|
|
88
88
|
may publish.
|
|
89
89
|
- rpcs: method+path (headed `http` since http/openapi are unconditional) +
|
|
@@ -95,9 +95,8 @@ then the surface columns, so the glyphs line up vertically across the tables.
|
|
|
95
95
|
rpc folds its method into a left-aligned prefix of the identifier cell, so paths
|
|
96
96
|
still start at a shared column. For sockets and rpcs the `schema`
|
|
97
97
|
column leads: it's what unlocks the non-browser surfaces, so a missing schema
|
|
98
|
-
reddens to flag the gated declaration. Loads the full registry
|
|
99
|
-
|
|
100
|
-
eager imports in production. Best-effort: enumeration failures are swallowed,
|
|
98
|
+
reddens to flag the gated declaration. Loads the full registry. Runs once at boot; suppressed only via DEBUG=-abide
|
|
99
|
+
(the always-on logging opt-out). Best-effort: enumeration failures are swallowed,
|
|
101
100
|
this is diagnostic only.
|
|
102
101
|
*/
|
|
103
102
|
export async function logExposedSurfaces(routing: { pages: Pages }): Promise<void> {
|
|
@@ -120,7 +120,10 @@ export function runWithRequestScope(
|
|
|
120
120
|
})
|
|
121
121
|
})
|
|
122
122
|
}
|
|
123
|
-
|
|
123
|
+
/* Reuse the dispatch pipeline's single S2 classification when it stashed
|
|
124
|
+
one; callers that don't (mcp, the fetch fallback, tests) classify here. */
|
|
125
|
+
const streaming = store.responseStreaming ?? isStreamingResponse(response)
|
|
126
|
+
if (response.body && streaming) {
|
|
124
127
|
/*
|
|
125
128
|
A hand-pumped wrap rather than a TransformStream: streams typically
|
|
126
129
|
end by client disconnect (an SSE tab closing), which cancels the
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { snapshotShippable } from '../../shared/snapshotShippable.ts'
|
|
2
2
|
import type { CacheSnapshotEntry } from '../../shared/types/CacheSnapshotEntry.ts'
|
|
3
3
|
import type { CacheStore } from '../../shared/types/CacheStore.ts'
|
|
4
4
|
import { snapshotEntryFromCache } from './snapshotEntryFromCache.ts'
|
|
5
5
|
|
|
6
6
|
/*
|
|
7
7
|
Snapshots the request-scoped cache for SSR at a single instant: every replayable
|
|
8
|
-
(GET
|
|
8
|
+
(GET-only) entry settled by now, serialized to a wire-safe CacheSnapshotEntry the
|
|
9
9
|
client seeds its store from. Unsettled and non-replayable entries are skipped; a body
|
|
10
10
|
that can't ship (binary / streaming / rejected) drops out via snapshotEntryFromCache.
|
|
11
11
|
|
|
@@ -21,12 +21,7 @@ over the wire — see its post-stream `__abideResolve` pass. So for a streaming
|
|
|
21
21
|
render-return snapshot is typically empty; the warm cache arrives over the stream.
|
|
22
22
|
*/
|
|
23
23
|
export async function serializeCacheSnapshot(store: CacheStore): Promise<CacheSnapshotEntry[]> {
|
|
24
|
-
const settled = Array.from(store.entries.values()).filter(
|
|
25
|
-
(entry) =>
|
|
26
|
-
entry.settled === true &&
|
|
27
|
-
entry.request !== undefined &&
|
|
28
|
-
isReplayableMethod(entry.request.method.toUpperCase()),
|
|
29
|
-
)
|
|
24
|
+
const settled = Array.from(store.entries.values()).filter(snapshotShippable)
|
|
30
25
|
const snapshots = await Promise.all(
|
|
31
26
|
settled.map((entry) => snapshotEntryFromCache(store, entry)),
|
|
32
27
|
)
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { contentTypeOf } from '../../shared/contentTypeOf.ts'
|
|
2
|
-
import {
|
|
2
|
+
import { hasReplayableRequest } from '../../shared/hasReplayableRequest.ts'
|
|
3
3
|
import { isStreamingResponse } from '../../shared/isStreamingResponse.ts'
|
|
4
4
|
import type { CacheEntry } from '../../shared/types/CacheEntry.ts'
|
|
5
5
|
import type { CacheSnapshotEntry } from '../../shared/types/CacheSnapshotEntry.ts'
|
|
6
6
|
import type { CacheStore } from '../../shared/types/CacheStore.ts'
|
|
7
|
+
import type { ReplayableMethod } from '../../shared/types/ReplayableMethod.ts'
|
|
7
8
|
|
|
8
9
|
/*
|
|
9
10
|
Awaits one cache entry and turns it into a wire-safe snapshot, or undefined
|
|
@@ -25,14 +26,16 @@ export async function snapshotEntryFromCache(
|
|
|
25
26
|
store: CacheStore,
|
|
26
27
|
entry: CacheEntry,
|
|
27
28
|
): Promise<CacheSnapshotEntry | undefined> {
|
|
28
|
-
/*
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (!isReplayableMethod(method)) {
|
|
29
|
+
/* The request half of the shared shippability gate: a replayable wire request.
|
|
30
|
+
Producer entries (no request) and non-replayable methods never snapshot — the
|
|
31
|
+
streaming-drain caller hands still-pending entries here, so `settled` is NOT gated
|
|
32
|
+
(the caller's snapshotShippable filter already required it for the entries it picks). */
|
|
33
|
+
if (!hasReplayableRequest(entry) || !entry.request) {
|
|
34
34
|
return undefined
|
|
35
35
|
}
|
|
36
|
+
/* `hasReplayableRequest` already verified the uppercased method is replayable; assert it
|
|
37
|
+
for the snapshot's typed `method` field rather than re-running the same Set check. */
|
|
38
|
+
const method = entry.request.method.toUpperCase() as ReplayableMethod
|
|
36
39
|
const response = await readSettled(entry.promise as Promise<Response>)
|
|
37
40
|
if (!response) {
|
|
38
41
|
return undefined
|
|
@@ -4,7 +4,7 @@ import { messageFromError } from '../../shared/messageFromError.ts'
|
|
|
4
4
|
Shared body builder for the streaming respond helpers (`jsonl`, `sse`).
|
|
5
5
|
Both flow the same shape — pull from an AsyncIterator, encode each frame
|
|
6
6
|
to bytes, emit a sentinel `error` frame on a generator throw, and route
|
|
7
|
-
ReadableStream's `cancel` into `
|
|
7
|
+
ReadableStream's `cancel` into `iterator.return()` so the handler's
|
|
8
8
|
`for await` exits via its normal control path. Only the per-frame
|
|
9
9
|
encoding and the optional keepalive payload differ between the two.
|
|
10
10
|
|
|
@@ -85,7 +85,7 @@ export function streamFromIterator<T>(
|
|
|
85
85
|
rejection from its cleanup: a `finally` that throws on `.return()`
|
|
86
86
|
would otherwise surface as an unhandled rejection (process-fatal
|
|
87
87
|
under Bun's default) on a client disconnect — a path every
|
|
88
|
-
sse/jsonl
|
|
88
|
+
sse/jsonl stream hits routinely.
|
|
89
89
|
*/
|
|
90
90
|
return iterator.return?.(reason)?.then(
|
|
91
91
|
() => undefined,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
|
-
One RPC
|
|
3
|
-
serializable facts the UI renders — where it mounts, its method, which
|
|
2
|
+
One declared RPC projected for the inspector: the registry entry reduced to
|
|
3
|
+
the serializable facts the UI renders — where it mounts, its method, which
|
|
4
4
|
non-browser surfaces advertise it, and its argument/result shapes as JSON
|
|
5
5
|
Schema. Schemas project through jsonSchemaForSchema (same as MCP/OpenAPI), so
|
|
6
6
|
a rpc whose library can't render a schema still lists with an opaque shape.
|
|
@@ -52,4 +52,20 @@ export type RequestStore = {
|
|
|
52
52
|
carried no file parts.
|
|
53
53
|
*/
|
|
54
54
|
files?: Record<string, File[]>
|
|
55
|
+
/*
|
|
56
|
+
The allowlisted headers forwarded onto in-process rpc Requests during this
|
|
57
|
+
request's SSR pass — derived once from the inbound headers (which don't change
|
|
58
|
+
within the scope) and reused by every defineRpc call. Each call clones it,
|
|
59
|
+
since buildRpcRequest mutates the Headers (content-type/ref-json) before
|
|
60
|
+
constructing the Request. Undefined until the first in-process rpc.
|
|
61
|
+
*/
|
|
62
|
+
forwardedHeaders?: Headers
|
|
63
|
+
/*
|
|
64
|
+
The body class of the response the dispatch pipeline produced, stashed so
|
|
65
|
+
runWithRequestScope's closing-record stream monitor reuses the single S2
|
|
66
|
+
classification instead of re-deriving from the Content-Type. Undefined for
|
|
67
|
+
callers that don't classify (mcp, the fetch fallback, tests) — those fall
|
|
68
|
+
back to classifying the response themselves.
|
|
69
|
+
*/
|
|
70
|
+
responseStreaming?: boolean
|
|
55
71
|
}
|
|
@@ -17,8 +17,13 @@ export function withResponseDefaults(
|
|
|
17
17
|
status?: number,
|
|
18
18
|
): ResponseInit {
|
|
19
19
|
const headers = new Headers(defaultHeaders)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
/* The dominant case carries no overrides — skip the second Headers
|
|
21
|
+
allocation + per-key copy and ship the defaults alone. Only merge when
|
|
22
|
+
the caller actually supplied headers. */
|
|
23
|
+
if (init?.headers !== undefined) {
|
|
24
|
+
new Headers(init.headers).forEach((value, key) => {
|
|
25
|
+
headers.set(key, value)
|
|
26
|
+
})
|
|
27
|
+
}
|
|
23
28
|
return { ...init, headers, ...(status !== undefined && { status }) }
|
|
24
29
|
}
|
package/src/lib/server/socket.ts
CHANGED
|
@@ -6,7 +6,6 @@ import type { SocketOptions } from './sockets/types/SocketOptions.ts'
|
|
|
6
6
|
Declares a Socket inside a file under `src/server/sockets/`. Each file contains
|
|
7
7
|
exactly one export, named after the file (e.g. `chat.ts` →
|
|
8
8
|
`export const chat = socket<ChatMessage>(...)`). The bundler reads the
|
|
9
|
-
// @documentation sockets
|
|
10
9
|
export name from the filename and the socket name from the file path
|
|
11
10
|
under `src/server/sockets/`, then rewrites this call to bind the name into the
|
|
12
11
|
runtime implementation (defineSocket on the server, socketProxy on the
|
|
@@ -21,6 +20,7 @@ to browser-only when schemaless, all surfaces when a schema is present.
|
|
|
21
20
|
This function exists only for the type signature; calling it directly
|
|
22
21
|
means the bundler plugin didn't process the file, which throws.
|
|
23
22
|
*/
|
|
23
|
+
// @documentation sockets
|
|
24
24
|
export function socket<Schema extends StandardSchemaV1>(
|
|
25
25
|
opts: SocketOptions<Schema> & { schema: Schema },
|
|
26
26
|
): Socket<StandardSchemaV1.InferOutput<Schema>>
|
|
@@ -220,7 +220,7 @@ export function createSocketDispatcher(sockets: SocketRoutes): SocketDispatcher
|
|
|
220
220
|
}
|
|
221
221
|
/*
|
|
222
222
|
publish() runs the topic's optional Standard Schema synchronously
|
|
223
|
-
and throws on failure (see defineSocket.
|
|
223
|
+
and throws on failure (see validateSync in defineSocket.ts). The
|
|
224
224
|
dispatcher invokes us via `void handlePub(...)`, so an unhandled
|
|
225
225
|
throw would surface as an unhandled promise rejection on every
|
|
226
226
|
malformed client frame. Catch + log so a buggy client can't take
|
|
@@ -317,7 +317,7 @@ export function createSocketDispatcher(sockets: SocketRoutes): SocketDispatcher
|
|
|
317
317
|
try {
|
|
318
318
|
/* ref-json from abide's own client; a non-abide client sends a plain-JSON
|
|
319
319
|
frame, which decodeRefJson rejects (a frame is always an object, never the
|
|
320
|
-
`[
|
|
320
|
+
`[rootValue, slots]` envelope) — fall back to JSON.parse so raw WS clients work. */
|
|
321
321
|
try {
|
|
322
322
|
frame = decodeRefJson(text) as SocketClientFrame
|
|
323
323
|
} catch {
|
|
@@ -119,7 +119,11 @@ export function defineSocket<T>(name: string, opts: SocketOptions = {}): Socket<
|
|
|
119
119
|
cachedServer = getActiveServer()
|
|
120
120
|
}
|
|
121
121
|
const server = cachedServer
|
|
122
|
-
|
|
122
|
+
/* Remote fan-out is the only consumer of the encoded frame: in-process iterators
|
|
123
|
+
already received the live object via notify() above. Skip the full message-graph
|
|
124
|
+
walk (encodeRefJson) + native publish when zero ws clients are subscribed to the
|
|
125
|
+
topic — subscriberCount is a cheap C-side check, encodeRefJson is not. */
|
|
126
|
+
if (server && server.subscriberCount(topic) > 0) {
|
|
123
127
|
/* Typed against the shared wire contract so a `SocketServerFrame` change can't
|
|
124
128
|
silently drift from this construction site (the dispatcher's `send` is already
|
|
125
129
|
typed; this was the last `msg` frame built through an unchecked JSON.stringify). */
|
|
@@ -3,7 +3,7 @@ import type { SocketRegistryEntry } from './types/SocketRegistryEntry.ts'
|
|
|
3
3
|
/*
|
|
4
4
|
Process-wide registry of every Socket declared in the app. defineSocket
|
|
5
5
|
inserts on first construction; the dispatcher reads on every `sub` /
|
|
6
|
-
`pub` frame so it can find the right Socket by
|
|
7
|
-
opted-in `allowClientPublish` policy.
|
|
6
|
+
`pub` frame and every REST request so it can find the right Socket by
|
|
7
|
+
name and check the opted-in `allowClientPublish` policy.
|
|
8
8
|
*/
|
|
9
9
|
export const socketRegistry = new Map<string, SocketRegistryEntry>()
|
|
@@ -9,9 +9,9 @@ the dispatcher checks the topic's `clientPublish` flag before fanning
|
|
|
9
9
|
out.
|
|
10
10
|
|
|
11
11
|
`sub` is the per-subscription id minted client-side; the server treats
|
|
12
|
-
it as opaque and routes inbound `
|
|
13
|
-
id
|
|
14
|
-
|
|
12
|
+
it as opaque and routes inbound `replay|err|end` frames back to the same
|
|
13
|
+
id — `msg` frames are broadcast keyed by socket name; the client
|
|
14
|
+
demuxes them against every local sub for that socket.
|
|
15
15
|
*/
|
|
16
16
|
export type SocketClientFrame =
|
|
17
17
|
| { type: 'sub'; sub: string; socket: string; replay?: number }
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
Brand on the invoker cache() returns; its property value is the wrapped fn,
|
|
3
3
|
so inspection shows what a wrapper wraps. Detection is certain (set by us,
|
|
4
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
|
-
re-wrapped wrapper silently
|
|
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).
|
|
7
8
|
*/
|
|
8
9
|
export const CACHE_WRAPPED: unique symbol = Symbol('abide.cacheWrapped')
|
|
@@ -3,6 +3,6 @@ Brand set by createRemoteFunction on both the decoded callable and its raw
|
|
|
3
3
|
sibling — the exact answer to "is this a abide remote?" where structural
|
|
4
4
|
checks ('url' in fn) would misclassify any user function that happens to
|
|
5
5
|
carry a url property. Read by cache()'s remote/producer discriminator,
|
|
6
|
-
|
|
6
|
+
selectorPrefix's identity branch, and the route dispatcher's export scan.
|
|
7
7
|
*/
|
|
8
8
|
export const REMOTE_FUNCTION: unique symbol = Symbol('abide.remoteFunction')
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/*
|
|
2
|
+
The `RpcArgs<Fn>` type alias both rpc d.ts writers emit verbatim: it lifts the
|
|
3
|
+
args type out of a RemoteFunction, dropping the FormData upload variant so a
|
|
4
|
+
url()/test-client call types against the rpc's plain args.
|
|
5
|
+
*/
|
|
6
|
+
export const RPC_ARGS_TYPE =
|
|
7
|
+
'type RpcArgs<Fn> = Fn extends (args: infer Args) => unknown ? Exclude<Args, FormData> : never'
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/*
|
|
2
2
|
Content-Type prefixes abide treats as streaming bodies — SSE for the
|
|
3
3
|
`sse()` helper, JSONL / NDJSON for the `jsonl()` helper. Used by
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
contentBodyKind (called by decodeResponse and the one-shot path in
|
|
5
|
+
streamResponse) to classify content, and by streamResponse to select the
|
|
6
|
+
JSONL frame parser.
|
|
6
7
|
*/
|
|
7
8
|
export const STREAMING_CONTENT_TYPES = [
|
|
8
9
|
'text/event-stream',
|
|
@@ -1,20 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createCacheStore } from './createCacheStore.ts'
|
|
1
|
+
import { cacheStoreResolver } from './cacheStoreResolver.ts'
|
|
3
2
|
import type { CacheStore } from './types/CacheStore.ts'
|
|
4
3
|
|
|
5
4
|
/*
|
|
6
|
-
Resolves the active CacheStore
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
a single fallback store is created lazily so isolated tests still work.
|
|
5
|
+
Resolves the active CacheStore: the registered resolver's store, or a single
|
|
6
|
+
lazily-created fallback when none is registered (so isolated tests work). The
|
|
7
|
+
fallback creator guarantees a value, hence the non-null assertion.
|
|
10
8
|
*/
|
|
11
9
|
export function activeCacheStore(): CacheStore {
|
|
12
|
-
|
|
13
|
-
if (fromResolver) {
|
|
14
|
-
return fromResolver
|
|
15
|
-
}
|
|
16
|
-
if (!cacheStoreSlot.fallback) {
|
|
17
|
-
cacheStoreSlot.fallback = createCacheStore()
|
|
18
|
-
}
|
|
19
|
-
return cacheStoreSlot.fallback
|
|
10
|
+
return cacheStoreResolver.get()!
|
|
20
11
|
}
|
|
@@ -1,25 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { pageResolver } from './pageResolver.ts'
|
|
2
2
|
import type { PageSnapshot } from './types/PageSnapshot.ts'
|
|
3
3
|
|
|
4
4
|
/*
|
|
5
|
-
Resolves the active page snapshot
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
fallback snapshot is created lazily so isolated tests still work. Mirrors
|
|
9
|
-
activeCacheStore.
|
|
5
|
+
Resolves the active page snapshot: the registered resolver's snapshot, or a
|
|
6
|
+
single lazily-created empty snapshot when none is registered (so isolated tests
|
|
7
|
+
work). The fallback creator guarantees a value, hence the non-null assertion.
|
|
10
8
|
*/
|
|
11
9
|
export function activePage(): PageSnapshot {
|
|
12
|
-
|
|
13
|
-
if (fromResolver) {
|
|
14
|
-
return fromResolver
|
|
15
|
-
}
|
|
16
|
-
if (!pageSlot.fallback) {
|
|
17
|
-
pageSlot.fallback = {
|
|
18
|
-
route: '',
|
|
19
|
-
params: {},
|
|
20
|
-
url: new URL('http://localhost/'),
|
|
21
|
-
navigating: false,
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
return pageSlot.fallback
|
|
10
|
+
return pageResolver.get()!
|
|
25
11
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Emits one `declare module '<module>' { interface <iface> { … } }` block, the
|
|
3
|
+
module-augmentation shape every codegen writer (routes, rpc, publicAssets,
|
|
4
|
+
testRpc, testSockets) shares. `entries` is `[key, type]` pairs already in final
|
|
5
|
+
order; each becomes an 8-space-indented `"key": type` member, the indentation
|
|
6
|
+
the consumer src tsconfig include expects. Returns the block body only — the
|
|
7
|
+
writeDts envelope adds banner + footer.
|
|
8
|
+
*/
|
|
9
|
+
export function augmentModule(module: string, iface: string, entries: [string, string][]): string {
|
|
10
|
+
const members = entries
|
|
11
|
+
.map(([key, type]) => ` ${JSON.stringify(key)}: ${type}`)
|
|
12
|
+
.join('\n')
|
|
13
|
+
return `declare module '${module}' {
|
|
14
|
+
interface ${iface} {
|
|
15
|
+
${members}
|
|
16
|
+
}
|
|
17
|
+
}`
|
|
18
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { baseResolver } from './baseResolver.ts'
|
|
2
2
|
|
|
3
3
|
/*
|
|
4
|
-
The current mount base path ('' at root). Resolved per side: the server
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
The current mount base path ('' at root). Resolved per side: the server installs
|
|
5
|
+
an APP_URL-derived resolver at boot, the client one reading window.__SSR__.base.
|
|
6
|
+
url() reads this to prefix rooted internal paths. Defaults to '' when no resolver
|
|
7
|
+
or fallback is set.
|
|
7
8
|
*/
|
|
8
9
|
export function basePath(): string {
|
|
9
|
-
return
|
|
10
|
+
return baseResolver.get() ?? ''
|
|
10
11
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
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,14 +1,8 @@
|
|
|
1
|
+
import { baseResolver } from './baseResolver.ts'
|
|
2
|
+
|
|
1
3
|
/*
|
|
2
|
-
Internal slot the runtime entries register their mount-base resolver into
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
used only when no resolver is registered — lets isolated tests set a base
|
|
6
|
-
without spinning up the runtime. Mirrors pageSlot / cacheStoreSlot.
|
|
4
|
+
Internal slot the runtime entries register their mount-base resolver into (see
|
|
5
|
+
baseResolver). Exposed so test helpers snapshot/poke `.resolver` and `.fallback`
|
|
6
|
+
directly.
|
|
7
7
|
*/
|
|
8
|
-
export const baseSlot
|
|
9
|
-
resolver: (() => string) | undefined
|
|
10
|
-
fallback: string | undefined
|
|
11
|
-
} = {
|
|
12
|
-
resolver: undefined,
|
|
13
|
-
fallback: undefined,
|
|
14
|
-
}
|
|
8
|
+
export const baseSlot = baseResolver.slot
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ContentBodyKind } from './contentBodyKind.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
The single body-kind → value mapping shared by the live read (decodeResponse) and
|
|
5
|
+
the synchronous warm read (warmValueFromSnapshot), so neither can disagree about
|
|
6
|
+
which kind yields what — the divergence behind the streaming-guard-in-one-but-not-
|
|
7
|
+
the-other bug.
|
|
8
|
+
|
|
9
|
+
Generic over the extraction (`Value`) so both async and sync callers reuse it:
|
|
10
|
+
the live read passes Promise-returning accessors (response.json()/text()) and
|
|
11
|
+
handles the deferred kinds (streaming → throw, binary → blob); the warm read
|
|
12
|
+
passes sync accessors (JSON.parse(body)/body) and treats every deferred kind as
|
|
13
|
+
no-warm-value, leaving the live async path to throw/blob exactly as a fresh call
|
|
14
|
+
would. The json/text branches — the only kinds that produce a directly-usable
|
|
15
|
+
value on both sides — are decided here, once.
|
|
16
|
+
|
|
17
|
+
Returns the DEFER sentinel for `streaming` and `binary`: kinds whose handling is
|
|
18
|
+
side-specific (the live read throws on streaming and blobs on binary; the warm
|
|
19
|
+
read has no synchronous equivalent for either), so the caller branches on it.
|
|
20
|
+
*/
|
|
21
|
+
export const DEFER: unique symbol = Symbol('defer')
|
|
22
|
+
|
|
23
|
+
export function bodyValueForKind<Value>(
|
|
24
|
+
kind: ContentBodyKind,
|
|
25
|
+
json: () => Value,
|
|
26
|
+
text: () => Value,
|
|
27
|
+
): Value | typeof DEFER {
|
|
28
|
+
if (kind === 'json') {
|
|
29
|
+
return json()
|
|
30
|
+
}
|
|
31
|
+
if (kind === 'text') {
|
|
32
|
+
return text()
|
|
33
|
+
}
|
|
34
|
+
return DEFER
|
|
35
|
+
}
|
package/src/lib/shared/cache.ts
CHANGED
|
@@ -92,7 +92,7 @@ request-scoped on the server, which keeps per-user data from leaking across
|
|
|
92
92
|
requests; on the client there is one tab store either way, so it is a no-op.
|
|
93
93
|
|
|
94
94
|
Reactivity is implicit: the invoker calls `store.subscribe(key)`, which
|
|
95
|
-
registers the surrounding
|
|
95
|
+
registers the surrounding scope().computed() / scope().effect() scope. Invalidating the key
|
|
96
96
|
then re-runs that scope, which calls cache() again and gets a fresh entry.
|
|
97
97
|
Outside a tracking scope subscribe() is a no-op, so cache() works the same
|
|
98
98
|
in server code and plain client code.
|
|
@@ -199,7 +199,7 @@ export function cache<Args, Return>(
|
|
|
199
199
|
fetch hands each reader a fresh object; cloning keeps warm reads the same.
|
|
200
200
|
*/
|
|
201
201
|
if (!isRaw && existing?.value !== undefined) {
|
|
202
|
-
return Promise.resolve(
|
|
202
|
+
return Promise.resolve(cloneWarmValue(existing.value)) as Promise<Return>
|
|
203
203
|
}
|
|
204
204
|
const responsePromise = invokeRemote(
|
|
205
205
|
store,
|
|
@@ -216,6 +216,26 @@ export function cache<Args, Return>(
|
|
|
216
216
|
return read
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
/*
|
|
220
|
+
Deep-copies a warm value so each reader gets its own mutable object — the
|
|
221
|
+
no-shared-mutation invariant the warm path turns on (a live fetch hands every
|
|
222
|
+
reader a fresh object; a warm read must match). A warm value only ever comes
|
|
223
|
+
from the json or text body kinds (bodyValueForKind): json yields JSON.parse
|
|
224
|
+
output and text yields a string, so the whole population is JSON-round-trippable
|
|
225
|
+
by construction — no Date/Map/Blob/cycle a structuredClone would be needed for.
|
|
226
|
+
A primitive (string/number/boolean from a text or scalar-json body) is immutable,
|
|
227
|
+
so it is returned as-is with no copy. An object/array goes through a JSON
|
|
228
|
+
round-trip, ~2x faster than structuredClone for this exact shape (measured:
|
|
229
|
+
773µs→516µs on a 149KB list, 3.4ms→1.7ms on 474KB) while producing the same
|
|
230
|
+
fresh, mutable, isomorphic-with-a-cold-read copy.
|
|
231
|
+
*/
|
|
232
|
+
function cloneWarmValue(value: unknown): unknown {
|
|
233
|
+
if (typeof value !== 'object' || value === null) {
|
|
234
|
+
return value
|
|
235
|
+
}
|
|
236
|
+
return JSON.parse(JSON.stringify(value))
|
|
237
|
+
}
|
|
238
|
+
|
|
219
239
|
/*
|
|
220
240
|
Normalises the `swr` option to its window, or undefined when off. `true` (or
|
|
221
241
|
`{}`) is stale-while-revalidate with no window — refetch immediately on every
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { bodyValueForKind, DEFER } from './bodyValueForKind.ts'
|
|
1
2
|
import { contentBodyKind } from './contentBodyKind.ts'
|
|
2
3
|
import { contentTypeOf } from './contentTypeOf.ts'
|
|
3
4
|
import type { CacheEntry } from './types/CacheEntry.ts'
|
|
@@ -32,31 +33,35 @@ export function cacheEntryFromSnapshot(entry: CacheSnapshotEntry): CacheEntry {
|
|
|
32
33
|
|
|
33
34
|
/*
|
|
34
35
|
Synchronously decodes a snapshot body so the warm entry reads without a
|
|
35
|
-
microtask hop on first render.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
what awaiting the Response would yield. Every
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
would.
|
|
36
|
+
microtask hop on first render. The json/text branches go through the shared
|
|
37
|
+
`bodyValueForKind` — the same mapping the live read (`decodeResponse`) uses — so
|
|
38
|
+
the two cannot disagree about how a body becomes a value, returning a value
|
|
39
|
+
identical to what awaiting the Response would yield. Every non-warmable case —
|
|
40
|
+
non-2xx, 204 (the status gate below), streaming, binary (the DEFER sentinel) —
|
|
41
|
+
yields no warm value and defers to the async path, which throws HttpError /
|
|
42
|
+
returns the streaming error / blobs exactly as a live call would.
|
|
42
43
|
*/
|
|
43
44
|
function warmValueFromSnapshot(status: number, headers: Headers, body: string): unknown {
|
|
45
|
+
/* Status gate is side-specific: an error/204 has no warm value here, but the live
|
|
46
|
+
read throws HttpError / returns undefined for the same status — so it stays out of
|
|
47
|
+
the shared kind mapping. */
|
|
44
48
|
if (status === 204 || status < 200 || status >= 300) {
|
|
45
49
|
return undefined
|
|
46
50
|
}
|
|
47
51
|
const kind = contentBodyKind(contentTypeOf(headers))
|
|
48
|
-
|
|
52
|
+
const value = bodyValueForKind(
|
|
53
|
+
kind,
|
|
49
54
|
/* The body may still be malformed JSON; fall back to the async path rather than
|
|
50
55
|
throwing a SyntaxError synchronously during hydration. */
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
() => {
|
|
57
|
+
try {
|
|
58
|
+
return JSON.parse(body)
|
|
59
|
+
} catch {
|
|
60
|
+
return undefined
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
() => body,
|
|
64
|
+
)
|
|
60
65
|
/* streaming and binary defer to the async path (the live decode throws / blobs). */
|
|
61
|
-
return undefined
|
|
66
|
+
return value === DEFER ? undefined : value
|
|
62
67
|
}
|