@belte/belte 0.19.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/CHANGELOG.md +313 -0
- package/LICENSE +21 -0
- package/README.md +559 -0
- package/bin/belte.ts +183 -0
- package/package.json +110 -0
- package/src/App.svelte +31 -0
- package/src/appEntry.ts +151 -0
- package/src/assets/app.html +14 -0
- package/src/belteResolverPlugin.ts +858 -0
- package/src/build.ts +147 -0
- package/src/buildCli.ts +129 -0
- package/src/buildDisconnected.ts +122 -0
- package/src/bundleApp.ts +149 -0
- package/src/bundleDisconnectedEntry.ts +17 -0
- package/src/cliEntry.ts +25 -0
- package/src/clientBuildPlugins.ts +41 -0
- package/src/clientEntry.ts +7 -0
- package/src/compile.ts +64 -0
- package/src/controlServerWorker.ts +422 -0
- package/src/dedupeSveltePlugin.ts +66 -0
- package/src/devEntry.ts +169 -0
- package/src/discoveryEntry.ts +81 -0
- package/src/lib/browser/applyStreamedResolution.ts +33 -0
- package/src/lib/browser/cacheEntryFromSnapshot.ts +48 -0
- package/src/lib/browser/flushUnresolvedPlaceholders.ts +16 -0
- package/src/lib/browser/installStreamingPlaceholders.ts +32 -0
- package/src/lib/browser/openResolveStream.ts +42 -0
- package/src/lib/browser/page.svelte.ts +258 -0
- package/src/lib/browser/pageStreamController.ts +17 -0
- package/src/lib/browser/refetchPlaceholder.ts +12 -0
- package/src/lib/browser/remoteProxy.ts +37 -0
- package/src/lib/browser/socketChannel.ts +192 -0
- package/src/lib/browser/socketProxy.ts +57 -0
- package/src/lib/browser/startClient.ts +153 -0
- package/src/lib/browser/subscribe.ts +131 -0
- package/src/lib/browser/types/Errors.ts +9 -0
- package/src/lib/browser/types/Layouts.ts +7 -0
- package/src/lib/browser/types/Pages.ts +7 -0
- package/src/lib/browser/types/StreamingDeferred.ts +9 -0
- package/src/lib/bundle/BundleMenu.ts +11 -0
- package/src/lib/bundle/BundleMenuItem.ts +24 -0
- package/src/lib/bundle/BundleWindow.ts +36 -0
- package/src/lib/bundle/WEBVIEW_BUILD_REVISION.ts +9 -0
- package/src/lib/bundle/WEBVIEW_VERSION.ts +7 -0
- package/src/lib/bundle/bindConnectedFlag.ts +29 -0
- package/src/lib/bundle/bindRequestNavigate.ts +31 -0
- package/src/lib/bundle/buildWebviewLib.ts +111 -0
- package/src/lib/bundle/disconnected.css +9 -0
- package/src/lib/bundle/disconnected.svelte +386 -0
- package/src/lib/bundle/ensureWebviewLib.ts +20 -0
- package/src/lib/bundle/exitWithParent.ts +28 -0
- package/src/lib/bundle/infoPlist.ts +46 -0
- package/src/lib/bundle/installDownloads.ts +24 -0
- package/src/lib/bundle/installMacMenu.ts +39 -0
- package/src/lib/bundle/listenLocalControlServer.ts +19 -0
- package/src/lib/bundle/native/belteMenu.mm +422 -0
- package/src/lib/bundle/native/webview.h +4557 -0
- package/src/lib/bundle/onMenu.ts +41 -0
- package/src/lib/bundle/openWebview.ts +104 -0
- package/src/lib/bundle/pngToIcns.ts +47 -0
- package/src/lib/bundle/probeBelteServer.ts +34 -0
- package/src/lib/bundle/resolveServerBinary.ts +12 -0
- package/src/lib/bundle/resolveWebviewLib.ts +53 -0
- package/src/lib/bundle/serverBinaryFilename.ts +8 -0
- package/src/lib/bundle/signMacApp.ts +35 -0
- package/src/lib/bundle/spawnEmbeddedServer.ts +65 -0
- package/src/lib/bundle/stableLocalPort.ts +19 -0
- package/src/lib/bundle/waitForServer.ts +23 -0
- package/src/lib/bundle/webviewCachePath.ts +23 -0
- package/src/lib/bundle/webviewLibName.ts +11 -0
- package/src/lib/cli/connectToServer.ts +23 -0
- package/src/lib/cli/createClient.ts +170 -0
- package/src/lib/cli/dispatchCommand.ts +71 -0
- package/src/lib/cli/loadEnvFromBinaryDir.ts +16 -0
- package/src/lib/cli/parseArgvForRpc.ts +97 -0
- package/src/lib/cli/printHelp.ts +119 -0
- package/src/lib/cli/printSessionHelp.ts +27 -0
- package/src/lib/cli/printSessionStatus.ts +21 -0
- package/src/lib/cli/printTrimmed.ts +8 -0
- package/src/lib/cli/printValue.ts +10 -0
- package/src/lib/cli/resolveCliTarget.ts +48 -0
- package/src/lib/cli/runCli.ts +139 -0
- package/src/lib/cli/runSession.ts +105 -0
- package/src/lib/cli/startLocalInstance.ts +14 -0
- package/src/lib/cli/tokenizeLine.ts +51 -0
- package/src/lib/cli/types/CliManifest.ts +9 -0
- package/src/lib/cli/types/CliManifestEntry.ts +17 -0
- package/src/lib/cli/types/CliTarget.ts +13 -0
- package/src/lib/mcp/annotationsForMethod.ts +29 -0
- package/src/lib/mcp/createMcpResourceServer.ts +101 -0
- package/src/lib/mcp/createMcpServer.ts +42 -0
- package/src/lib/mcp/dispatchMcpRequest.ts +146 -0
- package/src/lib/mcp/mcpResourceServerSlot.ts +18 -0
- package/src/lib/mcp/mcpSurface.ts +265 -0
- package/src/lib/mcp/toolResultFromResponse.ts +66 -0
- package/src/lib/mcp/types/JsonRpcRequest.ts +12 -0
- package/src/lib/mcp/types/JsonRpcResponse.ts +20 -0
- package/src/lib/mcp/types/McpResourceContents.ts +10 -0
- package/src/lib/mcp/types/McpResourceDescriptor.ts +6 -0
- package/src/lib/mcp/types/McpResourceServer.ts +12 -0
- package/src/lib/mcp/types/McpServer.ts +9 -0
- package/src/lib/mcp/types/McpServerOptions.ts +16 -0
- package/src/lib/server/AppModule.ts +33 -0
- package/src/lib/server/DELETE.ts +9 -0
- package/src/lib/server/GET.ts +9 -0
- package/src/lib/server/HEAD.ts +9 -0
- package/src/lib/server/PATCH.ts +9 -0
- package/src/lib/server/POST.ts +9 -0
- package/src/lib/server/PUT.ts +9 -0
- package/src/lib/server/agent.ts +76 -0
- package/src/lib/server/appDataDir.ts +15 -0
- package/src/lib/server/cli/buildEnvContent.ts +19 -0
- package/src/lib/server/cli/createTarGz.ts +76 -0
- package/src/lib/server/cli/handleCliDownload.ts +153 -0
- package/src/lib/server/cli/handleCliInstall.ts +37 -0
- package/src/lib/server/cli/installScript.ts +29 -0
- package/src/lib/server/cli/maxSourceMtime.ts +26 -0
- package/src/lib/server/cookies.ts +29 -0
- package/src/lib/server/env.ts +50 -0
- package/src/lib/server/error.ts +70 -0
- package/src/lib/server/json.ts +28 -0
- package/src/lib/server/jsonl.ts +46 -0
- package/src/lib/server/prompts/definePrompt.ts +20 -0
- package/src/lib/server/prompts/promptRegistry.ts +9 -0
- package/src/lib/server/prompts/registerPrompt.ts +6 -0
- package/src/lib/server/prompts/renderPromptTemplate.ts +16 -0
- package/src/lib/server/prompts/types/Prompt.ts +13 -0
- package/src/lib/server/prompts/types/PromptOptions.ts +12 -0
- package/src/lib/server/prompts/types/PromptRegistryEntry.ts +13 -0
- package/src/lib/server/prompts/types/PromptRoutes.ts +10 -0
- package/src/lib/server/redirect.ts +42 -0
- package/src/lib/server/request.ts +18 -0
- package/src/lib/server/rpc/defineVerb.ts +133 -0
- package/src/lib/server/rpc/dispatchVerbInProcess.ts +46 -0
- package/src/lib/server/rpc/findVerbByCommandName.ts +18 -0
- package/src/lib/server/rpc/parseArgs.ts +95 -0
- package/src/lib/server/rpc/registerVerb.ts +6 -0
- package/src/lib/server/rpc/types/RemoteHandler.ts +27 -0
- package/src/lib/server/rpc/types/RemoteRoutes.ts +13 -0
- package/src/lib/server/rpc/types/TypedResponse.ts +18 -0
- package/src/lib/server/rpc/types/VerbHelper.ts +68 -0
- package/src/lib/server/rpc/types/VerbRegistryEntry.ts +29 -0
- package/src/lib/server/rpc/unprocessed.ts +14 -0
- package/src/lib/server/rpc/verbRegistry.ts +11 -0
- package/src/lib/server/runtime/DEFAULT_PORT.ts +6 -0
- package/src/lib/server/runtime/DEV_REBUILD_MESSAGE.ts +4 -0
- package/src/lib/server/runtime/DEV_RELOAD_CLIENT_SCRIPT.ts +29 -0
- package/src/lib/server/runtime/acceptsZstd.ts +8 -0
- package/src/lib/server/runtime/buildOpenApiSpec.ts +106 -0
- package/src/lib/server/runtime/cacheControlForAsset.ts +22 -0
- package/src/lib/server/runtime/containsTraversal.ts +37 -0
- package/src/lib/server/runtime/createAssetHeaderCache.ts +35 -0
- package/src/lib/server/runtime/createPublicAssetServer.ts +63 -0
- package/src/lib/server/runtime/createRouteDispatcher.ts +100 -0
- package/src/lib/server/runtime/createServer.ts +692 -0
- package/src/lib/server/runtime/devReloadResponse.ts +35 -0
- package/src/lib/server/runtime/disableIdleTimeoutForStream.ts +27 -0
- package/src/lib/server/runtime/envSchemaStore.ts +15 -0
- package/src/lib/server/runtime/findOpenPort.ts +35 -0
- package/src/lib/server/runtime/getActiveServer.ts +6 -0
- package/src/lib/server/runtime/globToPathSet.ts +29 -0
- package/src/lib/server/runtime/inProcessServer.ts +20 -0
- package/src/lib/server/runtime/internalErrorResponse.ts +25 -0
- package/src/lib/server/runtime/isCrossOriginUpgrade.ts +19 -0
- package/src/lib/server/runtime/listenOnOpenPort.ts +36 -0
- package/src/lib/server/runtime/logExposedSurfaces.ts +162 -0
- package/src/lib/server/runtime/mimeForExtension.ts +20 -0
- package/src/lib/server/runtime/parseIdleTimeout.ts +10 -0
- package/src/lib/server/runtime/parsePort.ts +11 -0
- package/src/lib/server/runtime/registryManifests.ts +66 -0
- package/src/lib/server/runtime/requestContext.ts +5 -0
- package/src/lib/server/runtime/resolveStreamResponse.ts +29 -0
- package/src/lib/server/runtime/runWithRequestScope.ts +57 -0
- package/src/lib/server/runtime/safeJsonForScript.ts +17 -0
- package/src/lib/server/runtime/serializeCacheSnapshot.ts +45 -0
- package/src/lib/server/runtime/serverSlot.ts +13 -0
- package/src/lib/server/runtime/setActiveServer.ts +6 -0
- package/src/lib/server/runtime/snapshotEntryFromCache.ts +81 -0
- package/src/lib/server/runtime/streamCacheResolutions.ts +37 -0
- package/src/lib/server/runtime/streamFromIterator.ts +86 -0
- package/src/lib/server/runtime/streamStash.ts +64 -0
- package/src/lib/server/runtime/types/Assets.ts +1 -0
- package/src/lib/server/runtime/types/RequestStore.ts +27 -0
- package/src/lib/server/runtime/withResponseDefaults.ts +24 -0
- package/src/lib/server/server.ts +32 -0
- package/src/lib/server/socket.ts +31 -0
- package/src/lib/server/sockets/createSocketDispatcher.ts +311 -0
- package/src/lib/server/sockets/defineSocket.ts +167 -0
- package/src/lib/server/sockets/lookupSocket.ts +6 -0
- package/src/lib/server/sockets/recentHistory.ts +11 -0
- package/src/lib/server/sockets/registerSocket.ts +6 -0
- package/src/lib/server/sockets/socketOperations.ts +35 -0
- package/src/lib/server/sockets/socketRegistry.ts +9 -0
- package/src/lib/server/sockets/types/Socket.ts +21 -0
- package/src/lib/server/sockets/types/SocketClientFrame.ts +18 -0
- package/src/lib/server/sockets/types/SocketOperation.ts +22 -0
- package/src/lib/server/sockets/types/SocketOptions.ts +22 -0
- package/src/lib/server/sockets/types/SocketRegistryEntry.ts +17 -0
- package/src/lib/server/sockets/types/SocketRoutes.ts +10 -0
- package/src/lib/server/sockets/types/SocketServerFrame.ts +15 -0
- package/src/lib/server/sse.ts +53 -0
- package/src/lib/shared/BELTE_PACKAGE_NAME.ts +7 -0
- package/src/lib/shared/CACHE_CONTROL_VALUES.ts +16 -0
- package/src/lib/shared/HttpError.ts +19 -0
- package/src/lib/shared/RESOLVE_STREAM_PATH.ts +7 -0
- package/src/lib/shared/STREAMING_CONTENT_TYPES.ts +11 -0
- package/src/lib/shared/activeCacheStore.ts +20 -0
- package/src/lib/shared/appDataDir.ts +34 -0
- package/src/lib/shared/belteImportName.ts +44 -0
- package/src/lib/shared/browserClientFlags.ts +10 -0
- package/src/lib/shared/buildRpcRequest.ts +70 -0
- package/src/lib/shared/bundleLayout.ts +36 -0
- package/src/lib/shared/bundled.ts +34 -0
- package/src/lib/shared/cache.ts +559 -0
- package/src/lib/shared/cacheStoreSlot.ts +16 -0
- package/src/lib/shared/canonicalJson.ts +63 -0
- package/src/lib/shared/carriesBodyArgs.ts +13 -0
- package/src/lib/shared/clearLastConnection.ts +7 -0
- package/src/lib/shared/commandNameForUrl.ts +17 -0
- package/src/lib/shared/createCacheStore.ts +75 -0
- package/src/lib/shared/createPushIterator.ts +93 -0
- package/src/lib/shared/createRemoteFunction.ts +99 -0
- package/src/lib/shared/decodeResponse.ts +47 -0
- package/src/lib/shared/detectTarget.ts +27 -0
- package/src/lib/shared/exeSuffix.ts +9 -0
- package/src/lib/shared/exitOnBuildFailure.ts +17 -0
- package/src/lib/shared/extraForwardHeaders.ts +16 -0
- package/src/lib/shared/fileStem.ts +9 -0
- package/src/lib/shared/findExportCallSite.ts +479 -0
- package/src/lib/shared/forwardHeaders.ts +41 -0
- package/src/lib/shared/getRemoteMeta.ts +5 -0
- package/src/lib/shared/globalCacheStore.ts +15 -0
- package/src/lib/shared/globalCacheStoreSlot.ts +14 -0
- package/src/lib/shared/importNamesToStrip.ts +13 -0
- package/src/lib/shared/invalidateEvent.ts +11 -0
- package/src/lib/shared/isCompileTarget.ts +15 -0
- package/src/lib/shared/isDebugEnabled.ts +23 -0
- package/src/lib/shared/isModuleNotFound.ts +16 -0
- package/src/lib/shared/isReadOnlyMethod.ts +14 -0
- package/src/lib/shared/isStreamingResponse.ts +11 -0
- package/src/lib/shared/jsonSchemaForPromptArguments.ts +29 -0
- package/src/lib/shared/jsonSchemaForSchema.ts +32 -0
- package/src/lib/shared/jsonlErrorFrame.ts +24 -0
- package/src/lib/shared/keyForRemoteCall.ts +29 -0
- package/src/lib/shared/lastConnectionPath.ts +7 -0
- package/src/lib/shared/loadEnvFile.ts +17 -0
- package/src/lib/shared/loadEnvFromDataDir.ts +15 -0
- package/src/lib/shared/loadSvelteConfig.ts +18 -0
- package/src/lib/shared/log.ts +104 -0
- package/src/lib/shared/manifestModule.ts +39 -0
- package/src/lib/shared/memoizeByKey.ts +24 -0
- package/src/lib/shared/nearestLayoutPrefix.ts +36 -0
- package/src/lib/shared/normalizeTarget.ts +10 -0
- package/src/lib/shared/pageUrlForFile.ts +14 -0
- package/src/lib/shared/parseBoundedEnvInt.ts +20 -0
- package/src/lib/shared/parseEnv.ts +30 -0
- package/src/lib/shared/parsePromptMarkdown.ts +34 -0
- package/src/lib/shared/parseRouteSegments.ts +22 -0
- package/src/lib/shared/prepareRpcModule.ts +59 -0
- package/src/lib/shared/prepareSocketModule.ts +49 -0
- package/src/lib/shared/programNameForPackage.ts +14 -0
- package/src/lib/shared/promptNameForFile.ts +10 -0
- package/src/lib/shared/queryStringFromArgs.ts +27 -0
- package/src/lib/shared/readEnvFile.ts +15 -0
- package/src/lib/shared/readLastConnection.ts +18 -0
- package/src/lib/shared/readPackageJson.ts +9 -0
- package/src/lib/shared/recordRemoteMeta.ts +5 -0
- package/src/lib/shared/remoteMetaStore.ts +16 -0
- package/src/lib/shared/resolveClientFlags.ts +20 -0
- package/src/lib/shared/responseErrorText.ts +9 -0
- package/src/lib/shared/rpcUrlForFile.ts +19 -0
- package/src/lib/shared/runningAsStandaloneBinary.ts +13 -0
- package/src/lib/shared/serializeEnv.ts +18 -0
- package/src/lib/shared/setCacheStoreResolver.ts +6 -0
- package/src/lib/shared/setGlobalCacheStoreResolver.ts +6 -0
- package/src/lib/shared/socketNameForFile.ts +11 -0
- package/src/lib/shared/sseErrorFrame.ts +29 -0
- package/src/lib/shared/streamResponse.ts +169 -0
- package/src/lib/shared/stripImport.ts +27 -0
- package/src/lib/shared/subscribableFromResponse.ts +51 -0
- package/src/lib/shared/toBunRoutePattern.ts +28 -0
- package/src/lib/shared/types/CacheEntry.ts +63 -0
- package/src/lib/shared/types/CacheInvalidation.ts +9 -0
- package/src/lib/shared/types/CacheOptions.ts +33 -0
- package/src/lib/shared/types/CacheSnapshot.ts +16 -0
- package/src/lib/shared/types/CacheSnapshotEntry.ts +15 -0
- package/src/lib/shared/types/CacheStore.ts +32 -0
- package/src/lib/shared/types/ClientFlags.ts +11 -0
- package/src/lib/shared/types/CompileTarget.ts +6 -0
- package/src/lib/shared/types/HttpVerb.ts +1 -0
- package/src/lib/shared/types/LastConnection.ts +9 -0
- package/src/lib/shared/types/PromptArgument.ts +12 -0
- package/src/lib/shared/types/RawRemoteFunction.ts +13 -0
- package/src/lib/shared/types/RemoteFunction.ts +42 -0
- package/src/lib/shared/types/StandardSchemaV1.ts +57 -0
- package/src/lib/shared/types/StreamedResolution.ts +10 -0
- package/src/lib/shared/types/StreamingPlaceholder.ts +13 -0
- package/src/lib/shared/types/Subscribable.ts +15 -0
- package/src/lib/shared/types/SvelteConfig.ts +5 -0
- package/src/lib/shared/withJsonSchema.ts +20 -0
- package/src/lib/shared/writeLastConnection.ts +13 -0
- package/src/lib/shared/writeRoutesDts.ts +67 -0
- package/src/lib/test/clearVerbRegistry.ts +11 -0
- package/src/lib/test/createTestClient.ts +78 -0
- package/src/preload.ts +20 -0
- package/src/scaffold.ts +92 -0
- package/src/serverBuildPlugins.ts +25 -0
- package/src/serverEntry.ts +94 -0
- package/src/sveltePlugin.ts +58 -0
- package/src/tailwindStylePreprocessor.ts +62 -0
- package/template/bunfig.toml +4 -0
- package/template/package.json +19 -0
- package/template/src/app.ts +23 -0
- package/template/src/browser/app.css +21 -0
- package/template/src/browser/app.html +24 -0
- package/template/src/browser/pages/about/page.svelte +5 -0
- package/template/src/browser/pages/layout.svelte +26 -0
- package/template/src/browser/pages/page.svelte +20 -0
- package/template/src/bundle/icon.png +0 -0
- package/template/src/cli/banner.txt +3 -0
- package/template/src/cli/footer.txt +1 -0
- package/template/src/server/config.ts +17 -0
- package/template/src/server/rpc/getHello.ts +35 -0
- package/template/svelte.config.js +12 -0
- package/template/tsconfig.json +18 -0
- package/tsconfig.app.json +16 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { NO_STORE } from '../../shared/CACHE_CONTROL_VALUES.ts'
|
|
2
|
+
|
|
3
|
+
// Keepalive comment cadence — keeps the idle SSE connection from being dropped.
|
|
4
|
+
const KEEPALIVE_INTERVAL_MS = 15000
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
The dev live-reload channel (`/__belte/dev`, dev only). An SSE stream that
|
|
8
|
+
carries no events of its own — the browser-side client (DEV_RELOAD_CLIENT_SCRIPT)
|
|
9
|
+
reloads when this connection drops and reconnects, which only happens when the
|
|
10
|
+
dev orchestrator restarts the server after a rebuild. The opening `retry: 250`
|
|
11
|
+
shortens EventSource's reconnect backoff; a periodic comment keeps the idle
|
|
12
|
+
connection alive. The interval is cleared when the consumer disconnects.
|
|
13
|
+
*/
|
|
14
|
+
export function devReloadResponse(): Response {
|
|
15
|
+
let keepalive: ReturnType<typeof setInterval>
|
|
16
|
+
const body = new ReadableStream<Uint8Array>({
|
|
17
|
+
start(controller) {
|
|
18
|
+
controller.enqueue(new TextEncoder().encode('retry: 250\n\n'))
|
|
19
|
+
keepalive = setInterval(() => {
|
|
20
|
+
controller.enqueue(new TextEncoder().encode(': keepalive\n\n'))
|
|
21
|
+
}, KEEPALIVE_INTERVAL_MS)
|
|
22
|
+
},
|
|
23
|
+
cancel() {
|
|
24
|
+
clearInterval(keepalive)
|
|
25
|
+
},
|
|
26
|
+
})
|
|
27
|
+
return new Response(body, {
|
|
28
|
+
headers: {
|
|
29
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
30
|
+
'Cache-Control': NO_STORE,
|
|
31
|
+
'X-Content-Type-Options': 'nosniff',
|
|
32
|
+
Connection: 'keep-alive',
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Server } from 'bun'
|
|
2
|
+
import { isStreamingResponse } from '../../shared/isStreamingResponse.ts'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
Opts a long-lived streaming response (SSE / JSONL) out of Bun's per-connection
|
|
6
|
+
idle timeout. Such a stream can stay quiet for longer than the 10s default
|
|
7
|
+
between frames, which Bun would otherwise read as an idle connection and close
|
|
8
|
+
mid-stream. `server.timeout(req, 0)` clears the timeout for just this in-flight
|
|
9
|
+
request, leaving the global default in place for ordinary traffic. Streaming is
|
|
10
|
+
detected by Content-Type (the shared signal the CLI/MCP drain paths use) rather
|
|
11
|
+
than `body instanceof ReadableStream`, since every bodied Response exposes one.
|
|
12
|
+
|
|
13
|
+
A streamed SSR page (`text/html`) is deliberately not opted out: it inherits the
|
|
14
|
+
configured `idleTimeout` as a bounded cap on how long it can hold the connection,
|
|
15
|
+
and the client re-fetches any placeholder left unresolved by a cut stream (see
|
|
16
|
+
flushUnresolvedPlaceholders). Non-stream responses pass through.
|
|
17
|
+
*/
|
|
18
|
+
export function disableIdleTimeoutForStream(
|
|
19
|
+
server: Server<unknown>,
|
|
20
|
+
req: Request,
|
|
21
|
+
response: Response,
|
|
22
|
+
): Response {
|
|
23
|
+
if (isStreamingResponse(response)) {
|
|
24
|
+
server.timeout(req, 0)
|
|
25
|
+
}
|
|
26
|
+
return response
|
|
27
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from '../../shared/types/StandardSchemaV1.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Holds the schema handed to env() so the bundle launcher can project the
|
|
5
|
+
first-run setup form from it (jsonSchemaForSchema) without re-running boot
|
|
6
|
+
validation. `skipValidation` lets the launcher import src/server/config.ts
|
|
7
|
+
purely to register the schema: env() records it and returns early instead of
|
|
8
|
+
validating Bun.env — which the launcher has no business doing, that's the
|
|
9
|
+
embedded server's job at its own boot. In-process module state, so the server
|
|
10
|
+
child the launcher spawns gets a fresh store and validates normally.
|
|
11
|
+
*/
|
|
12
|
+
export const envSchemaStore: {
|
|
13
|
+
schema: StandardSchemaV1 | undefined
|
|
14
|
+
skipValidation: boolean
|
|
15
|
+
} = { schema: undefined, skipValidation: false }
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Ports probed upward from `start` before giving up and letting the kernel assign one.
|
|
2
|
+
const SCAN_RANGE = 100
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
Returns the first bindable TCP port at or above `start`, probing upward.
|
|
6
|
+
Used when no PORT is configured so the server lands on a predictable
|
|
7
|
+
3000+ port (3000, then 3001, …) instead of a random kernel-assigned one —
|
|
8
|
+
running a second app just steps to the next free port. Each probe binds a
|
|
9
|
+
throwaway server and stops it; like any release-then-rebind there's a tiny
|
|
10
|
+
race before the real listener takes the port, negligible for a local boot.
|
|
11
|
+
After SCAN_RANGE occupied ports it gives up scanning and lets the kernel
|
|
12
|
+
assign any free port (bind to 0).
|
|
13
|
+
*/
|
|
14
|
+
export function findOpenPort(start: number): number {
|
|
15
|
+
for (let port = start; port < start + SCAN_RANGE; port++) {
|
|
16
|
+
try {
|
|
17
|
+
return bindAndRelease(port)
|
|
18
|
+
} catch {
|
|
19
|
+
// port in use — try the next one up
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
// every candidate was taken; bind to 0 so the kernel picks a free port
|
|
23
|
+
return bindAndRelease(0)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/*
|
|
27
|
+
Binds a throwaway server to `port` (0 = let the kernel assign one), reads the
|
|
28
|
+
actual bound port, and releases it. Throws if the port is already in use.
|
|
29
|
+
*/
|
|
30
|
+
function bindAndRelease(port: number): number {
|
|
31
|
+
const probe = Bun.serve({ port, fetch: () => new Response() })
|
|
32
|
+
const bound = probe.port as number
|
|
33
|
+
probe.stop(true)
|
|
34
|
+
return bound
|
|
35
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Glob } from 'bun'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Scans `cwd` for files matching `pattern` and returns their request paths as
|
|
5
|
+
a Set, mapping each relative file path to a root-relative URL via `keyFor`.
|
|
6
|
+
Used to snapshot the on-disk asset trees (the `public/` files, the `_app`
|
|
7
|
+
precompressed `.zst` siblings) once at boot so the request path is a Set
|
|
8
|
+
lookup instead of a filesystem stat.
|
|
9
|
+
|
|
10
|
+
A missing directory makes scan throw ENOENT — swallowed to an empty Set so
|
|
11
|
+
the caller just falls through. This scan-and-catch is also the reliable
|
|
12
|
+
directory existence test: `Bun.file(dir).exists()` returns false for a
|
|
13
|
+
directory, so guarding the scan with it silently yields an empty Set.
|
|
14
|
+
*/
|
|
15
|
+
export async function globToPathSet(
|
|
16
|
+
cwd: string,
|
|
17
|
+
pattern: string,
|
|
18
|
+
keyFor: (file: string) => string,
|
|
19
|
+
options?: { dot?: boolean },
|
|
20
|
+
): Promise<Set<string>> {
|
|
21
|
+
try {
|
|
22
|
+
const files = await Array.fromAsync(
|
|
23
|
+
new Glob(pattern).scan({ cwd, dot: options?.dot ?? false }),
|
|
24
|
+
)
|
|
25
|
+
return new Set(files.map(keyFor))
|
|
26
|
+
} catch {
|
|
27
|
+
return new Set()
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Server } from 'bun'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
The Server `server()` hands back under in-process dispatch (CLI / MCP / test
|
|
5
|
+
client), where no Bun.serve has booted so there is no live connection to act on.
|
|
6
|
+
Each member is the honest no-op for a request that never rode a socket: timeout
|
|
7
|
+
and upgrade do nothing, publish reaches no subscribers (0 bytes sent),
|
|
8
|
+
subscriberCount is 0, requestIP has no peer (null). Handlers using these idioms
|
|
9
|
+
run unchanged in-process instead of throwing; createServer's live Server takes
|
|
10
|
+
precedence whenever one is booted. Connection-scoped only — config-shaped
|
|
11
|
+
members (port, url, hostname …) are intentionally absent: there is no server to
|
|
12
|
+
describe, and a stubbed value would mislead more than their plain absence.
|
|
13
|
+
*/
|
|
14
|
+
export const inProcessServer = {
|
|
15
|
+
timeout() {},
|
|
16
|
+
upgrade: () => false,
|
|
17
|
+
publish: () => 0,
|
|
18
|
+
subscriberCount: () => 0,
|
|
19
|
+
requestIP: () => null,
|
|
20
|
+
} as unknown as Server<unknown>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { NO_STORE } from '../../shared/CACHE_CONTROL_VALUES.ts'
|
|
2
|
+
import { isDebugEnabled } from '../../shared/isDebugEnabled.ts'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
The framework's default 500 response. Shared by the per-request scope's catch
|
|
6
|
+
(runWithRequestScope) and Bun.serve's global error() fallback so the two can't
|
|
7
|
+
drift. Only reached when the app supplies no `handleError` hook.
|
|
8
|
+
|
|
9
|
+
Secure by default: a bare `Internal Server Error` so paths, library versions,
|
|
10
|
+
and message contents never leak to clients in production. The full stack is
|
|
11
|
+
shown only under `DEBUG=belte` (the same dev signal that turns on request
|
|
12
|
+
logging); the cause is logged server-side regardless of the flag.
|
|
13
|
+
*/
|
|
14
|
+
export function internalErrorResponse(error: unknown): Response {
|
|
15
|
+
const body = isDebugEnabled('belte')
|
|
16
|
+
? `<pre>${String((error as Error)?.stack ?? error)}</pre>`
|
|
17
|
+
: 'Internal Server Error'
|
|
18
|
+
return new Response(body, {
|
|
19
|
+
status: 500,
|
|
20
|
+
headers: {
|
|
21
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
22
|
+
'Cache-Control': NO_STORE,
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/*
|
|
2
|
+
True when a WebSocket upgrade carries a browser Origin that doesn't match the
|
|
3
|
+
request's own host — the cross-site WebSocket hijacking (CSWSH) shape. A
|
|
4
|
+
mismatched Origin means another site is trying to open the socket in a visitor's
|
|
5
|
+
authenticated browser. Native clients (CLI, MCP) send no Origin, so an absent
|
|
6
|
+
header is allowed; only a present-and-mismatched (or unparseable) Origin is
|
|
7
|
+
rejected. An unparseable Origin is treated as cross-origin (fail closed).
|
|
8
|
+
*/
|
|
9
|
+
export function isCrossOriginUpgrade(request: Request, requestUrl: URL): boolean {
|
|
10
|
+
const origin = request.headers.get('origin')
|
|
11
|
+
if (!origin) {
|
|
12
|
+
return false
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
return new URL(origin).host !== requestUrl.host
|
|
16
|
+
} catch {
|
|
17
|
+
return true
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Server } from 'bun'
|
|
2
|
+
|
|
3
|
+
// Ports tried upward from `start` before giving up and letting the kernel assign one.
|
|
4
|
+
const SCAN_RANGE = 100
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
Binds the real server, scanning upward from `start` for the first free port.
|
|
8
|
+
The listener that wins a port is the one that keeps it: unlike probing a
|
|
9
|
+
throwaway server and releasing it before the real bind, this leaves no window
|
|
10
|
+
for the chosen port to be stolen in between — the gap that crashed boot on
|
|
11
|
+
EADDRINUSE instead of stepping to the next port. `bindAt` does the actual
|
|
12
|
+
Bun.serve; only an in-use port is retried, any other failure propagates. After
|
|
13
|
+
SCAN_RANGE occupied ports it binds port 0 so the kernel assigns any free port.
|
|
14
|
+
*/
|
|
15
|
+
export function listenOnOpenPort(
|
|
16
|
+
bindAt: (port: number) => Server<unknown>,
|
|
17
|
+
start: number,
|
|
18
|
+
): Server<unknown> {
|
|
19
|
+
for (let port = start; port < start + SCAN_RANGE; port++) {
|
|
20
|
+
try {
|
|
21
|
+
return bindAt(port)
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (!isAddressInUse(error)) {
|
|
24
|
+
throw error
|
|
25
|
+
}
|
|
26
|
+
// port in use — try the next one up
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// every candidate was taken; bind to 0 so the kernel picks a free port
|
|
30
|
+
return bindAt(0)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Bun reports a taken port as an Error carrying code 'EADDRINUSE'.
|
|
34
|
+
function isAddressInUse(error: unknown): boolean {
|
|
35
|
+
return error instanceof Error && (error as { code?: string }).code === 'EADDRINUSE'
|
|
36
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { Pages } from '../../browser/types/Pages.ts'
|
|
2
|
+
import { log } from '../../shared/log.ts'
|
|
3
|
+
import type { NormalizedLayoutPrefix } from '../../shared/nearestLayoutPrefix.ts'
|
|
4
|
+
import { nearestLayoutPrefix } from '../../shared/nearestLayoutPrefix.ts'
|
|
5
|
+
import { verbRegistry } from '../rpc/verbRegistry.ts'
|
|
6
|
+
import { socketRegistry } from '../sockets/socketRegistry.ts'
|
|
7
|
+
import { ensureRegistriesLoaded } from './registryManifests.ts'
|
|
8
|
+
|
|
9
|
+
// Cell glyphs: PRESENT, ABSENT.
|
|
10
|
+
const PRESENT = '✓'
|
|
11
|
+
const ABSENT = '·'
|
|
12
|
+
const COLUMN_GAP = 2
|
|
13
|
+
|
|
14
|
+
const hasColor = typeof Bun !== 'undefined' && Bun.enableANSIColors
|
|
15
|
+
// Red foreground then `\x1b[39m` (default-foreground, not full reset) so the enclosing dim survives.
|
|
16
|
+
const redden = (text: string): string =>
|
|
17
|
+
hasColor ? `${Bun.color('red', 'ansi-256')}${text}\x1b[39m` : text
|
|
18
|
+
|
|
19
|
+
/*
|
|
20
|
+
A declared inputSchema is what makes mcp/cli safe to advertise (see defineVerb /
|
|
21
|
+
defineSocket), so a missing schema gets a red `·` to flag the declaration whose
|
|
22
|
+
machine surfaces are gated behind it.
|
|
23
|
+
*/
|
|
24
|
+
const schemaCell = (hasSchema: boolean): string => (hasSchema ? PRESENT : redden(ABSENT))
|
|
25
|
+
const flag = (on: boolean): string => (on ? PRESENT : ABSENT)
|
|
26
|
+
|
|
27
|
+
// Display width ignoring ANSI color escapes, so colored glyphs don't inflate alignment.
|
|
28
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: the ESC (\x1b) is the intended match — stripping the color escape to measure visible width
|
|
29
|
+
const displayWidth = (cell: string): number => cell.replace(/\x1b\[[0-9;]*m/g, '').length
|
|
30
|
+
|
|
31
|
+
// A titled table: its header row plus data rows, all sharing the column layout below.
|
|
32
|
+
type SurfaceTable = { title: string; header: string[]; rows: string[][] }
|
|
33
|
+
|
|
34
|
+
// Per-column width: the widest cell's display width across every row given.
|
|
35
|
+
function columnWidths(rows: string[][]): number[] {
|
|
36
|
+
const columnCount = Math.max(...rows.map((row) => row.length))
|
|
37
|
+
return Array.from({ length: columnCount }, (_, column) =>
|
|
38
|
+
Math.max(...rows.map((row) => displayWidth(row[column] ?? ''))),
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/*
|
|
43
|
+
Left-aligns one row's cells into the given fixed column widths plus a gap.
|
|
44
|
+
Padding is explicit spaces measured by displayWidth so embedded color escapes
|
|
45
|
+
don't inflate the width and break alignment. Two-space indented.
|
|
46
|
+
*/
|
|
47
|
+
function renderRow(row: string[], widths: number[]): string {
|
|
48
|
+
return (
|
|
49
|
+
' ' +
|
|
50
|
+
row
|
|
51
|
+
.map(
|
|
52
|
+
(cell, column) =>
|
|
53
|
+
cell + ' '.repeat(widths[column] - displayWidth(cell) + COLUMN_GAP),
|
|
54
|
+
)
|
|
55
|
+
.join('')
|
|
56
|
+
.trimEnd()
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/*
|
|
61
|
+
Prints every non-empty table under one shared set of column widths, so the
|
|
62
|
+
identifier and glyph columns line up vertically across the page/socket/rpc
|
|
63
|
+
tables rather than each table aligning only within itself. Each table keeps its
|
|
64
|
+
own dim header row.
|
|
65
|
+
*/
|
|
66
|
+
function logTables(tables: SurfaceTable[]): void {
|
|
67
|
+
const present = tables.filter((table) => table.rows.length > 0)
|
|
68
|
+
if (present.length === 0) {
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
const widths = columnWidths(present.flatMap((table) => [table.header, ...table.rows]))
|
|
72
|
+
present.forEach((table) => {
|
|
73
|
+
log.info(`${table.title}:`)
|
|
74
|
+
log.detail([table.header, ...table.rows].map((row) => renderRow(row, widths)).join('\n'))
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/*
|
|
79
|
+
Boot-time surface map: every page, socket, and rpc with the surfaces it is
|
|
80
|
+
exposed on, so belte's routing and multimodal-by-default exposure are auditable
|
|
81
|
+
rather than implicit. Three aligned tables — scan a column to spot a missing
|
|
82
|
+
surface, a row to see one declaration's reach:
|
|
83
|
+
|
|
84
|
+
- pages: each route with the nearest layout and error boundary wrapping it.
|
|
85
|
+
- sockets: name + client surfaces (schema/browser/mcp/cli) and whether clients
|
|
86
|
+
may publish.
|
|
87
|
+
- rpcs: method+path (headed `http` since http/openapi are unconditional) +
|
|
88
|
+
per-declaration client surfaces.
|
|
89
|
+
|
|
90
|
+
All three render under one shared column grid (see logTables): the identifier
|
|
91
|
+
leads flush-left in every table — page route, socket name, or rpc method+path —
|
|
92
|
+
then the surface columns, so the glyphs line up vertically across the tables.
|
|
93
|
+
rpc folds its method into a left-aligned prefix of the identifier cell, so paths
|
|
94
|
+
still start at a shared column. For sockets and rpcs the `schema`
|
|
95
|
+
column leads: it's what unlocks the non-browser surfaces, so a missing schema
|
|
96
|
+
reddens to flag the gated declaration. Loads the full registry, so it runs once
|
|
97
|
+
at boot and only under `belte` debug logging (DEBUG=belte) to avoid forcing
|
|
98
|
+
eager imports in production. Best-effort: enumeration failures are swallowed,
|
|
99
|
+
this is diagnostic only.
|
|
100
|
+
*/
|
|
101
|
+
export async function logExposedSurfaces(routing: {
|
|
102
|
+
pages: Pages
|
|
103
|
+
layoutPrefixes: NormalizedLayoutPrefix[]
|
|
104
|
+
errorPrefixes: NormalizedLayoutPrefix[]
|
|
105
|
+
}): Promise<void> {
|
|
106
|
+
try {
|
|
107
|
+
await ensureRegistriesLoaded()
|
|
108
|
+
} catch {
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const pageRows = Object.keys(routing.pages)
|
|
113
|
+
.map((route) => [
|
|
114
|
+
route,
|
|
115
|
+
nearestLayoutPrefix(route, routing.layoutPrefixes) ?? ABSENT,
|
|
116
|
+
nearestLayoutPrefix(route, routing.errorPrefixes) ?? ABSENT,
|
|
117
|
+
])
|
|
118
|
+
.sort()
|
|
119
|
+
|
|
120
|
+
const socketRows = Array.from(socketRegistry.values(), (entry) => [
|
|
121
|
+
entry.socket.name,
|
|
122
|
+
schemaCell(Boolean(entry.schema)),
|
|
123
|
+
flag(entry.clients.browser),
|
|
124
|
+
flag(entry.clients.mcp),
|
|
125
|
+
flag(entry.clients.cli),
|
|
126
|
+
flag(entry.allowClientPublish),
|
|
127
|
+
]).sort()
|
|
128
|
+
|
|
129
|
+
/*
|
|
130
|
+
rpc identifier = method left-aligned to a shared width then the path, so the
|
|
131
|
+
methods line up and every path starts at the same column — while the cell as
|
|
132
|
+
a whole still leads flush-left like the page route and socket name.
|
|
133
|
+
*/
|
|
134
|
+
const methodWidth = Math.max(
|
|
135
|
+
'http'.length,
|
|
136
|
+
...Array.from(verbRegistry.values(), (entry) => entry.remote.method.length),
|
|
137
|
+
)
|
|
138
|
+
const withMethod = (method: string, identifier: string): string =>
|
|
139
|
+
method.padEnd(methodWidth + COLUMN_GAP) + identifier
|
|
140
|
+
|
|
141
|
+
const rpcRows = Array.from(verbRegistry.values(), (entry) => [
|
|
142
|
+
withMethod(entry.remote.method, entry.remote.url),
|
|
143
|
+
schemaCell(Boolean(entry.inputSchema)),
|
|
144
|
+
flag(entry.clients.browser),
|
|
145
|
+
flag(entry.clients.mcp),
|
|
146
|
+
flag(entry.clients.cli),
|
|
147
|
+
]).sort()
|
|
148
|
+
|
|
149
|
+
logTables([
|
|
150
|
+
{ title: 'pages', header: ['page', 'layout', 'error'], rows: pageRows },
|
|
151
|
+
{
|
|
152
|
+
title: 'sockets',
|
|
153
|
+
header: ['socket', 'schema', 'browser', 'mcp', 'cli', 'publish'],
|
|
154
|
+
rows: socketRows,
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
title: 'rpcs',
|
|
158
|
+
header: [withMethod('http', ''), 'schema', 'browser', 'mcp', 'cli'],
|
|
159
|
+
rows: rpcRows,
|
|
160
|
+
},
|
|
161
|
+
])
|
|
162
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Derives the MIME type from a URL pathname using Bun.file().type, which
|
|
3
|
+
operates on the file extension synchronously without touching the disk. The
|
|
4
|
+
Bun.file ref here is never read from — it exists only to reuse Bun's
|
|
5
|
+
extension-to-MIME table. Cache by extension so repeat hits for the same
|
|
6
|
+
chunk type (.js / .css / .map / .svg / …) skip the BunFile allocation.
|
|
7
|
+
*/
|
|
8
|
+
const mimeByExtension = new Map<string, string>()
|
|
9
|
+
|
|
10
|
+
export function mimeForExtension(pathname: string): string {
|
|
11
|
+
const dot = pathname.lastIndexOf('.')
|
|
12
|
+
const extension = dot === -1 ? '' : pathname.slice(dot)
|
|
13
|
+
const cached = mimeByExtension.get(extension)
|
|
14
|
+
if (cached !== undefined) {
|
|
15
|
+
return cached
|
|
16
|
+
}
|
|
17
|
+
const type = Bun.file(pathname).type
|
|
18
|
+
mimeByExtension.set(extension, type)
|
|
19
|
+
return type
|
|
20
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { parseBoundedEnvInt } from '../../shared/parseBoundedEnvInt.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Parses BELTE_IDLE_TIMEOUT into Bun's per-connection idle timeout in seconds.
|
|
5
|
+
Bun accepts 0–255 (0 disables the timeout); returns undefined for missing,
|
|
6
|
+
empty, or out-of-range/non-integer input so the caller keeps its default.
|
|
7
|
+
*/
|
|
8
|
+
export function parseIdleTimeout(value: string | undefined): number | undefined {
|
|
9
|
+
return parseBoundedEnvInt(value, 0, 255)
|
|
10
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { parseBoundedEnvInt } from '../../shared/parseBoundedEnvInt.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Parses a PORT env value into a usable TCP port (0–65535), returning undefined
|
|
5
|
+
for missing, empty, or out-of-range/non-integer input so the caller can fall
|
|
6
|
+
back to a default. A bare Number() would turn '' into 0 (a random
|
|
7
|
+
kernel-assigned port) and 'abc' into NaN, both silently wrong.
|
|
8
|
+
*/
|
|
9
|
+
export function parsePort(value: string | undefined): number | undefined {
|
|
10
|
+
return parseBoundedEnvInt(value, 0, 65535)
|
|
11
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { PromptRoutes } from '../prompts/types/PromptRoutes.ts'
|
|
2
|
+
import type { RemoteRoutes } from '../rpc/types/RemoteRoutes.ts'
|
|
3
|
+
import type { SocketRoutes } from '../sockets/types/SocketRoutes.ts'
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
Process-wide slot for the rpc + sockets + prompts manifests. createServer
|
|
7
|
+
assigns once at boot (right after the route table is built); the MCP
|
|
8
|
+
server, the OpenAPI emitter, and prompt enumeration read it on first
|
|
9
|
+
request so they can lazy-import every module and walk the
|
|
10
|
+
verb/socket/prompt registries.
|
|
11
|
+
|
|
12
|
+
The slot pattern (mirrors getActiveServer) lets the framework-generated
|
|
13
|
+
McpServer bind to the manifests at module scope while the loaders stay
|
|
14
|
+
lazy until the first enumeration request.
|
|
15
|
+
*/
|
|
16
|
+
type RegistryManifests = {
|
|
17
|
+
rpc: RemoteRoutes
|
|
18
|
+
sockets: SocketRoutes
|
|
19
|
+
prompts: PromptRoutes
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let manifests: RegistryManifests | undefined
|
|
23
|
+
let loading: Promise<void> | undefined
|
|
24
|
+
|
|
25
|
+
export function setRegistryManifests(value: RegistryManifests): void {
|
|
26
|
+
manifests = value
|
|
27
|
+
loading = undefined
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/*
|
|
31
|
+
On first call, eagerly imports every rpc + socket + prompt module so
|
|
32
|
+
defineVerb / defineSocket / definePrompt fire and populate the
|
|
33
|
+
registries. Idempotent — repeat calls reuse the same in-flight promise,
|
|
34
|
+
so concurrent first requests (e.g. /openapi.json + an MCP tools/list)
|
|
35
|
+
trigger exactly one load instead of racing to fire the full import set
|
|
36
|
+
each. Eager loading is acceptable here because enumeration (MCP
|
|
37
|
+
tool/resource/prompt lists, the OpenAPI document) fundamentally requires
|
|
38
|
+
the full surface; the alternative of per-call lazy loading produces flaky
|
|
39
|
+
first-call latency.
|
|
40
|
+
*/
|
|
41
|
+
export function ensureRegistriesLoaded(): Promise<void> {
|
|
42
|
+
if (!manifests) {
|
|
43
|
+
return Promise.resolve()
|
|
44
|
+
}
|
|
45
|
+
if (!loading) {
|
|
46
|
+
const { rpc, sockets, prompts } = manifests
|
|
47
|
+
loading = Promise.all([
|
|
48
|
+
...Object.values(rpc).map((loader) => loader()),
|
|
49
|
+
...Object.values(sockets).map((loader) => loader()),
|
|
50
|
+
...Object.values(prompts).map((loader) => loader()),
|
|
51
|
+
])
|
|
52
|
+
.then(() => undefined)
|
|
53
|
+
/*
|
|
54
|
+
Clear the memo on failure so a transient import error (a
|
|
55
|
+
module that throws at load, fixed by the next HMR pass)
|
|
56
|
+
doesn't poison every later enumeration request for the
|
|
57
|
+
process lifetime. The rejection still propagates to this
|
|
58
|
+
caller; the reset only affects subsequent calls.
|
|
59
|
+
*/
|
|
60
|
+
.catch((error) => {
|
|
61
|
+
loading = undefined
|
|
62
|
+
throw error
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
return loading
|
|
66
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { NO_STORE } from '../../shared/CACHE_CONTROL_VALUES.ts'
|
|
2
|
+
import { streamCacheResolutions } from './streamCacheResolutions.ts'
|
|
3
|
+
import { streamFromIterator } from './streamFromIterator.ts'
|
|
4
|
+
import { takePendingStream } from './streamStash.ts'
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
The out-of-band resolution stream. The browser opens this once per streamed page
|
|
8
|
+
(token from `__SSR__.streamToken`) and reads newline-delimited StreamedResolution
|
|
9
|
+
objects as each pending {#await} fetch lands — draining the SAME in-flight
|
|
10
|
+
promises stashed during SSR, so handlers run once. A missing/expired token
|
|
11
|
+
(404) tells the client to re-fetch its placeholders live.
|
|
12
|
+
|
|
13
|
+
Returned directly (not via dispatchRequest), so it inherits the configured
|
|
14
|
+
`idleTimeout` as a bounded cap rather than the long-lived-stream disable; a cut
|
|
15
|
+
is recovered client-side off the fetch reader's clean EOF.
|
|
16
|
+
*/
|
|
17
|
+
export function resolveStreamResponse(token: string): Response {
|
|
18
|
+
const stash = takePendingStream(token)
|
|
19
|
+
if (!stash) {
|
|
20
|
+
return new Response('', { status: 404 })
|
|
21
|
+
}
|
|
22
|
+
const body = streamFromIterator(streamCacheResolutions(stash.store, stash.pending), {
|
|
23
|
+
encodeFrame: (resolution) => `${JSON.stringify(resolution)}\n`,
|
|
24
|
+
encodeError: () => '',
|
|
25
|
+
})
|
|
26
|
+
return new Response(body, {
|
|
27
|
+
headers: { 'Content-Type': 'application/x-ndjson', 'Cache-Control': NO_STORE },
|
|
28
|
+
})
|
|
29
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createCacheStore } from '../../shared/createCacheStore.ts'
|
|
2
|
+
import { log } from '../../shared/log.ts'
|
|
3
|
+
import type { AppModule } from '../AppModule.ts'
|
|
4
|
+
import { internalErrorResponse } from './internalErrorResponse.ts'
|
|
5
|
+
import { requestContext } from './requestContext.ts'
|
|
6
|
+
import type { RequestStore } from './types/RequestStore.ts'
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
Establishes the per-request scope and runs `body` inside it: a fresh
|
|
10
|
+
CacheStore plus request metadata published through the AsyncLocalStorage
|
|
11
|
+
RequestStore (so cache() and request()/server() resolve without threading
|
|
12
|
+
args), the app's handleError — or the framework's 500 fallback — on a thrown
|
|
13
|
+
body, and optional request logging. The single seam every dynamic route
|
|
14
|
+
crosses; extracted from createServer so the scope, error, and logging
|
|
15
|
+
behaviour is exercisable through this interface without booting a Bun server.
|
|
16
|
+
*/
|
|
17
|
+
export function runWithRequestScope(
|
|
18
|
+
req: Request,
|
|
19
|
+
options: { app?: AppModule; logRequests: boolean },
|
|
20
|
+
body: (store: RequestStore) => Promise<Response>,
|
|
21
|
+
): Promise<Response> {
|
|
22
|
+
const url = new URL(req.url)
|
|
23
|
+
const store: RequestStore = {
|
|
24
|
+
url,
|
|
25
|
+
req,
|
|
26
|
+
cache: createCacheStore(),
|
|
27
|
+
}
|
|
28
|
+
return requestContext.run(store, async () => {
|
|
29
|
+
const start = options.logRequests ? Bun.nanoseconds() : 0
|
|
30
|
+
let response: Response
|
|
31
|
+
try {
|
|
32
|
+
response = await body(store)
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (options.app?.handleError) {
|
|
35
|
+
response = await options.app.handleError(error, req)
|
|
36
|
+
} else {
|
|
37
|
+
log.error(error)
|
|
38
|
+
response = internalErrorResponse(error)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/*
|
|
42
|
+
Flush any cookies the handler set onto the outgoing response. Only when
|
|
43
|
+
a jar was materialized (cookies() was called) and only via append, so a
|
|
44
|
+
Set-Cookie the handler already placed on the response init survives.
|
|
45
|
+
*/
|
|
46
|
+
if (store.cookies) {
|
|
47
|
+
store.cookies.toSetCookieHeaders().forEach((header) => {
|
|
48
|
+
response.headers.append('set-cookie', header)
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
if (options.logRequests) {
|
|
52
|
+
const ms = (Bun.nanoseconds() - start) / 1e6
|
|
53
|
+
log.request(req.method, `${url.pathname}${url.search}`, response.status, ms)
|
|
54
|
+
}
|
|
55
|
+
return response
|
|
56
|
+
})
|
|
57
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Escapes characters that could prematurely terminate the surrounding <script>
|
|
3
|
+
tag or be interpreted as HTML comment delimiters when a JSON literal is
|
|
4
|
+
inlined into an HTML document. U+2028 (LS) and U+2029 (PS) are valid in JSON
|
|
5
|
+
but break a `<script>` tag's inline content because the JavaScript lexer
|
|
6
|
+
treats them as line terminators; encode them as Unicode escapes.
|
|
7
|
+
*/
|
|
8
|
+
const LINE_SEPARATOR = String.fromCharCode(0x2028)
|
|
9
|
+
const PARAGRAPH_SEPARATOR = String.fromCharCode(0x2029)
|
|
10
|
+
|
|
11
|
+
export function safeJsonForScript(value: unknown): string {
|
|
12
|
+
return JSON.stringify(value)
|
|
13
|
+
.replace(/</g, '\\u003c')
|
|
14
|
+
.replace(/-->/g, '--\\u003e')
|
|
15
|
+
.replaceAll(LINE_SEPARATOR, '\\u2028')
|
|
16
|
+
.replaceAll(PARAGRAPH_SEPARATOR, '\\u2029')
|
|
17
|
+
}
|