@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,17 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Holds the AbortController for the current page's resolution stream so a
|
|
3
|
+
client-side navigation can cancel it — freeing the connection and stopping the
|
|
4
|
+
server drain — instead of letting it run to completion for a page that's gone.
|
|
5
|
+
Setting a new controller aborts any prior one.
|
|
6
|
+
*/
|
|
7
|
+
let current: AbortController | undefined
|
|
8
|
+
|
|
9
|
+
export function setPageStreamController(controller: AbortController): void {
|
|
10
|
+
current?.abort()
|
|
11
|
+
current = controller
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function abortPageStream(): void {
|
|
15
|
+
current?.abort()
|
|
16
|
+
current = undefined
|
|
17
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { StreamingDeferred } from './types/StreamingDeferred.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
The single placeholder-recovery primitive: settles a deferred {#await} read with
|
|
5
|
+
a live re-fetch of its request. Used whenever a streamed resolution can't supply
|
|
6
|
+
warm data — a `{ key, miss }` marker (non-snapshottable body) or a placeholder
|
|
7
|
+
the stream never settled (clean EOF with leftovers, or a cut). Keeping the
|
|
8
|
+
re-fetch policy in one place means the apply path and the flush path can't drift.
|
|
9
|
+
*/
|
|
10
|
+
export function refetchPlaceholder(deferred: StreamingDeferred): void {
|
|
11
|
+
deferred.resolve(fetch(deferred.request))
|
|
12
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { browserClientFlags } from '../shared/browserClientFlags.ts'
|
|
2
|
+
import { buildRpcRequest } from '../shared/buildRpcRequest.ts'
|
|
3
|
+
import { createRemoteFunction } from '../shared/createRemoteFunction.ts'
|
|
4
|
+
import type { HttpVerb } from '../shared/types/HttpVerb.ts'
|
|
5
|
+
import type { RemoteFunction } from '../shared/types/RemoteFunction.ts'
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
Client-side substitute for a verb-defined handler. The bundler emits one
|
|
9
|
+
call per verb export inside an `$rpc/**` module (GET / POST / …): server
|
|
10
|
+
target uses defineVerb (real handler), browser target uses remoteProxy
|
|
11
|
+
(fetch over the network). Both paths produce identical RemoteFunction
|
|
12
|
+
shapes and identical WeakMap metadata so cache() works the same on either
|
|
13
|
+
side.
|
|
14
|
+
|
|
15
|
+
`url` is the flat rpc route. Args go in the JSON body (POST/PUT/PATCH) or
|
|
16
|
+
the query string (GET/DELETE/HEAD). Plain `fn(args)` decodes the Response
|
|
17
|
+
by Content-Type and throws HttpError on non-2xx; `.raw(args)` is the
|
|
18
|
+
escape hatch that returns the Response untouched.
|
|
19
|
+
*/
|
|
20
|
+
export function remoteProxy<Args, Return>(
|
|
21
|
+
method: HttpVerb,
|
|
22
|
+
url: string,
|
|
23
|
+
): RemoteFunction<Args, Return> {
|
|
24
|
+
return createRemoteFunction<Args, Return>({
|
|
25
|
+
method,
|
|
26
|
+
url,
|
|
27
|
+
clients: browserClientFlags,
|
|
28
|
+
buildRequest: (args) =>
|
|
29
|
+
buildRpcRequest({ method, url, args, baseUrl: window.location.href }),
|
|
30
|
+
/*
|
|
31
|
+
Forcing `getRequest()` once builds the Request and seeds the
|
|
32
|
+
cache meta thunk in createRemoteFunction with the same instance,
|
|
33
|
+
so cache() readers don't reconstruct it.
|
|
34
|
+
*/
|
|
35
|
+
invoke: (_args, getRequest) => fetch(getRequest()),
|
|
36
|
+
})
|
|
37
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import type { SocketClientFrame } from '../server/sockets/types/SocketClientFrame.ts'
|
|
2
|
+
import type { SocketServerFrame } from '../server/sockets/types/SocketServerFrame.ts'
|
|
3
|
+
|
|
4
|
+
type SubCallbacks = {
|
|
5
|
+
onMessage(message: unknown): void
|
|
6
|
+
onError(message: string): void
|
|
7
|
+
onEnd(): void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type Channel = {
|
|
11
|
+
subscribe(
|
|
12
|
+
sub: string,
|
|
13
|
+
socket: string,
|
|
14
|
+
replay: number | undefined,
|
|
15
|
+
callbacks: SubCallbacks,
|
|
16
|
+
): void
|
|
17
|
+
unsubscribe(sub: string): void
|
|
18
|
+
publish(socket: string, message: unknown): void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const SOCKETS_PATH = '/__belte/sockets'
|
|
22
|
+
|
|
23
|
+
let singleton: Channel | undefined
|
|
24
|
+
|
|
25
|
+
/*
|
|
26
|
+
Lazily opens the single multiplexed ws used by every socket proxy on
|
|
27
|
+
the page. Routes inbound frames:
|
|
28
|
+
`msg` → all local subs of that socket
|
|
29
|
+
`end` → the matching sub
|
|
30
|
+
`err` → the matching sub
|
|
31
|
+
|
|
32
|
+
`msg` frames carry no sub id: one publish from the server fans out to
|
|
33
|
+
every connected ws via Bun's native publish, and each ws delivers the
|
|
34
|
+
message to every local sub of that socket. `end`/`err` are per-sub
|
|
35
|
+
because they're subscription-lifecycle events, not data.
|
|
36
|
+
|
|
37
|
+
Outbound frames sent before `ws.onopen` fires are queued and flushed
|
|
38
|
+
on open. The channel reconnects on close with bounded backoff;
|
|
39
|
+
in-flight subs are torn down with a synthetic error so consumers'
|
|
40
|
+
`for await` loops can surface the disconnect, then the connection
|
|
41
|
+
comes back up and fresh subs can be opened. We intentionally do not
|
|
42
|
+
silently re-subscribe across a reconnect — most socket consumers need
|
|
43
|
+
to reconcile state on a fresh connection (e.g. re-fetch a snapshot
|
|
44
|
+
before reapplying deltas), so the framework hands the disconnect to
|
|
45
|
+
user code instead of papering over it.
|
|
46
|
+
*/
|
|
47
|
+
export function getSocketChannel(): Channel {
|
|
48
|
+
if (singleton) {
|
|
49
|
+
return singleton
|
|
50
|
+
}
|
|
51
|
+
const subs = new Map<string, { socket: string; callbacks: SubCallbacks }>()
|
|
52
|
+
const subsBySocket = new Map<string, Set<string>>()
|
|
53
|
+
let ws: WebSocket | undefined
|
|
54
|
+
let pendingSends: string[] = []
|
|
55
|
+
let backoffMs = 250
|
|
56
|
+
|
|
57
|
+
function flushPending(): void {
|
|
58
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
for (const message of pendingSends) {
|
|
62
|
+
ws.send(message)
|
|
63
|
+
}
|
|
64
|
+
pendingSends = []
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function send(frame: SocketClientFrame): void {
|
|
68
|
+
const message = JSON.stringify(frame)
|
|
69
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
70
|
+
ws.send(message)
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
pendingSends.push(message)
|
|
74
|
+
connect()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function connect(): void {
|
|
78
|
+
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
const scheme = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
|
82
|
+
ws = new WebSocket(`${scheme}//${window.location.host}${SOCKETS_PATH}`)
|
|
83
|
+
ws.addEventListener('open', () => {
|
|
84
|
+
backoffMs = 250
|
|
85
|
+
flushPending()
|
|
86
|
+
})
|
|
87
|
+
ws.addEventListener('message', (event) => {
|
|
88
|
+
let frame: SocketServerFrame
|
|
89
|
+
try {
|
|
90
|
+
frame = JSON.parse(event.data) as SocketServerFrame
|
|
91
|
+
} catch {
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
if (frame.type === 'msg') {
|
|
95
|
+
/*
|
|
96
|
+
One Bun-published frame fans out to every local sub of
|
|
97
|
+
that socket on this ws — addressed by socket name, not
|
|
98
|
+
per-sub id.
|
|
99
|
+
*/
|
|
100
|
+
const targets = subsBySocket.get(frame.socket)
|
|
101
|
+
if (!targets) {
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
for (const subId of targets) {
|
|
105
|
+
subs.get(subId)?.callbacks.onMessage(frame.message)
|
|
106
|
+
}
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
if (frame.type === 'end') {
|
|
110
|
+
const sub = subs.get(frame.sub)
|
|
111
|
+
if (!sub) {
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
dropSub(frame.sub)
|
|
115
|
+
sub.callbacks.onEnd()
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
if (frame.type === 'err') {
|
|
119
|
+
const sub = subs.get(frame.sub)
|
|
120
|
+
if (!sub) {
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
dropSub(frame.sub)
|
|
124
|
+
sub.callbacks.onError(frame.message)
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
ws.addEventListener('close', () => {
|
|
129
|
+
const active = [...subs.entries()]
|
|
130
|
+
subs.clear()
|
|
131
|
+
subsBySocket.clear()
|
|
132
|
+
for (const [, sub] of active) {
|
|
133
|
+
sub.callbacks.onError('socket channel disconnected')
|
|
134
|
+
}
|
|
135
|
+
/*
|
|
136
|
+
Drop any queued frames too. We've just torn down every local
|
|
137
|
+
sub, so replaying their `sub`/`unsub`/`pub` frames on
|
|
138
|
+
reconnect would open ghost subscriptions on the server that
|
|
139
|
+
no client object tracks (and never gets an `unsub`). This
|
|
140
|
+
keeps the "no silent re-subscribe across a reconnect"
|
|
141
|
+
contract above honest — consumers re-open fresh subs.
|
|
142
|
+
*/
|
|
143
|
+
const hadPending = pendingSends.length > 0
|
|
144
|
+
pendingSends = []
|
|
145
|
+
ws = undefined
|
|
146
|
+
if (active.length === 0 && !hadPending) {
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
setTimeout(connect, backoffMs)
|
|
150
|
+
backoffMs = Math.min(backoffMs * 2, 5000)
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function dropSub(id: string): void {
|
|
155
|
+
const entry = subs.get(id)
|
|
156
|
+
if (!entry) {
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
subs.delete(id)
|
|
160
|
+
const set = subsBySocket.get(entry.socket)
|
|
161
|
+
if (set) {
|
|
162
|
+
set.delete(id)
|
|
163
|
+
if (set.size === 0) {
|
|
164
|
+
subsBySocket.delete(entry.socket)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
singleton = {
|
|
170
|
+
subscribe(id, socket, replay, callbacks) {
|
|
171
|
+
subs.set(id, { socket, callbacks })
|
|
172
|
+
let set = subsBySocket.get(socket)
|
|
173
|
+
if (!set) {
|
|
174
|
+
set = new Set()
|
|
175
|
+
subsBySocket.set(socket, set)
|
|
176
|
+
}
|
|
177
|
+
set.add(id)
|
|
178
|
+
send({ type: 'sub', sub: id, socket, replay })
|
|
179
|
+
},
|
|
180
|
+
unsubscribe(id) {
|
|
181
|
+
if (!subs.has(id)) {
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
dropSub(id)
|
|
185
|
+
send({ type: 'unsub', sub: id })
|
|
186
|
+
},
|
|
187
|
+
publish(socket, message) {
|
|
188
|
+
send({ type: 'pub', socket, message })
|
|
189
|
+
},
|
|
190
|
+
}
|
|
191
|
+
return singleton
|
|
192
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Socket } from '../server/sockets/types/Socket.ts'
|
|
2
|
+
import { browserClientFlags } from '../shared/browserClientFlags.ts'
|
|
3
|
+
import { createPushIterator } from '../shared/createPushIterator.ts'
|
|
4
|
+
import { getSocketChannel } from './socketChannel.ts'
|
|
5
|
+
|
|
6
|
+
let nextId = 0
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
Client-side substitute for a server-declared Socket. The bundler emits
|
|
10
|
+
one call per socket export under `src/server/sockets/`: server target uses
|
|
11
|
+
defineSocket (real fan-out), browser target uses socketProxy (subscribe
|
|
12
|
+
over the multiplexed ws channel). Both paths produce identical Socket
|
|
13
|
+
shapes so user code reads the same on either side.
|
|
14
|
+
|
|
15
|
+
Bare iteration opens a subscription with full history replay; `.tail(n)`
|
|
16
|
+
opens one that replays the last `n` items (default `0`, clamped server-
|
|
17
|
+
side to the topic's history max). Each subscription mints its own id
|
|
18
|
+
used to route lifecycle frames (`end`, `err`). Calling `.publish` sends
|
|
19
|
+
a `pub` frame the server validates against the topic's
|
|
20
|
+
`allowClientPublish` policy — there is no client-side enforcement, so a
|
|
21
|
+
publish attempt on a server-only topic is silently dropped server-side.
|
|
22
|
+
|
|
23
|
+
Backpressure is unbounded — a slow consumer with a chatty socket will
|
|
24
|
+
grow the per-iterator buffer; bounded policies belong in a future
|
|
25
|
+
socketProxy API, not the wire layer.
|
|
26
|
+
*/
|
|
27
|
+
export function socketProxy<T>(name: string): Socket<T> {
|
|
28
|
+
/*
|
|
29
|
+
replay === undefined → full history replay (bare for-await);
|
|
30
|
+
replay: number → trailing-n replay, clamped by the server.
|
|
31
|
+
*/
|
|
32
|
+
function iterate(replay: number | undefined): AsyncIterable<T> {
|
|
33
|
+
return {
|
|
34
|
+
[Symbol.asyncIterator](): AsyncIterator<T, void, undefined> {
|
|
35
|
+
const id = `s${++nextId}`
|
|
36
|
+
const channel = getSocketChannel()
|
|
37
|
+
const iter = createPushIterator<T>(() => channel.unsubscribe(id))
|
|
38
|
+
channel.subscribe(id, name, replay, {
|
|
39
|
+
onMessage: (value) => iter.push(value as T),
|
|
40
|
+
onEnd: () => iter.end(),
|
|
41
|
+
onError: (message) => iter.error(message),
|
|
42
|
+
})
|
|
43
|
+
return iter
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
name,
|
|
50
|
+
clients: browserClientFlags,
|
|
51
|
+
publish(message: T) {
|
|
52
|
+
getSocketChannel().publish(name, message)
|
|
53
|
+
},
|
|
54
|
+
tail: (count = 0) => iterate(count),
|
|
55
|
+
[Symbol.asyncIterator]: () => iterate(undefined)[Symbol.asyncIterator](),
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { hydrate } from 'svelte'
|
|
2
|
+
import App from '../../App.svelte'
|
|
3
|
+
import { createCacheStore } from '../shared/createCacheStore.ts'
|
|
4
|
+
import { setCacheStoreResolver } from '../shared/setCacheStoreResolver.ts'
|
|
5
|
+
import { setGlobalCacheStoreResolver } from '../shared/setGlobalCacheStoreResolver.ts'
|
|
6
|
+
import type { CacheSnapshotEntry } from '../shared/types/CacheSnapshotEntry.ts'
|
|
7
|
+
import type { CacheStore } from '../shared/types/CacheStore.ts'
|
|
8
|
+
import type { StreamingPlaceholder } from '../shared/types/StreamingPlaceholder.ts'
|
|
9
|
+
import { cacheEntryFromSnapshot } from './cacheEntryFromSnapshot.ts'
|
|
10
|
+
import { installStreamingPlaceholders } from './installStreamingPlaceholders.ts'
|
|
11
|
+
import { openResolveStream } from './openResolveStream.ts'
|
|
12
|
+
import { bindPage, handlePopstate, navigate, page, renderState } from './page.svelte.ts'
|
|
13
|
+
import type { Layouts } from './types/Layouts.ts'
|
|
14
|
+
import type { Pages } from './types/Pages.ts'
|
|
15
|
+
|
|
16
|
+
declare global {
|
|
17
|
+
interface Window {
|
|
18
|
+
__SSR__: {
|
|
19
|
+
route: string
|
|
20
|
+
params: Record<string, string>
|
|
21
|
+
cache?: CacheSnapshotEntry[]
|
|
22
|
+
/* Pending {#await} keys the client pre-creates placeholders for. */
|
|
23
|
+
streaming?: StreamingPlaceholder[]
|
|
24
|
+
/* Single-use token for the out-of-band resolution stream. */
|
|
25
|
+
streamToken?: string
|
|
26
|
+
/* A server-rendered error.svelte page — static, nothing to hydrate. */
|
|
27
|
+
error?: boolean
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/*
|
|
33
|
+
Pre-populates the client cache store with response entries captured during SSR.
|
|
34
|
+
Each becomes an already-resolved Response so the first hydration pass finds the
|
|
35
|
+
data via cache() without a network round-trip.
|
|
36
|
+
*/
|
|
37
|
+
function hydrateCacheFromSnapshot(store: CacheStore, snapshot: CacheSnapshotEntry[]): void {
|
|
38
|
+
for (const entry of snapshot) {
|
|
39
|
+
store.entries.set(entry.key, cacheEntryFromSnapshot(entry))
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isInternalLinkEvent(event: MouseEvent): HTMLAnchorElement | undefined {
|
|
44
|
+
if (event.defaultPrevented) {
|
|
45
|
+
return undefined
|
|
46
|
+
}
|
|
47
|
+
if (event.button !== 0) {
|
|
48
|
+
return undefined
|
|
49
|
+
}
|
|
50
|
+
if (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) {
|
|
51
|
+
return undefined
|
|
52
|
+
}
|
|
53
|
+
const anchor = (event.target as HTMLElement | null)?.closest?.('a')
|
|
54
|
+
if (!anchor) {
|
|
55
|
+
return undefined
|
|
56
|
+
}
|
|
57
|
+
if (anchor.target && anchor.target !== '_self') {
|
|
58
|
+
return undefined
|
|
59
|
+
}
|
|
60
|
+
if (anchor.hasAttribute('download')) {
|
|
61
|
+
return undefined
|
|
62
|
+
}
|
|
63
|
+
if (anchor.getAttribute('rel')?.includes('external')) {
|
|
64
|
+
return undefined
|
|
65
|
+
}
|
|
66
|
+
const href = anchor.getAttribute('href')
|
|
67
|
+
if (!href || href.startsWith('#')) {
|
|
68
|
+
return undefined
|
|
69
|
+
}
|
|
70
|
+
const url = new URL(href, window.location.href)
|
|
71
|
+
if (url.origin !== window.location.origin) {
|
|
72
|
+
return undefined
|
|
73
|
+
}
|
|
74
|
+
return anchor
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/*
|
|
78
|
+
Hydrates the SSR'd document against the SSR payload on `window.__SSR__`,
|
|
79
|
+
then intercepts internal link clicks (delegating to navigate) and popstate
|
|
80
|
+
events. The page module owns the route/Page/Layout state and the
|
|
81
|
+
URL-resolution logic; this entry just wires the cache store, runs the
|
|
82
|
+
initial bind, and attaches the global listeners. App.svelte receives the
|
|
83
|
+
public `page` proxy plus the internal renderState so the same reactive
|
|
84
|
+
objects update across navigations.
|
|
85
|
+
*/
|
|
86
|
+
export async function startClient({
|
|
87
|
+
pages,
|
|
88
|
+
layouts,
|
|
89
|
+
}: {
|
|
90
|
+
pages: Pages
|
|
91
|
+
layouts?: Layouts
|
|
92
|
+
}): Promise<void> {
|
|
93
|
+
const target = document.getElementById('app')
|
|
94
|
+
if (!target) {
|
|
95
|
+
throw new Error('[belte] missing #app target')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/*
|
|
99
|
+
A server-rendered error.svelte (404 / page-render failure) ships static HTML
|
|
100
|
+
with no route to hydrate against — leave the markup as-is and wire nothing.
|
|
101
|
+
*/
|
|
102
|
+
if (window.__SSR__.error) {
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const cacheStore = createCacheStore()
|
|
107
|
+
setCacheStoreResolver(() => cacheStore)
|
|
108
|
+
/* One tab store: cache(fn, { global: true }) shares it, so global is a no-op here. */
|
|
109
|
+
setGlobalCacheStoreResolver(() => cacheStore)
|
|
110
|
+
if (window.__SSR__.cache) {
|
|
111
|
+
hydrateCacheFromSnapshot(cacheStore, window.__SSR__.cache)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/*
|
|
115
|
+
Install placeholders for pending {#await} keys before hydrate(), so cache()
|
|
116
|
+
reads hit a placeholder on first evaluation instead of firing their own
|
|
117
|
+
fetch, then open the out-of-band resolution stream to settle them. The fetch
|
|
118
|
+
runs in the background — hydration doesn't wait on it.
|
|
119
|
+
*/
|
|
120
|
+
const deferreds = installStreamingPlaceholders(cacheStore, window.__SSR__.streaming ?? [])
|
|
121
|
+
if (window.__SSR__.streamToken && deferreds.size > 0) {
|
|
122
|
+
void openResolveStream(window.__SSR__.streamToken, cacheStore, deferreds)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
await bindPage({ pages, layouts, ssr: window.__SSR__ })
|
|
127
|
+
hydrate(App, { target, props: { state: { page, render: renderState } } })
|
|
128
|
+
} catch (err) {
|
|
129
|
+
console.error('[belte] initial hydration failed', err)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
document.addEventListener('click', (event) => {
|
|
133
|
+
const anchor = isInternalLinkEvent(event)
|
|
134
|
+
if (!anchor) {
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
const url = new URL(anchor.href, window.location.href)
|
|
138
|
+
/*
|
|
139
|
+
Hash-only same-page navigations fall through to the browser so the
|
|
140
|
+
native scroll-into-view for `#anchor` targets keeps working.
|
|
141
|
+
Anything else (pathname, search, or pathname+hash combo) goes
|
|
142
|
+
through navigate() — it pushes history, refreshes page state, and
|
|
143
|
+
short-circuits the JSON resolve when only search/hash differ.
|
|
144
|
+
*/
|
|
145
|
+
if (url.pathname === window.location.pathname && url.search === window.location.search) {
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
event.preventDefault()
|
|
149
|
+
void navigate(`${url.pathname}${url.search}${url.hash}`)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
window.addEventListener('popstate', handlePopstate)
|
|
153
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { createSubscriber } from 'svelte/reactivity'
|
|
2
|
+
import type { Subscribable } from '../shared/types/Subscribable.ts'
|
|
3
|
+
|
|
4
|
+
type SubscriptionStatus = 'pending' | 'open' | 'done' | 'error'
|
|
5
|
+
|
|
6
|
+
type Entry<T> = {
|
|
7
|
+
latest: T | undefined
|
|
8
|
+
error: Error | undefined
|
|
9
|
+
status: SubscriptionStatus
|
|
10
|
+
tap: () => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const registry = new Map<string, Entry<unknown>>()
|
|
14
|
+
|
|
15
|
+
/*
|
|
16
|
+
Reactive consumer for streaming sources. Takes a Subscribable<T> — the
|
|
17
|
+
shape both `Socket<T>` (declared under src/server/sockets/) and the result of
|
|
18
|
+
`fn.stream(args)` satisfy:
|
|
19
|
+
|
|
20
|
+
const latest = $derived(subscribe(chat)) // socket
|
|
21
|
+
const latest = $derived(subscribe(tickFeed.stream())) // rpc stream (no args)
|
|
22
|
+
const latest = $derived(subscribe(countLog.stream({ to: 5 }))) // rpc stream
|
|
23
|
+
|
|
24
|
+
Lifecycle mirrors cache(): the entry's tracker is a Svelte
|
|
25
|
+
createSubscriber, so the first $derived read in a tracking scope opens
|
|
26
|
+
the underlying iterator (with history replay on a Socket, or a fresh
|
|
27
|
+
fetch on an rpc stream), and the last $derived to stop reading closes
|
|
28
|
+
it. Many $deriveds reading the same source share one underlying
|
|
29
|
+
subscription — the registry dedupes by `subscribable.name`, which is
|
|
30
|
+
the socket name for declared sockets and `keyForRemoteCall(method, url,
|
|
31
|
+
args)` for rpc streams. So passing fresh `fn.stream(args)` Subscribables
|
|
32
|
+
across re-renders is safe: same args → same key → shared subscription.
|
|
33
|
+
|
|
34
|
+
Subscribe is a no-op on the server (returns undefined) — SSR can't
|
|
35
|
+
keep a stream open across the request boundary. Pages that want a
|
|
36
|
+
seeded value in the initial HTML should fetch via cache() against an
|
|
37
|
+
HTTP rpc handler and layer subscribe() on top for live updates after
|
|
38
|
+
hydration.
|
|
39
|
+
|
|
40
|
+
Errors are surfaced through subscribe.error(x) rather than thrown, so
|
|
41
|
+
reading `latest` from a $derived can't crash the component. Status
|
|
42
|
+
distinguishes "haven't received the first frame" (pending) from
|
|
43
|
+
"stream ended cleanly" (done) and "wire layer surfaced an error"
|
|
44
|
+
(error).
|
|
45
|
+
*/
|
|
46
|
+
export function subscribe<T>(subscribable: Subscribable<T>): T | undefined {
|
|
47
|
+
return readField(subscribable, 'latest') as T | undefined
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
subscribe.error = function subscribeError<T>(subscribable: Subscribable<T>): Error | undefined {
|
|
51
|
+
return readField(subscribable, 'error') as Error | undefined
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
subscribe.status = function subscribeStatus<T>(subscribable: Subscribable<T>): SubscriptionStatus {
|
|
55
|
+
return (readField(subscribable, 'status') as SubscriptionStatus | undefined) ?? 'pending'
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readField<T, K extends keyof Entry<T>>(
|
|
59
|
+
subscribable: Subscribable<T>,
|
|
60
|
+
field: K,
|
|
61
|
+
): Entry<T>[K] | undefined {
|
|
62
|
+
if (typeof window === 'undefined') {
|
|
63
|
+
if (field === 'status') {
|
|
64
|
+
return 'pending' as Entry<T>[K]
|
|
65
|
+
}
|
|
66
|
+
return undefined
|
|
67
|
+
}
|
|
68
|
+
const entry = getOrCreateEntry(subscribable) as Entry<T>
|
|
69
|
+
entry.tap()
|
|
70
|
+
return entry[field]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getOrCreateEntry<T>(subscribable: Subscribable<T>): Entry<T> {
|
|
74
|
+
const key = subscribable.name
|
|
75
|
+
const cached = registry.get(key) as Entry<T> | undefined
|
|
76
|
+
if (cached) {
|
|
77
|
+
return cached
|
|
78
|
+
}
|
|
79
|
+
const entry: Entry<T> = {
|
|
80
|
+
latest: undefined,
|
|
81
|
+
error: undefined,
|
|
82
|
+
status: 'pending',
|
|
83
|
+
tap: () => undefined,
|
|
84
|
+
}
|
|
85
|
+
entry.tap = createSubscriber((update) => {
|
|
86
|
+
entry.latest = undefined
|
|
87
|
+
entry.error = undefined
|
|
88
|
+
entry.status = 'pending'
|
|
89
|
+
const iterator = subscribable[Symbol.asyncIterator]()
|
|
90
|
+
let cancelled = false
|
|
91
|
+
;(async () => {
|
|
92
|
+
try {
|
|
93
|
+
while (!cancelled) {
|
|
94
|
+
const next = await iterator.next()
|
|
95
|
+
if (next.done) {
|
|
96
|
+
if (!cancelled) {
|
|
97
|
+
if (entry.status !== 'error') {
|
|
98
|
+
entry.status = 'done'
|
|
99
|
+
}
|
|
100
|
+
update()
|
|
101
|
+
}
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
entry.latest = next.value
|
|
105
|
+
entry.status = 'open'
|
|
106
|
+
update()
|
|
107
|
+
}
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (!cancelled) {
|
|
110
|
+
entry.error = error instanceof Error ? error : new Error(String(error))
|
|
111
|
+
entry.status = 'error'
|
|
112
|
+
update()
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
})()
|
|
116
|
+
return () => {
|
|
117
|
+
cancelled = true
|
|
118
|
+
iterator.return?.(undefined)?.catch(() => undefined)
|
|
119
|
+
/*
|
|
120
|
+
Identity-guard the eviction: if a fresh Subscribable with the
|
|
121
|
+
same name has already replaced us in the registry, this stale
|
|
122
|
+
cleanup must not nuke the new entry.
|
|
123
|
+
*/
|
|
124
|
+
if (registry.get(key) === (entry as Entry<unknown>)) {
|
|
125
|
+
registry.delete(key)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
registry.set(key, entry as Entry<unknown>)
|
|
130
|
+
return entry
|
|
131
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Component } from 'svelte'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Manifest of directory prefix → error.svelte module loader. The deepest prefix
|
|
5
|
+
that is an ancestor of the failed path wins (nearest-only, like layouts). An
|
|
6
|
+
error.svelte renders on the server for an unknown route (404) or a throw during
|
|
7
|
+
a page render; the component receives `{ status, message }` props.
|
|
8
|
+
*/
|
|
9
|
+
export type Errors = Record<string, () => Promise<{ default: Component }>>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Component } from 'svelte'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Manifest of route URL → page.svelte module loader. Produced by the resolver
|
|
5
|
+
plugin from `page.svelte` files anywhere under src/browser/pages.
|
|
6
|
+
*/
|
|
7
|
+
export type Pages = Record<string, () => Promise<{ default: Component }>>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/*
|
|
2
|
+
A pending {#await} read's deferred entry. `resolve` settles the placeholder
|
|
3
|
+
promise the cache handed out (with the streamed Response or a live re-fetch);
|
|
4
|
+
`request` is what to re-fetch on a miss or a cut stream.
|
|
5
|
+
*/
|
|
6
|
+
export type StreamingDeferred = {
|
|
7
|
+
resolve: (response: Promise<Response>) => void
|
|
8
|
+
request: Request
|
|
9
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { BundleMenuItem } from './BundleMenuItem.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
A top-level bundle menu, inserted into the macOS menu bar between the standard
|
|
5
|
+
Edit and Window menus. `label` titles the menu; `items` are its entries top to
|
|
6
|
+
bottom.
|
|
7
|
+
*/
|
|
8
|
+
export type BundleMenu = {
|
|
9
|
+
label: string
|
|
10
|
+
items: BundleMenuItem[]
|
|
11
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/*
|
|
2
|
+
A single entry in a bundle menu. Serializable data — the native shim builds the
|
|
3
|
+
matching NSMenuItem. Either a divider or a clickable item that dispatches a
|
|
4
|
+
`belte:menu` CustomEvent into the page (detail `{ name }`); the app's own code
|
|
5
|
+
handles it:
|
|
6
|
+
|
|
7
|
+
window.addEventListener('belte:menu', (event) => {
|
|
8
|
+
if (event.detail.name === 'sync') syncNow()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
Emitting an event (rather than calling a verb directly) is what lets a menu
|
|
12
|
+
drive parameterised work: a click carries no arguments, so the app computes
|
|
13
|
+
them and makes the call itself. `shortcut` is the key for the Cmd-based
|
|
14
|
+
equivalent (e.g. `'r'` → Cmd-R).
|
|
15
|
+
|
|
16
|
+
A `navigate` item moves the window instead of talking to the page: clicking it
|
|
17
|
+
calls `webview_navigate` with the given URL (the native side, on the UI thread).
|
|
18
|
+
That's how the built-in Server menu drives the connect screen — `emit` reaches
|
|
19
|
+
the loaded page, `navigate` repoints the window itself.
|
|
20
|
+
*/
|
|
21
|
+
export type BundleMenuItem =
|
|
22
|
+
| { separator: true }
|
|
23
|
+
| { label: string; shortcut?: string; emit: string }
|
|
24
|
+
| { label: string; shortcut?: string; navigate: string }
|