@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,311 @@
|
|
|
1
|
+
import type { ServerWebSocket } from 'bun'
|
|
2
|
+
import { log } from '../../shared/log.ts'
|
|
3
|
+
import { memoizeByKey } from '../../shared/memoizeByKey.ts'
|
|
4
|
+
import { error } from '../error.ts'
|
|
5
|
+
import { json } from '../json.ts'
|
|
6
|
+
import { sse } from '../sse.ts'
|
|
7
|
+
import { lookupSocket } from './lookupSocket.ts'
|
|
8
|
+
import { recentHistory } from './recentHistory.ts'
|
|
9
|
+
import type { SocketClientFrame } from './types/SocketClientFrame.ts'
|
|
10
|
+
import type { SocketRoutes } from './types/SocketRoutes.ts'
|
|
11
|
+
import type { SocketServerFrame } from './types/SocketServerFrame.ts'
|
|
12
|
+
|
|
13
|
+
// Reused across every inbound binary frame rather than allocated per message.
|
|
14
|
+
const textDecoder = new TextDecoder()
|
|
15
|
+
|
|
16
|
+
type SocketDispatcher = {
|
|
17
|
+
open(ws: ServerWebSocket<unknown>): void
|
|
18
|
+
message(ws: ServerWebSocket<unknown>, data: string | Buffer): void
|
|
19
|
+
close(ws: ServerWebSocket<unknown>): void
|
|
20
|
+
rest(req: Request, name: string): Promise<Response>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/*
|
|
24
|
+
Per-connection state: which sockets this ws is currently subscribed to
|
|
25
|
+
(at the Bun-topic level), and which `sub` ids map to which socket. One
|
|
26
|
+
ws can hold multiple subs against the same socket (e.g. one with
|
|
27
|
+
history, one without); the Bun-topic subscription is reference-counted
|
|
28
|
+
so we only `ws.unsubscribe` when the last local sub drops.
|
|
29
|
+
*/
|
|
30
|
+
type ConnectionState = {
|
|
31
|
+
subToSocket: Map<string, string>
|
|
32
|
+
socketSubs: Map<string, Set<string>>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/*
|
|
36
|
+
Bridges the framework's socket registry to a single ws per client. All
|
|
37
|
+
sockets multiplex over `/__belte/sockets`. Steady-state fan-out rides
|
|
38
|
+
Bun's native `server.publish('socket:<name>', frame)` so the dispatcher
|
|
39
|
+
is only on the path for sub/unsub bookkeeping and client-initiated pub
|
|
40
|
+
validation; the published `msg` frames go from publisher to subscribers
|
|
41
|
+
without touching JS per frame.
|
|
42
|
+
|
|
43
|
+
`sub` opens a subscription: history is replayed (unless the client
|
|
44
|
+
passed `tail: true`) directly to this ws, then the ws is added to the
|
|
45
|
+
Bun topic. `unsub` drops the local sub and unsubscribes the ws from
|
|
46
|
+
the Bun topic if no other local subs remain. `pub` validates the
|
|
47
|
+
socket's `allowClientPublish` policy and calls `socket.publish` —
|
|
48
|
+
which fans out to in-process iterators and republishes through Bun
|
|
49
|
+
to other connected clients.
|
|
50
|
+
|
|
51
|
+
Module-level lookups are cached per socket name: loading a socket
|
|
52
|
+
module triggers its `defineSocket` call, which inserts into the
|
|
53
|
+
registry. After that the dispatcher just reads the registry.
|
|
54
|
+
*/
|
|
55
|
+
export function createSocketDispatcher(sockets: SocketRoutes): SocketDispatcher {
|
|
56
|
+
const connections = new WeakMap<ServerWebSocket<unknown>, ConnectionState>()
|
|
57
|
+
|
|
58
|
+
const ensureLoaded = memoizeByKey((name): Promise<void> | undefined => {
|
|
59
|
+
const loader = sockets[name]
|
|
60
|
+
return loader ? loader().then(() => undefined) : undefined
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
function send(ws: ServerWebSocket<unknown>, frame: SocketServerFrame): void {
|
|
64
|
+
if (ws.readyState !== WebSocket.OPEN) {
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
ws.send(JSON.stringify(frame))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function addSub(state: ConnectionState, name: string, sub: string): boolean {
|
|
71
|
+
state.subToSocket.set(sub, name)
|
|
72
|
+
let subs = state.socketSubs.get(name)
|
|
73
|
+
if (!subs) {
|
|
74
|
+
subs = new Set()
|
|
75
|
+
state.socketSubs.set(name, subs)
|
|
76
|
+
}
|
|
77
|
+
const wasEmpty = subs.size === 0
|
|
78
|
+
subs.add(sub)
|
|
79
|
+
return wasEmpty
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function removeSub(state: ConnectionState, sub: string): string | undefined {
|
|
83
|
+
const name = state.subToSocket.get(sub)
|
|
84
|
+
if (!name) {
|
|
85
|
+
return undefined
|
|
86
|
+
}
|
|
87
|
+
state.subToSocket.delete(sub)
|
|
88
|
+
const subs = state.socketSubs.get(name)
|
|
89
|
+
if (!subs) {
|
|
90
|
+
return undefined
|
|
91
|
+
}
|
|
92
|
+
subs.delete(sub)
|
|
93
|
+
if (subs.size === 0) {
|
|
94
|
+
state.socketSubs.delete(name)
|
|
95
|
+
return name
|
|
96
|
+
}
|
|
97
|
+
return undefined
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function handleSub(
|
|
101
|
+
ws: ServerWebSocket<unknown>,
|
|
102
|
+
state: ConnectionState,
|
|
103
|
+
frame: Extract<SocketClientFrame, { type: 'sub' }>,
|
|
104
|
+
): Promise<void> {
|
|
105
|
+
// Reject this sub: emit the error then the terminal end frame for its id.
|
|
106
|
+
function fail(message: string): void {
|
|
107
|
+
send(ws, { type: 'err', sub: frame.sub, message })
|
|
108
|
+
send(ws, { type: 'end', sub: frame.sub })
|
|
109
|
+
}
|
|
110
|
+
const loader = ensureLoaded(frame.socket)
|
|
111
|
+
if (!loader) {
|
|
112
|
+
return fail(`[belte] no socket registered at ${frame.socket}`)
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
await loader
|
|
116
|
+
} catch (error) {
|
|
117
|
+
log.error(error)
|
|
118
|
+
return fail(error instanceof Error ? error.message : String(error))
|
|
119
|
+
}
|
|
120
|
+
const entry = lookupSocket(frame.socket)
|
|
121
|
+
if (!entry) {
|
|
122
|
+
return fail(`[belte] socket module at ${frame.socket} did not register a Socket export`)
|
|
123
|
+
}
|
|
124
|
+
const isFirstLocalSub = addSub(state, frame.socket, frame.sub)
|
|
125
|
+
if (isFirstLocalSub) {
|
|
126
|
+
ws.subscribe(`socket:${frame.socket}`)
|
|
127
|
+
}
|
|
128
|
+
/*
|
|
129
|
+
Replay history directly to this ws via ws.send (not
|
|
130
|
+
server.publish) so other connected subscribers don't see the
|
|
131
|
+
replay. Live messages published from now on flow through the
|
|
132
|
+
Bun topic the ws just joined; clients may observe live messages
|
|
133
|
+
interleaved with the tail of history, so user payloads should
|
|
134
|
+
carry an id/timestamp when ordering matters.
|
|
135
|
+
|
|
136
|
+
`replay === undefined` means full replay (bare `for await`);
|
|
137
|
+
a number is clamped to the buffer length so the client can ask
|
|
138
|
+
for "as many as available, up to N".
|
|
139
|
+
*/
|
|
140
|
+
recentHistory(entry, frame.replay).forEach((message) => {
|
|
141
|
+
send(ws, { type: 'msg', socket: frame.socket, message })
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function handleUnsub(
|
|
146
|
+
ws: ServerWebSocket<unknown>,
|
|
147
|
+
state: ConnectionState,
|
|
148
|
+
frame: Extract<SocketClientFrame, { type: 'unsub' }>,
|
|
149
|
+
): void {
|
|
150
|
+
const emptied = removeSub(state, frame.sub)
|
|
151
|
+
if (emptied) {
|
|
152
|
+
ws.unsubscribe(`socket:${emptied}`)
|
|
153
|
+
}
|
|
154
|
+
send(ws, { type: 'end', sub: frame.sub })
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function handlePub(
|
|
158
|
+
ws: ServerWebSocket<unknown>,
|
|
159
|
+
frame: Extract<SocketClientFrame, { type: 'pub' }>,
|
|
160
|
+
): Promise<void> {
|
|
161
|
+
const loader = ensureLoaded(frame.socket)
|
|
162
|
+
if (!loader) {
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
await loader
|
|
167
|
+
} catch (error) {
|
|
168
|
+
log.error(error)
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
const entry = lookupSocket(frame.socket)
|
|
172
|
+
if (!entry) {
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
if (!entry.allowClientPublish) {
|
|
176
|
+
/*
|
|
177
|
+
Silent drop: the publish is rejected because the topic
|
|
178
|
+
wasn't declared `{ clientPublish: true }`. Surfacing this as
|
|
179
|
+
an error per-publish would tempt apps to attempt-then-handle
|
|
180
|
+
instead of routing through an HTTP route for auth. Log it
|
|
181
|
+
once per process at debug level (out of scope here) if
|
|
182
|
+
visibility is needed.
|
|
183
|
+
*/
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
/*
|
|
187
|
+
publish() runs the topic's optional Standard Schema synchronously
|
|
188
|
+
and throws on failure (see defineSocket.validateSync). The
|
|
189
|
+
dispatcher invokes us via `void handlePub(...)`, so an unhandled
|
|
190
|
+
throw would surface as an unhandled promise rejection on every
|
|
191
|
+
malformed client frame. Catch + log so a buggy client can't take
|
|
192
|
+
the process down.
|
|
193
|
+
*/
|
|
194
|
+
try {
|
|
195
|
+
entry.socket.publish(frame.message)
|
|
196
|
+
} catch (error) {
|
|
197
|
+
log.error(error)
|
|
198
|
+
}
|
|
199
|
+
/*
|
|
200
|
+
ws parameter retained for future per-ws auth context (cookies on
|
|
201
|
+
upgrade) the canPublish hook would consult.
|
|
202
|
+
*/
|
|
203
|
+
void ws
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/*
|
|
207
|
+
HTTP face of the sockets hub at `/__belte/sockets/<name>`, for the CLI
|
|
208
|
+
and MCP (which can't speak the ws multiplex protocol):
|
|
209
|
+
|
|
210
|
+
GET text/event-stream → live SSE stream; `?tail=N` replays the last
|
|
211
|
+
N buffered messages before tailing live (default 0 = live only).
|
|
212
|
+
GET otherwise → JSON array of the recent history buffer
|
|
213
|
+
(`?tail=N` caps it; default all).
|
|
214
|
+
POST → publish the JSON body, gated by the socket's
|
|
215
|
+
clientPublish policy and validated against its schema.
|
|
216
|
+
|
|
217
|
+
Loads the socket module on first hit (same cache the ws path uses) so
|
|
218
|
+
its defineSocket call populates the registry.
|
|
219
|
+
*/
|
|
220
|
+
async function rest(req: Request, name: string): Promise<Response> {
|
|
221
|
+
const loader = ensureLoaded(name)
|
|
222
|
+
if (!loader) {
|
|
223
|
+
return error(404)
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
await loader
|
|
227
|
+
} catch (loadError) {
|
|
228
|
+
log.error(loadError)
|
|
229
|
+
return error(500, 'socket failed to load')
|
|
230
|
+
}
|
|
231
|
+
const entry = lookupSocket(name)
|
|
232
|
+
if (!entry) {
|
|
233
|
+
return error(404)
|
|
234
|
+
}
|
|
235
|
+
const tailParam = new URL(req.url).searchParams.get('tail')
|
|
236
|
+
const count = tailParam !== null ? Number(tailParam) : undefined
|
|
237
|
+
if (req.method === 'GET' || req.method === 'HEAD') {
|
|
238
|
+
if ((req.headers.get('accept') ?? '').includes('text/event-stream')) {
|
|
239
|
+
return sse(entry.socket.tail(count ?? 0))
|
|
240
|
+
}
|
|
241
|
+
return json(recentHistory(entry, count))
|
|
242
|
+
}
|
|
243
|
+
if (req.method === 'POST') {
|
|
244
|
+
if (!entry.allowClientPublish) {
|
|
245
|
+
return error(403, 'publishing not allowed')
|
|
246
|
+
}
|
|
247
|
+
let message: unknown
|
|
248
|
+
try {
|
|
249
|
+
message = await req.json()
|
|
250
|
+
} catch {
|
|
251
|
+
return error(400, 'body must be JSON')
|
|
252
|
+
}
|
|
253
|
+
try {
|
|
254
|
+
// publish() validates against the socket schema and throws on a bad payload.
|
|
255
|
+
entry.socket.publish(message)
|
|
256
|
+
} catch (publishError) {
|
|
257
|
+
return error(
|
|
258
|
+
422,
|
|
259
|
+
publishError instanceof Error ? publishError.message : String(publishError),
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
return json({ ok: true })
|
|
263
|
+
}
|
|
264
|
+
return error(405, undefined, { headers: { Allow: 'GET, POST' } })
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
rest,
|
|
269
|
+
|
|
270
|
+
open(ws) {
|
|
271
|
+
connections.set(ws, { subToSocket: new Map(), socketSubs: new Map() })
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
message(ws, data) {
|
|
275
|
+
const state = connections.get(ws)
|
|
276
|
+
if (!state) {
|
|
277
|
+
return
|
|
278
|
+
}
|
|
279
|
+
const text = typeof data === 'string' ? data : textDecoder.decode(data)
|
|
280
|
+
let frame: SocketClientFrame
|
|
281
|
+
try {
|
|
282
|
+
frame = JSON.parse(text) as SocketClientFrame
|
|
283
|
+
} catch {
|
|
284
|
+
return
|
|
285
|
+
}
|
|
286
|
+
if (frame.type === 'sub') {
|
|
287
|
+
void handleSub(ws, state, frame)
|
|
288
|
+
return
|
|
289
|
+
}
|
|
290
|
+
if (frame.type === 'unsub') {
|
|
291
|
+
handleUnsub(ws, state, frame)
|
|
292
|
+
return
|
|
293
|
+
}
|
|
294
|
+
if (frame.type === 'pub') {
|
|
295
|
+
void handlePub(ws, frame)
|
|
296
|
+
return
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
close(ws) {
|
|
301
|
+
const state = connections.get(ws)
|
|
302
|
+
if (!state) {
|
|
303
|
+
return
|
|
304
|
+
}
|
|
305
|
+
connections.delete(ws)
|
|
306
|
+
for (const name of state.socketSubs.keys()) {
|
|
307
|
+
ws.unsubscribe(`socket:${name}`)
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
}
|
|
311
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { createPushIterator } from '../../shared/createPushIterator.ts'
|
|
2
|
+
import { resolveClientFlags } from '../../shared/resolveClientFlags.ts'
|
|
3
|
+
import { getActiveServer } from '../runtime/getActiveServer.ts'
|
|
4
|
+
import { registerSocket } from './registerSocket.ts'
|
|
5
|
+
import type { Socket } from './types/Socket.ts'
|
|
6
|
+
import type { SocketOptions } from './types/SocketOptions.ts'
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
Server-side construction of a Socket. The bundler rewrites every
|
|
10
|
+
`export const NAME = socket(opts)` inside `src/server/sockets/<file>.ts` into
|
|
11
|
+
`__belteDefineSocket__("<name>", opts)` so the file path becomes the
|
|
12
|
+
socket's identity. Each subscriber gets its own queue + notifier, the
|
|
13
|
+
optional history buffer is shared, and outbound fan-out rides Bun's
|
|
14
|
+
native `server.publish` so connected ws clients are notified by the
|
|
15
|
+
runtime in C rather than per-client iteration in JS.
|
|
16
|
+
|
|
17
|
+
The Socket itself is the AsyncIterable: `for await (const m of chat)`
|
|
18
|
+
replays the full history buffer then tails live. `chat.tail(count)`
|
|
19
|
+
opens a subscription that replays the last `count` items (default `0`,
|
|
20
|
+
clamped to the configured `history` max). When `ttl` is set, history
|
|
21
|
+
entries older than `ttl` ms are evicted lazily on every read/append —
|
|
22
|
+
no timer runs in the background. `chat.publish(m)` is isomorphic —
|
|
23
|
+
called server-side it both notifies in-process iterators and broadcasts
|
|
24
|
+
to remote subscribers; called client-side (via socketProxy) it sends a
|
|
25
|
+
`pub` frame the dispatcher validates and forwards.
|
|
26
|
+
*/
|
|
27
|
+
export function defineSocket<T>(name: string, opts: SocketOptions = {}): Socket<T> {
|
|
28
|
+
const historySize = opts.history ?? 0
|
|
29
|
+
const ttl = opts.ttl
|
|
30
|
+
const schema = opts.schema
|
|
31
|
+
/*
|
|
32
|
+
A schema makes the socket's payload safe to advertise to non-browser
|
|
33
|
+
surfaces, so it flips mcp/cli on by default — exposing the `tail` read
|
|
34
|
+
tool (and `publish` when clientPublish is set). Explicit `clients` wins.
|
|
35
|
+
*/
|
|
36
|
+
const hasSchema = schema !== undefined
|
|
37
|
+
const clients = resolveClientFlags(opts.clients, { mcp: hasSchema, cli: hasSchema })
|
|
38
|
+
type BufferEntry = { value: T; expiresAt: number | undefined }
|
|
39
|
+
const buffer: BufferEntry[] = []
|
|
40
|
+
const subscribers = new Set<(message: T) => void>()
|
|
41
|
+
const topic = `socket:${name}`
|
|
42
|
+
|
|
43
|
+
/*
|
|
44
|
+
History entries are stored with an expiry timestamp. When `ttl` is set,
|
|
45
|
+
every read/append starts by dropping leading entries whose expiry has
|
|
46
|
+
passed — entries are appended in order so the expired prefix is
|
|
47
|
+
contiguous. No timer/setInterval is needed: expiry is lazy.
|
|
48
|
+
*/
|
|
49
|
+
function pruneExpired(now: number): void {
|
|
50
|
+
if (ttl === undefined) {
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
let drop = 0
|
|
54
|
+
for (const entry of buffer) {
|
|
55
|
+
if (entry.expiresAt !== undefined && entry.expiresAt <= now) {
|
|
56
|
+
drop++
|
|
57
|
+
} else {
|
|
58
|
+
break
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (drop > 0) {
|
|
62
|
+
buffer.splice(0, drop)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/*
|
|
67
|
+
Active server is set once per process during createServer's boot,
|
|
68
|
+
immediately after Bun.serve resolves, and never reassigned. Resolve
|
|
69
|
+
it lazily on the first publish then keep the reference so subsequent
|
|
70
|
+
publishes skip the per-call getter.
|
|
71
|
+
*/
|
|
72
|
+
let cachedServer: ReturnType<typeof getActiveServer>
|
|
73
|
+
/*
|
|
74
|
+
When a schema is attached, publish() validates synchronously and
|
|
75
|
+
throws on bad payloads. Standard Schema's validate() is generally
|
|
76
|
+
async — but for the synchronous server-side publish path we treat
|
|
77
|
+
a Promise return as a programming error (publish must be sync to
|
|
78
|
+
preserve in-process notify ordering). Schemas that need async
|
|
79
|
+
refinement should pre-validate at the call site instead.
|
|
80
|
+
*/
|
|
81
|
+
function validateSync(message: T): T {
|
|
82
|
+
if (!schema) {
|
|
83
|
+
return message
|
|
84
|
+
}
|
|
85
|
+
const result = schema['~standard'].validate(message)
|
|
86
|
+
if (result instanceof Promise) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`[belte] socket "${name}" schema returned a Promise — sockets require sync validation`,
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
if (result.issues) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`[belte] socket "${name}" publish payload failed validation: ${JSON.stringify(result.issues)}`,
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
return result.value as T
|
|
97
|
+
}
|
|
98
|
+
function publish(message: T): void {
|
|
99
|
+
const validated = validateSync(message)
|
|
100
|
+
if (historySize > 0) {
|
|
101
|
+
const now = Date.now()
|
|
102
|
+
pruneExpired(now)
|
|
103
|
+
buffer.push({ value: validated, expiresAt: ttl === undefined ? undefined : now + ttl })
|
|
104
|
+
if (buffer.length > historySize) {
|
|
105
|
+
buffer.shift()
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
for (const notify of subscribers) {
|
|
109
|
+
notify(validated)
|
|
110
|
+
}
|
|
111
|
+
if (cachedServer === undefined) {
|
|
112
|
+
cachedServer = getActiveServer()
|
|
113
|
+
}
|
|
114
|
+
const server = cachedServer
|
|
115
|
+
if (server) {
|
|
116
|
+
server.publish(topic, JSON.stringify({ type: 'msg', socket: name, message: validated }))
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/*
|
|
121
|
+
replay === 'all' replays the entire buffer (bare `for await`);
|
|
122
|
+
a number replays the last min(count, buffer.length) items.
|
|
123
|
+
*/
|
|
124
|
+
function iterate(replay: number | 'all'): AsyncIterable<T> {
|
|
125
|
+
return {
|
|
126
|
+
[Symbol.asyncIterator](): AsyncIterator<T, void, undefined> {
|
|
127
|
+
let subscriber: ((message: T) => void) | undefined
|
|
128
|
+
const iter = createPushIterator<T>(() => {
|
|
129
|
+
if (subscriber) {
|
|
130
|
+
subscribers.delete(subscriber)
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
pruneExpired(Date.now())
|
|
134
|
+
const replayCount =
|
|
135
|
+
replay === 'all' ? buffer.length : Math.min(replay, buffer.length)
|
|
136
|
+
if (replayCount > 0) {
|
|
137
|
+
const start = buffer.length - replayCount
|
|
138
|
+
for (let index = start; index < buffer.length; index++) {
|
|
139
|
+
iter.push((buffer[index] as BufferEntry).value)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
subscriber = (message: T) => iter.push(message)
|
|
143
|
+
subscribers.add(subscriber)
|
|
144
|
+
return iter
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const self: Socket<T> = {
|
|
150
|
+
name,
|
|
151
|
+
clients,
|
|
152
|
+
publish,
|
|
153
|
+
tail: (count = 0) => iterate(count),
|
|
154
|
+
[Symbol.asyncIterator]: () => iterate('all')[Symbol.asyncIterator](),
|
|
155
|
+
}
|
|
156
|
+
registerSocket({
|
|
157
|
+
socket: self as Socket<unknown>,
|
|
158
|
+
allowClientPublish: opts.clientPublish ?? false,
|
|
159
|
+
schema,
|
|
160
|
+
clients,
|
|
161
|
+
snapshotHistory: () => {
|
|
162
|
+
pruneExpired(Date.now())
|
|
163
|
+
return buffer.map((entry) => entry.value)
|
|
164
|
+
},
|
|
165
|
+
})
|
|
166
|
+
return self
|
|
167
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SocketRegistryEntry } from './types/SocketRegistryEntry.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Recent slice of a socket's history buffer: the last `count` messages, or
|
|
5
|
+
the whole buffer when `count` is undefined. Shared by the sockets HTTP
|
|
6
|
+
`rest()` face and the MCP `<base>-tail` tool so the two can't drift.
|
|
7
|
+
*/
|
|
8
|
+
export function recentHistory(entry: SocketRegistryEntry, count: number | undefined): unknown[] {
|
|
9
|
+
const history = entry.snapshotHistory()
|
|
10
|
+
return count === undefined ? history : history.slice(Math.max(0, history.length - count))
|
|
11
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { commandNameForUrl } from '../../shared/commandNameForUrl.ts'
|
|
2
|
+
import type { SocketOperation } from './types/SocketOperation.ts'
|
|
3
|
+
import type { SocketRegistryEntry } from './types/SocketRegistryEntry.ts'
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
Projects a socket registry entry into the operations it exposes to the
|
|
7
|
+
CLI and MCP. Single source for the naming convention (`<base>-tail` /
|
|
8
|
+
`<base>-publish`), the existence rule (tail always; publish only when the
|
|
9
|
+
socket allows client publishing), and each operation's HTTP face — so the
|
|
10
|
+
CLI manifest builder, the MCP tool list, and the MCP tool dispatcher can't
|
|
11
|
+
disagree about which operations a socket has or what they're called.
|
|
12
|
+
*/
|
|
13
|
+
export function socketOperations(entry: SocketRegistryEntry): SocketOperation[] {
|
|
14
|
+
const base = commandNameForUrl(entry.socket.name)
|
|
15
|
+
const restUrl = `/__belte/sockets/${entry.socket.name}`
|
|
16
|
+
const operations: SocketOperation[] = [
|
|
17
|
+
{
|
|
18
|
+
kind: 'tail',
|
|
19
|
+
name: `${base}-tail`,
|
|
20
|
+
socketName: entry.socket.name,
|
|
21
|
+
restUrl,
|
|
22
|
+
method: 'GET',
|
|
23
|
+
},
|
|
24
|
+
]
|
|
25
|
+
if (entry.allowClientPublish) {
|
|
26
|
+
operations.push({
|
|
27
|
+
kind: 'publish',
|
|
28
|
+
name: `${base}-publish`,
|
|
29
|
+
socketName: entry.socket.name,
|
|
30
|
+
restUrl,
|
|
31
|
+
method: 'POST',
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
return operations
|
|
35
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SocketRegistryEntry } from './types/SocketRegistryEntry.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Process-wide registry of every Socket declared in the app. defineSocket
|
|
5
|
+
inserts on first construction; the dispatcher reads on every `sub` /
|
|
6
|
+
`pub` frame so it can find the right Socket by name and check the
|
|
7
|
+
opted-in `allowClientPublish` policy.
|
|
8
|
+
*/
|
|
9
|
+
export const socketRegistry = new Map<string, SocketRegistryEntry>()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ClientFlags } from '../../../shared/types/ClientFlags.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Bidirectional named broadcast primitive. Declared once with `socket<T>()`
|
|
5
|
+
inside a file under `src/server/sockets/`; the same import resolves to a server-side
|
|
6
|
+
fan-out and a client-side ws proxy by build target. Iterating the socket
|
|
7
|
+
opens a subscription with full history replay if the topic was declared
|
|
8
|
+
with `{ history: n }`. `.tail(count)` opens one that replays the last
|
|
9
|
+
`count` items (default `0`, clamped to the topic's history max) before
|
|
10
|
+
tailing live. `publish` is isomorphic: server code publishes in-process
|
|
11
|
+
and fans out to remote subscribers; client code sends a `pub` frame the
|
|
12
|
+
dispatcher validates against the topic's `clientPublish` flag. `clients`
|
|
13
|
+
exposes which adapter surfaces (browser / mcp / cli) advertise this
|
|
14
|
+
socket.
|
|
15
|
+
*/
|
|
16
|
+
export interface Socket<T> extends AsyncIterable<T> {
|
|
17
|
+
readonly name: string
|
|
18
|
+
readonly clients: ClientFlags
|
|
19
|
+
publish(message: T): void
|
|
20
|
+
tail(count?: number): AsyncIterable<T>
|
|
21
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Wire frame the browser sends over the multiplexed socket connection.
|
|
3
|
+
`sub` opens a subscription against `socket`. The optional `replay`
|
|
4
|
+
controls history: omitted = full replay (default `for await`); a
|
|
5
|
+
number = at most that many trailing items (clamped server-side to the
|
|
6
|
+
topic's history max). `unsub` closes one. `pub` publishes a message —
|
|
7
|
+
the dispatcher checks the topic's `clientPublish` flag before fanning
|
|
8
|
+
out.
|
|
9
|
+
|
|
10
|
+
`sub` is the per-subscription id minted client-side; the server treats
|
|
11
|
+
it as opaque and routes inbound `msg|err|end` frames back to the same
|
|
12
|
+
id so one ws can multiplex many subscriptions to the same or different
|
|
13
|
+
sockets.
|
|
14
|
+
*/
|
|
15
|
+
export type SocketClientFrame =
|
|
16
|
+
| { type: 'sub'; sub: string; socket: string; replay?: number }
|
|
17
|
+
| { type: 'unsub'; sub: string }
|
|
18
|
+
| { type: 'pub'; socket: string; message: unknown }
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { HttpVerb } from '../../../shared/types/HttpVerb.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
One operation a socket exposes to the non-browser surfaces. A socket
|
|
5
|
+
always offers a `tail` (read recent / stream live) and, when
|
|
6
|
+
`clientPublish` is set, a `publish` (send a message). This is the shared
|
|
7
|
+
skeleton — name, kind, HTTP face — that the CLI manifest, the MCP tool
|
|
8
|
+
list, and the MCP dispatcher all read instead of re-deriving the naming
|
|
9
|
+
convention and existence rule independently. Each surface dresses it with
|
|
10
|
+
its own presentation (descriptions, input schema, annotations).
|
|
11
|
+
*/
|
|
12
|
+
export type SocketOperation = {
|
|
13
|
+
kind: 'tail' | 'publish'
|
|
14
|
+
// Command/tool name: the socket's command-name base plus `-tail` / `-publish`.
|
|
15
|
+
name: string
|
|
16
|
+
// Raw socket name, for the HTTP path and human-facing descriptions.
|
|
17
|
+
socketName: string
|
|
18
|
+
// HTTP face of the operation: `/__belte/sockets/<name>`.
|
|
19
|
+
restUrl: string
|
|
20
|
+
// GET for tail, POST for publish.
|
|
21
|
+
method: HttpVerb
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ClientFlags } from '../../../shared/types/ClientFlags.ts'
|
|
2
|
+
import type { StandardSchemaV1 } from '../../../shared/types/StandardSchemaV1.ts'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
Server-side options passed when declaring a socket via `socket<T>(opts)`.
|
|
6
|
+
History buffer (replayed on first iteration), per-frame TTL (history
|
|
7
|
+
entries older than `ttl` ms are evicted before replay), and the client-
|
|
8
|
+
publish gate (off by default — server-only topics ignore pub frames
|
|
9
|
+
coming over the wire). Optional Standard Schema validates payloads on
|
|
10
|
+
publish and gives MCP / CLI a typed payload to describe (projected via the
|
|
11
|
+
schema's own `toJSONSchema()` — wrap with withJsonSchema if its library lacks
|
|
12
|
+
one). `clients` controls which non-browser surfaces (mcp / cli) expose this
|
|
13
|
+
socket; browser is the historical default. All server-only state the bundler
|
|
14
|
+
strips out of the client stub.
|
|
15
|
+
*/
|
|
16
|
+
export type SocketOptions<Schema extends StandardSchemaV1 = StandardSchemaV1> = {
|
|
17
|
+
history?: number
|
|
18
|
+
ttl?: number
|
|
19
|
+
clientPublish?: boolean
|
|
20
|
+
schema?: Schema
|
|
21
|
+
clients?: Partial<ClientFlags>
|
|
22
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ClientFlags } from '../../../shared/types/ClientFlags.ts'
|
|
2
|
+
import type { StandardSchemaV1 } from '../../../shared/types/StandardSchemaV1.ts'
|
|
3
|
+
import type { Socket } from './Socket.ts'
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
Per-socket registry record. The Socket itself stays uniform between
|
|
7
|
+
server and client by parking policy state (history snapshot, client
|
|
8
|
+
publish gate, payload schema, client targeting) here instead of leaking
|
|
9
|
+
into the public Socket shape.
|
|
10
|
+
*/
|
|
11
|
+
export type SocketRegistryEntry = {
|
|
12
|
+
socket: Socket<unknown>
|
|
13
|
+
allowClientPublish: boolean
|
|
14
|
+
schema: StandardSchemaV1 | undefined
|
|
15
|
+
clients: ClientFlags
|
|
16
|
+
snapshotHistory(): unknown[]
|
|
17
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Socket } from './Socket.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Manifest of socket-name → module loader. Produced by the resolver
|
|
5
|
+
plugin from each `.ts` under src/server/sockets/. Each module has exactly one
|
|
6
|
+
named export, a Socket whose `.name` was stamped in by the bundler
|
|
7
|
+
rewrite. The dispatcher imports a module on first access and caches the
|
|
8
|
+
resolved Socket against its name.
|
|
9
|
+
*/
|
|
10
|
+
export type SocketRoutes = Record<string, () => Promise<Record<string, Socket<unknown>>>>
|