@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.
Files changed (326) hide show
  1. package/CHANGELOG.md +313 -0
  2. package/LICENSE +21 -0
  3. package/README.md +559 -0
  4. package/bin/belte.ts +183 -0
  5. package/package.json +110 -0
  6. package/src/App.svelte +31 -0
  7. package/src/appEntry.ts +151 -0
  8. package/src/assets/app.html +14 -0
  9. package/src/belteResolverPlugin.ts +858 -0
  10. package/src/build.ts +147 -0
  11. package/src/buildCli.ts +129 -0
  12. package/src/buildDisconnected.ts +122 -0
  13. package/src/bundleApp.ts +149 -0
  14. package/src/bundleDisconnectedEntry.ts +17 -0
  15. package/src/cliEntry.ts +25 -0
  16. package/src/clientBuildPlugins.ts +41 -0
  17. package/src/clientEntry.ts +7 -0
  18. package/src/compile.ts +64 -0
  19. package/src/controlServerWorker.ts +422 -0
  20. package/src/dedupeSveltePlugin.ts +66 -0
  21. package/src/devEntry.ts +169 -0
  22. package/src/discoveryEntry.ts +81 -0
  23. package/src/lib/browser/applyStreamedResolution.ts +33 -0
  24. package/src/lib/browser/cacheEntryFromSnapshot.ts +48 -0
  25. package/src/lib/browser/flushUnresolvedPlaceholders.ts +16 -0
  26. package/src/lib/browser/installStreamingPlaceholders.ts +32 -0
  27. package/src/lib/browser/openResolveStream.ts +42 -0
  28. package/src/lib/browser/page.svelte.ts +258 -0
  29. package/src/lib/browser/pageStreamController.ts +17 -0
  30. package/src/lib/browser/refetchPlaceholder.ts +12 -0
  31. package/src/lib/browser/remoteProxy.ts +37 -0
  32. package/src/lib/browser/socketChannel.ts +192 -0
  33. package/src/lib/browser/socketProxy.ts +57 -0
  34. package/src/lib/browser/startClient.ts +153 -0
  35. package/src/lib/browser/subscribe.ts +131 -0
  36. package/src/lib/browser/types/Errors.ts +9 -0
  37. package/src/lib/browser/types/Layouts.ts +7 -0
  38. package/src/lib/browser/types/Pages.ts +7 -0
  39. package/src/lib/browser/types/StreamingDeferred.ts +9 -0
  40. package/src/lib/bundle/BundleMenu.ts +11 -0
  41. package/src/lib/bundle/BundleMenuItem.ts +24 -0
  42. package/src/lib/bundle/BundleWindow.ts +36 -0
  43. package/src/lib/bundle/WEBVIEW_BUILD_REVISION.ts +9 -0
  44. package/src/lib/bundle/WEBVIEW_VERSION.ts +7 -0
  45. package/src/lib/bundle/bindConnectedFlag.ts +29 -0
  46. package/src/lib/bundle/bindRequestNavigate.ts +31 -0
  47. package/src/lib/bundle/buildWebviewLib.ts +111 -0
  48. package/src/lib/bundle/disconnected.css +9 -0
  49. package/src/lib/bundle/disconnected.svelte +386 -0
  50. package/src/lib/bundle/ensureWebviewLib.ts +20 -0
  51. package/src/lib/bundle/exitWithParent.ts +28 -0
  52. package/src/lib/bundle/infoPlist.ts +46 -0
  53. package/src/lib/bundle/installDownloads.ts +24 -0
  54. package/src/lib/bundle/installMacMenu.ts +39 -0
  55. package/src/lib/bundle/listenLocalControlServer.ts +19 -0
  56. package/src/lib/bundle/native/belteMenu.mm +422 -0
  57. package/src/lib/bundle/native/webview.h +4557 -0
  58. package/src/lib/bundle/onMenu.ts +41 -0
  59. package/src/lib/bundle/openWebview.ts +104 -0
  60. package/src/lib/bundle/pngToIcns.ts +47 -0
  61. package/src/lib/bundle/probeBelteServer.ts +34 -0
  62. package/src/lib/bundle/resolveServerBinary.ts +12 -0
  63. package/src/lib/bundle/resolveWebviewLib.ts +53 -0
  64. package/src/lib/bundle/serverBinaryFilename.ts +8 -0
  65. package/src/lib/bundle/signMacApp.ts +35 -0
  66. package/src/lib/bundle/spawnEmbeddedServer.ts +65 -0
  67. package/src/lib/bundle/stableLocalPort.ts +19 -0
  68. package/src/lib/bundle/waitForServer.ts +23 -0
  69. package/src/lib/bundle/webviewCachePath.ts +23 -0
  70. package/src/lib/bundle/webviewLibName.ts +11 -0
  71. package/src/lib/cli/connectToServer.ts +23 -0
  72. package/src/lib/cli/createClient.ts +170 -0
  73. package/src/lib/cli/dispatchCommand.ts +71 -0
  74. package/src/lib/cli/loadEnvFromBinaryDir.ts +16 -0
  75. package/src/lib/cli/parseArgvForRpc.ts +97 -0
  76. package/src/lib/cli/printHelp.ts +119 -0
  77. package/src/lib/cli/printSessionHelp.ts +27 -0
  78. package/src/lib/cli/printSessionStatus.ts +21 -0
  79. package/src/lib/cli/printTrimmed.ts +8 -0
  80. package/src/lib/cli/printValue.ts +10 -0
  81. package/src/lib/cli/resolveCliTarget.ts +48 -0
  82. package/src/lib/cli/runCli.ts +139 -0
  83. package/src/lib/cli/runSession.ts +105 -0
  84. package/src/lib/cli/startLocalInstance.ts +14 -0
  85. package/src/lib/cli/tokenizeLine.ts +51 -0
  86. package/src/lib/cli/types/CliManifest.ts +9 -0
  87. package/src/lib/cli/types/CliManifestEntry.ts +17 -0
  88. package/src/lib/cli/types/CliTarget.ts +13 -0
  89. package/src/lib/mcp/annotationsForMethod.ts +29 -0
  90. package/src/lib/mcp/createMcpResourceServer.ts +101 -0
  91. package/src/lib/mcp/createMcpServer.ts +42 -0
  92. package/src/lib/mcp/dispatchMcpRequest.ts +146 -0
  93. package/src/lib/mcp/mcpResourceServerSlot.ts +18 -0
  94. package/src/lib/mcp/mcpSurface.ts +265 -0
  95. package/src/lib/mcp/toolResultFromResponse.ts +66 -0
  96. package/src/lib/mcp/types/JsonRpcRequest.ts +12 -0
  97. package/src/lib/mcp/types/JsonRpcResponse.ts +20 -0
  98. package/src/lib/mcp/types/McpResourceContents.ts +10 -0
  99. package/src/lib/mcp/types/McpResourceDescriptor.ts +6 -0
  100. package/src/lib/mcp/types/McpResourceServer.ts +12 -0
  101. package/src/lib/mcp/types/McpServer.ts +9 -0
  102. package/src/lib/mcp/types/McpServerOptions.ts +16 -0
  103. package/src/lib/server/AppModule.ts +33 -0
  104. package/src/lib/server/DELETE.ts +9 -0
  105. package/src/lib/server/GET.ts +9 -0
  106. package/src/lib/server/HEAD.ts +9 -0
  107. package/src/lib/server/PATCH.ts +9 -0
  108. package/src/lib/server/POST.ts +9 -0
  109. package/src/lib/server/PUT.ts +9 -0
  110. package/src/lib/server/agent.ts +76 -0
  111. package/src/lib/server/appDataDir.ts +15 -0
  112. package/src/lib/server/cli/buildEnvContent.ts +19 -0
  113. package/src/lib/server/cli/createTarGz.ts +76 -0
  114. package/src/lib/server/cli/handleCliDownload.ts +153 -0
  115. package/src/lib/server/cli/handleCliInstall.ts +37 -0
  116. package/src/lib/server/cli/installScript.ts +29 -0
  117. package/src/lib/server/cli/maxSourceMtime.ts +26 -0
  118. package/src/lib/server/cookies.ts +29 -0
  119. package/src/lib/server/env.ts +50 -0
  120. package/src/lib/server/error.ts +70 -0
  121. package/src/lib/server/json.ts +28 -0
  122. package/src/lib/server/jsonl.ts +46 -0
  123. package/src/lib/server/prompts/definePrompt.ts +20 -0
  124. package/src/lib/server/prompts/promptRegistry.ts +9 -0
  125. package/src/lib/server/prompts/registerPrompt.ts +6 -0
  126. package/src/lib/server/prompts/renderPromptTemplate.ts +16 -0
  127. package/src/lib/server/prompts/types/Prompt.ts +13 -0
  128. package/src/lib/server/prompts/types/PromptOptions.ts +12 -0
  129. package/src/lib/server/prompts/types/PromptRegistryEntry.ts +13 -0
  130. package/src/lib/server/prompts/types/PromptRoutes.ts +10 -0
  131. package/src/lib/server/redirect.ts +42 -0
  132. package/src/lib/server/request.ts +18 -0
  133. package/src/lib/server/rpc/defineVerb.ts +133 -0
  134. package/src/lib/server/rpc/dispatchVerbInProcess.ts +46 -0
  135. package/src/lib/server/rpc/findVerbByCommandName.ts +18 -0
  136. package/src/lib/server/rpc/parseArgs.ts +95 -0
  137. package/src/lib/server/rpc/registerVerb.ts +6 -0
  138. package/src/lib/server/rpc/types/RemoteHandler.ts +27 -0
  139. package/src/lib/server/rpc/types/RemoteRoutes.ts +13 -0
  140. package/src/lib/server/rpc/types/TypedResponse.ts +18 -0
  141. package/src/lib/server/rpc/types/VerbHelper.ts +68 -0
  142. package/src/lib/server/rpc/types/VerbRegistryEntry.ts +29 -0
  143. package/src/lib/server/rpc/unprocessed.ts +14 -0
  144. package/src/lib/server/rpc/verbRegistry.ts +11 -0
  145. package/src/lib/server/runtime/DEFAULT_PORT.ts +6 -0
  146. package/src/lib/server/runtime/DEV_REBUILD_MESSAGE.ts +4 -0
  147. package/src/lib/server/runtime/DEV_RELOAD_CLIENT_SCRIPT.ts +29 -0
  148. package/src/lib/server/runtime/acceptsZstd.ts +8 -0
  149. package/src/lib/server/runtime/buildOpenApiSpec.ts +106 -0
  150. package/src/lib/server/runtime/cacheControlForAsset.ts +22 -0
  151. package/src/lib/server/runtime/containsTraversal.ts +37 -0
  152. package/src/lib/server/runtime/createAssetHeaderCache.ts +35 -0
  153. package/src/lib/server/runtime/createPublicAssetServer.ts +63 -0
  154. package/src/lib/server/runtime/createRouteDispatcher.ts +100 -0
  155. package/src/lib/server/runtime/createServer.ts +692 -0
  156. package/src/lib/server/runtime/devReloadResponse.ts +35 -0
  157. package/src/lib/server/runtime/disableIdleTimeoutForStream.ts +27 -0
  158. package/src/lib/server/runtime/envSchemaStore.ts +15 -0
  159. package/src/lib/server/runtime/findOpenPort.ts +35 -0
  160. package/src/lib/server/runtime/getActiveServer.ts +6 -0
  161. package/src/lib/server/runtime/globToPathSet.ts +29 -0
  162. package/src/lib/server/runtime/inProcessServer.ts +20 -0
  163. package/src/lib/server/runtime/internalErrorResponse.ts +25 -0
  164. package/src/lib/server/runtime/isCrossOriginUpgrade.ts +19 -0
  165. package/src/lib/server/runtime/listenOnOpenPort.ts +36 -0
  166. package/src/lib/server/runtime/logExposedSurfaces.ts +162 -0
  167. package/src/lib/server/runtime/mimeForExtension.ts +20 -0
  168. package/src/lib/server/runtime/parseIdleTimeout.ts +10 -0
  169. package/src/lib/server/runtime/parsePort.ts +11 -0
  170. package/src/lib/server/runtime/registryManifests.ts +66 -0
  171. package/src/lib/server/runtime/requestContext.ts +5 -0
  172. package/src/lib/server/runtime/resolveStreamResponse.ts +29 -0
  173. package/src/lib/server/runtime/runWithRequestScope.ts +57 -0
  174. package/src/lib/server/runtime/safeJsonForScript.ts +17 -0
  175. package/src/lib/server/runtime/serializeCacheSnapshot.ts +45 -0
  176. package/src/lib/server/runtime/serverSlot.ts +13 -0
  177. package/src/lib/server/runtime/setActiveServer.ts +6 -0
  178. package/src/lib/server/runtime/snapshotEntryFromCache.ts +81 -0
  179. package/src/lib/server/runtime/streamCacheResolutions.ts +37 -0
  180. package/src/lib/server/runtime/streamFromIterator.ts +86 -0
  181. package/src/lib/server/runtime/streamStash.ts +64 -0
  182. package/src/lib/server/runtime/types/Assets.ts +1 -0
  183. package/src/lib/server/runtime/types/RequestStore.ts +27 -0
  184. package/src/lib/server/runtime/withResponseDefaults.ts +24 -0
  185. package/src/lib/server/server.ts +32 -0
  186. package/src/lib/server/socket.ts +31 -0
  187. package/src/lib/server/sockets/createSocketDispatcher.ts +311 -0
  188. package/src/lib/server/sockets/defineSocket.ts +167 -0
  189. package/src/lib/server/sockets/lookupSocket.ts +6 -0
  190. package/src/lib/server/sockets/recentHistory.ts +11 -0
  191. package/src/lib/server/sockets/registerSocket.ts +6 -0
  192. package/src/lib/server/sockets/socketOperations.ts +35 -0
  193. package/src/lib/server/sockets/socketRegistry.ts +9 -0
  194. package/src/lib/server/sockets/types/Socket.ts +21 -0
  195. package/src/lib/server/sockets/types/SocketClientFrame.ts +18 -0
  196. package/src/lib/server/sockets/types/SocketOperation.ts +22 -0
  197. package/src/lib/server/sockets/types/SocketOptions.ts +22 -0
  198. package/src/lib/server/sockets/types/SocketRegistryEntry.ts +17 -0
  199. package/src/lib/server/sockets/types/SocketRoutes.ts +10 -0
  200. package/src/lib/server/sockets/types/SocketServerFrame.ts +15 -0
  201. package/src/lib/server/sse.ts +53 -0
  202. package/src/lib/shared/BELTE_PACKAGE_NAME.ts +7 -0
  203. package/src/lib/shared/CACHE_CONTROL_VALUES.ts +16 -0
  204. package/src/lib/shared/HttpError.ts +19 -0
  205. package/src/lib/shared/RESOLVE_STREAM_PATH.ts +7 -0
  206. package/src/lib/shared/STREAMING_CONTENT_TYPES.ts +11 -0
  207. package/src/lib/shared/activeCacheStore.ts +20 -0
  208. package/src/lib/shared/appDataDir.ts +34 -0
  209. package/src/lib/shared/belteImportName.ts +44 -0
  210. package/src/lib/shared/browserClientFlags.ts +10 -0
  211. package/src/lib/shared/buildRpcRequest.ts +70 -0
  212. package/src/lib/shared/bundleLayout.ts +36 -0
  213. package/src/lib/shared/bundled.ts +34 -0
  214. package/src/lib/shared/cache.ts +559 -0
  215. package/src/lib/shared/cacheStoreSlot.ts +16 -0
  216. package/src/lib/shared/canonicalJson.ts +63 -0
  217. package/src/lib/shared/carriesBodyArgs.ts +13 -0
  218. package/src/lib/shared/clearLastConnection.ts +7 -0
  219. package/src/lib/shared/commandNameForUrl.ts +17 -0
  220. package/src/lib/shared/createCacheStore.ts +75 -0
  221. package/src/lib/shared/createPushIterator.ts +93 -0
  222. package/src/lib/shared/createRemoteFunction.ts +99 -0
  223. package/src/lib/shared/decodeResponse.ts +47 -0
  224. package/src/lib/shared/detectTarget.ts +27 -0
  225. package/src/lib/shared/exeSuffix.ts +9 -0
  226. package/src/lib/shared/exitOnBuildFailure.ts +17 -0
  227. package/src/lib/shared/extraForwardHeaders.ts +16 -0
  228. package/src/lib/shared/fileStem.ts +9 -0
  229. package/src/lib/shared/findExportCallSite.ts +479 -0
  230. package/src/lib/shared/forwardHeaders.ts +41 -0
  231. package/src/lib/shared/getRemoteMeta.ts +5 -0
  232. package/src/lib/shared/globalCacheStore.ts +15 -0
  233. package/src/lib/shared/globalCacheStoreSlot.ts +14 -0
  234. package/src/lib/shared/importNamesToStrip.ts +13 -0
  235. package/src/lib/shared/invalidateEvent.ts +11 -0
  236. package/src/lib/shared/isCompileTarget.ts +15 -0
  237. package/src/lib/shared/isDebugEnabled.ts +23 -0
  238. package/src/lib/shared/isModuleNotFound.ts +16 -0
  239. package/src/lib/shared/isReadOnlyMethod.ts +14 -0
  240. package/src/lib/shared/isStreamingResponse.ts +11 -0
  241. package/src/lib/shared/jsonSchemaForPromptArguments.ts +29 -0
  242. package/src/lib/shared/jsonSchemaForSchema.ts +32 -0
  243. package/src/lib/shared/jsonlErrorFrame.ts +24 -0
  244. package/src/lib/shared/keyForRemoteCall.ts +29 -0
  245. package/src/lib/shared/lastConnectionPath.ts +7 -0
  246. package/src/lib/shared/loadEnvFile.ts +17 -0
  247. package/src/lib/shared/loadEnvFromDataDir.ts +15 -0
  248. package/src/lib/shared/loadSvelteConfig.ts +18 -0
  249. package/src/lib/shared/log.ts +104 -0
  250. package/src/lib/shared/manifestModule.ts +39 -0
  251. package/src/lib/shared/memoizeByKey.ts +24 -0
  252. package/src/lib/shared/nearestLayoutPrefix.ts +36 -0
  253. package/src/lib/shared/normalizeTarget.ts +10 -0
  254. package/src/lib/shared/pageUrlForFile.ts +14 -0
  255. package/src/lib/shared/parseBoundedEnvInt.ts +20 -0
  256. package/src/lib/shared/parseEnv.ts +30 -0
  257. package/src/lib/shared/parsePromptMarkdown.ts +34 -0
  258. package/src/lib/shared/parseRouteSegments.ts +22 -0
  259. package/src/lib/shared/prepareRpcModule.ts +59 -0
  260. package/src/lib/shared/prepareSocketModule.ts +49 -0
  261. package/src/lib/shared/programNameForPackage.ts +14 -0
  262. package/src/lib/shared/promptNameForFile.ts +10 -0
  263. package/src/lib/shared/queryStringFromArgs.ts +27 -0
  264. package/src/lib/shared/readEnvFile.ts +15 -0
  265. package/src/lib/shared/readLastConnection.ts +18 -0
  266. package/src/lib/shared/readPackageJson.ts +9 -0
  267. package/src/lib/shared/recordRemoteMeta.ts +5 -0
  268. package/src/lib/shared/remoteMetaStore.ts +16 -0
  269. package/src/lib/shared/resolveClientFlags.ts +20 -0
  270. package/src/lib/shared/responseErrorText.ts +9 -0
  271. package/src/lib/shared/rpcUrlForFile.ts +19 -0
  272. package/src/lib/shared/runningAsStandaloneBinary.ts +13 -0
  273. package/src/lib/shared/serializeEnv.ts +18 -0
  274. package/src/lib/shared/setCacheStoreResolver.ts +6 -0
  275. package/src/lib/shared/setGlobalCacheStoreResolver.ts +6 -0
  276. package/src/lib/shared/socketNameForFile.ts +11 -0
  277. package/src/lib/shared/sseErrorFrame.ts +29 -0
  278. package/src/lib/shared/streamResponse.ts +169 -0
  279. package/src/lib/shared/stripImport.ts +27 -0
  280. package/src/lib/shared/subscribableFromResponse.ts +51 -0
  281. package/src/lib/shared/toBunRoutePattern.ts +28 -0
  282. package/src/lib/shared/types/CacheEntry.ts +63 -0
  283. package/src/lib/shared/types/CacheInvalidation.ts +9 -0
  284. package/src/lib/shared/types/CacheOptions.ts +33 -0
  285. package/src/lib/shared/types/CacheSnapshot.ts +16 -0
  286. package/src/lib/shared/types/CacheSnapshotEntry.ts +15 -0
  287. package/src/lib/shared/types/CacheStore.ts +32 -0
  288. package/src/lib/shared/types/ClientFlags.ts +11 -0
  289. package/src/lib/shared/types/CompileTarget.ts +6 -0
  290. package/src/lib/shared/types/HttpVerb.ts +1 -0
  291. package/src/lib/shared/types/LastConnection.ts +9 -0
  292. package/src/lib/shared/types/PromptArgument.ts +12 -0
  293. package/src/lib/shared/types/RawRemoteFunction.ts +13 -0
  294. package/src/lib/shared/types/RemoteFunction.ts +42 -0
  295. package/src/lib/shared/types/StandardSchemaV1.ts +57 -0
  296. package/src/lib/shared/types/StreamedResolution.ts +10 -0
  297. package/src/lib/shared/types/StreamingPlaceholder.ts +13 -0
  298. package/src/lib/shared/types/Subscribable.ts +15 -0
  299. package/src/lib/shared/types/SvelteConfig.ts +5 -0
  300. package/src/lib/shared/withJsonSchema.ts +20 -0
  301. package/src/lib/shared/writeLastConnection.ts +13 -0
  302. package/src/lib/shared/writeRoutesDts.ts +67 -0
  303. package/src/lib/test/clearVerbRegistry.ts +11 -0
  304. package/src/lib/test/createTestClient.ts +78 -0
  305. package/src/preload.ts +20 -0
  306. package/src/scaffold.ts +92 -0
  307. package/src/serverBuildPlugins.ts +25 -0
  308. package/src/serverEntry.ts +94 -0
  309. package/src/sveltePlugin.ts +58 -0
  310. package/src/tailwindStylePreprocessor.ts +62 -0
  311. package/template/bunfig.toml +4 -0
  312. package/template/package.json +19 -0
  313. package/template/src/app.ts +23 -0
  314. package/template/src/browser/app.css +21 -0
  315. package/template/src/browser/app.html +24 -0
  316. package/template/src/browser/pages/about/page.svelte +5 -0
  317. package/template/src/browser/pages/layout.svelte +26 -0
  318. package/template/src/browser/pages/page.svelte +20 -0
  319. package/template/src/bundle/icon.png +0 -0
  320. package/template/src/cli/banner.txt +3 -0
  321. package/template/src/cli/footer.txt +1 -0
  322. package/template/src/server/config.ts +17 -0
  323. package/template/src/server/rpc/getHello.ts +35 -0
  324. package/template/svelte.config.js +12 -0
  325. package/template/tsconfig.json +18 -0
  326. package/tsconfig.app.json +16 -0
@@ -0,0 +1,559 @@
1
+ import { activeCacheStore } from './activeCacheStore.ts'
2
+ import { canonicalJson } from './canonicalJson.ts'
3
+ import { decodeResponse } from './decodeResponse.ts'
4
+ import { getRemoteMeta } from './getRemoteMeta.ts'
5
+ import { globalCacheStore } from './globalCacheStore.ts'
6
+ import { invalidateEvent } from './invalidateEvent.ts'
7
+ import { keyForRemoteCall } from './keyForRemoteCall.ts'
8
+ import type { CacheEntry } from './types/CacheEntry.ts'
9
+ import type { CacheOptions } from './types/CacheOptions.ts'
10
+ import type { CacheStore } from './types/CacheStore.ts'
11
+ import type { RawRemoteFunction } from './types/RawRemoteFunction.ts'
12
+ import type { RemoteFunction } from './types/RemoteFunction.ts'
13
+
14
+ type AnyRemote<Args, Return> = RemoteFunction<Args, Return> | RawRemoteFunction<Args>
15
+ type Producer<Args, Return> = (args?: Args) => Promise<Return>
16
+
17
+ /*
18
+ Curries a call against a cache store. `cache(fn, options?)` returns an invoker;
19
+ calling that invoker with args checks the store for a prior entry and returns a
20
+ shared promise on hit, or invokes `fn` once and stores its promise on miss.
21
+ Splitting configuration (the outer call) from invocation (the inner call) keeps
22
+ options anchored in a fixed position so they can't collide with arg shapes. TTL
23
+ = undefined → forever; ttl = 0 → dedupe only; ttl > 0 → entry expires `ttl` ms
24
+ after the promise resolves.
25
+
26
+ `fn` is either a remote function (a GET/POST/... helper) or a plain producer
27
+ returning a Promise:
28
+
29
+ cache(getPost)({ id }) // → Promise<Post> (decoded body)
30
+ cache(getPost.raw)({ id }) // → Promise<Response> (raw escape hatch)
31
+ cache(fetchRates)() // → Promise<Rates> (plain producer)
32
+
33
+ Remote calls key on fn.method + fn.url + args and store the underlying Response
34
+ (the decoded view is derived on the way out for the non-raw variant; both share
35
+ one entry). Producers have no wire identity, so they key on the producer's
36
+ reference + args — pass a hoisted/stable function to dedupe (an inline arrow is a
37
+ new reference every call and never does), and the promise is stored and handed
38
+ back as-is (no Response, no decode, no SSR snapshot).
39
+
40
+ `options.global` puts the entry in the process-level store instead of the
41
+ request-scoped one, so a value computed in one request is reused by later
42
+ requests — the memoise-an-external-endpoint case. Default (omitted) is
43
+ request-scoped on the server, which keeps per-user data from leaking across
44
+ requests; on the client there is one tab store either way, so it is a no-op.
45
+
46
+ Reactivity is implicit: the invoker calls `store.subscribe(key)`, which
47
+ registers the surrounding $derived / $effect scope. Invalidating the key
48
+ then re-runs that scope, which calls cache() again and gets a fresh entry.
49
+ Outside a tracking scope subscribe() is a no-op, so cache() works the same
50
+ in server code and plain client code.
51
+
52
+ SSR: how you consume the call decides inline vs streaming, per Svelte's
53
+ documented {#await} rule (only the pending branch renders during SSR):
54
+
55
+ const post = await cache(getPost)({ id }) // blocks render → baked into
56
+ // the initial SSR HTML
57
+ {#await cache(getPost)({ id })} // renders pending → shell flushes
58
+ // now, value streams in on the
59
+ // same response when it resolves
60
+
61
+ The two don't mix within one component. A top-level `await` flips Svelte's
62
+ async render into await-everything mode and sweeps in every promise created
63
+ in that same component instance — so a sibling {#await} (or a
64
+ <svelte:boundary pending>) gets awaited and inlined too, buffering the whole
65
+ response to the slowest read. The markup form doesn't change this: a boundary
66
+ renders its pending branch but render() still blocks. To get both on one page,
67
+ isolate each blocking (top-level await) read in its own child component and
68
+ keep streaming reads in a parent that never top-level awaits — the
69
+ await-everything mode is per component instance, so a child's await blocks only
70
+ the child.
71
+ */
72
+ export function cache<Args, Return>(
73
+ fn: RemoteFunction<Args, Return>,
74
+ options?: CacheOptions,
75
+ ): (args?: Args) => Promise<Return>
76
+ export function cache<Args>(
77
+ fn: RawRemoteFunction<Args>,
78
+ options?: CacheOptions,
79
+ ): (args?: Args) => Promise<Response>
80
+ export function cache<Args, Return>(
81
+ fn: Producer<Args, Return>,
82
+ options?: CacheOptions,
83
+ ): (args?: Args) => Promise<Return>
84
+ export function cache<Args, Return>(
85
+ fn: AnyRemote<Args, Return> | Producer<Args, Return>,
86
+ options?: CacheOptions,
87
+ ): (args?: Args) => Promise<Return | Response> | Return {
88
+ /*
89
+ A remote function carries `url`/`method`; a plain producer carries neither —
90
+ that's the discriminator. Among remotes, the "raw" variant lacks its own
91
+ `.raw` sibling (only the decoded callable carries one), which selects whether
92
+ the decode step runs on the way out.
93
+ */
94
+ const isRemote = 'url' in fn
95
+ const isRaw = isRemote && !('raw' in fn)
96
+ const rawFn = !isRemote
97
+ ? undefined
98
+ : isRaw
99
+ ? (fn as RawRemoteFunction<Args>)
100
+ : (fn as RemoteFunction<Args, Return>).raw
101
+ return (args) => {
102
+ const store = options?.global ? globalCacheStore() : activeCacheStore()
103
+ if (!isRemote) {
104
+ return invokeProducer(store, fn as Producer<Args, Return>, args, options)
105
+ }
106
+ const remote = rawFn as RawRemoteFunction<Args>
107
+ const key = keyForRemoteCall(remote.method, remote.url, args)
108
+ store.subscribe(key)
109
+ const existing = store.entries.get(key)
110
+ if (existing) {
111
+ tagScope(existing, options?.scope)
112
+ }
113
+ /*
114
+ Snapshot warm path: hydration pre-decoded the SSR body onto the
115
+ entry, so the decoded variant returns it synchronously — the first
116
+ {#await} render resolves without a microtask suspension and matches
117
+ the SSR DOM. Raw callers always take the Response path. After an
118
+ invalidate the replacement entry carries no value and falls through
119
+ to the async fetch as before.
120
+
121
+ The public overload stays typed Promise<Return> on purpose: a
122
+ non-thenable is the only thing {#await} can render synchronously, so
123
+ the sync return is left as an internal optimization rather than
124
+ widened to `Return | Promise<Return>` (which would leak it into every
125
+ caller's types). The one cost is that `.then`/`.catch`/`.finally`
126
+ directly on a warm result throws — consume cache via `await`/`{#await}`,
127
+ never `.then`. Don't "fix" the type; see memory cache-warm-sync-tradeoff.
128
+
129
+ Each warm read returns its own clone of the stored value: the entry's
130
+ value is decoded once at hydration and would otherwise be handed by
131
+ reference to every component reading the key, so one reader mutating
132
+ it would corrupt the others and the hydrated state. A live fetch hands
133
+ each reader a freshly-decoded object, so cloning keeps warm reads
134
+ consistent with that. The clone is synchronous, preserving the
135
+ {#await}-renders-without-suspension property.
136
+ */
137
+ if (!isRaw && existing?.value !== undefined) {
138
+ return structuredClone(existing.value) as Return
139
+ }
140
+ const responsePromise = invokeRemote(
141
+ store,
142
+ key,
143
+ existing,
144
+ rawFn as RawRemoteFunction<Args>,
145
+ args,
146
+ options,
147
+ )
148
+ return isRaw ? responsePromise : (responsePromise.then(decodeResponse) as Promise<Return>)
149
+ }
150
+ }
151
+
152
+ /*
153
+ Producer path: key on the producer's reference + args, share the
154
+ in-flight/retained promise on hit, and store the value promise as-is on miss — no
155
+ Response, no decode, no SSR request metadata.
156
+ */
157
+ function invokeProducer<Args, Return>(
158
+ store: CacheStore,
159
+ producer: Producer<Args, Return>,
160
+ args: Args | undefined,
161
+ options: CacheOptions | undefined,
162
+ ): Promise<Return> {
163
+ const key = producerKey(producer, args)
164
+ store.subscribe(key)
165
+ const existing = store.entries.get(key)
166
+ if (existing) {
167
+ tagScope(existing, options?.scope)
168
+ return existing.promise as Promise<Return>
169
+ }
170
+ const promise = producer(args)
171
+ registerEntry(store, key, promise, options, undefined, () => producer(args))
172
+ return promise
173
+ }
174
+
175
+ function invokeRemote<Args>(
176
+ store: CacheStore,
177
+ key: string,
178
+ existing: CacheEntry | undefined,
179
+ rawFn: RawRemoteFunction<Args>,
180
+ args: Args | undefined,
181
+ options: CacheOptions | undefined,
182
+ ): Promise<Response> {
183
+ if (existing) {
184
+ return shareable(existing.promise as Promise<Response>)
185
+ }
186
+ const promise = rawFn(args as Args)
187
+ const request = getRemoteMeta(promise)
188
+ if (!request) {
189
+ throw new Error(
190
+ '[belte] cache() received a function whose call did not record metadata — was it produced by a verb helper?',
191
+ )
192
+ }
193
+ registerEntry(store, key, promise, options, request, () => rawFn(args as Args))
194
+ return shareable(promise)
195
+ }
196
+
197
+ /*
198
+ Stores a fresh entry and wires its settle / ttl / eviction lifecycle. Shared by
199
+ the remote and producer paths; `request` is set for remote entries (drives the
200
+ SSR snapshot) and undefined for producers.
201
+ */
202
+ function registerEntry(
203
+ store: CacheStore,
204
+ key: string,
205
+ promise: Promise<unknown>,
206
+ options: CacheOptions | undefined,
207
+ request: Request | undefined,
208
+ refetch: () => Promise<unknown>,
209
+ ): CacheEntry {
210
+ const ttl = options?.ttl
211
+ /* Capture the refetch thunk + policy only when an invalidate window was asked for. */
212
+ const policy = options?.invalidate
213
+ const invalidation =
214
+ policy?.throttle !== undefined || policy?.debounce !== undefined
215
+ ? { refetch, throttle: policy.throttle, debounce: policy.debounce }
216
+ : undefined
217
+ /*
218
+ A prior entry for this key was dropped by invalidate() and is awaiting its
219
+ next read — consume the marker so this replacement read reports as a reload
220
+ (cache.refreshing) until it settles, not as a first-ever load.
221
+ */
222
+ const refreshing = store.pendingRefresh.delete(key) || undefined
223
+ const entry: CacheEntry = {
224
+ key,
225
+ promise,
226
+ request,
227
+ ttl,
228
+ expiresAt: undefined,
229
+ scope: options?.scope === undefined ? undefined : toScopeSet(options.scope),
230
+ refreshing,
231
+ invalidation,
232
+ }
233
+ store.entries.set(key, entry)
234
+ markLifecycle(store)
235
+ /*
236
+ A ttl=0 remote entry in the request-scoped server store is kept until the
237
+ response is GC'd so the post-render SSR snapshot can still pick it up. That
238
+ exception never applies on the client (window defined), to producer entries
239
+ (never snapshotted), or to the process-level `global` store (not request-
240
+ scoped — keeping it would leak forever) — those evict the moment they settle.
241
+ */
242
+ const keepZeroTtlForSnapshot =
243
+ request !== undefined && !options?.global && typeof window === 'undefined'
244
+ function deleteIfCurrent() {
245
+ if (store.entries.get(key) === entry) {
246
+ store.entries.delete(key)
247
+ markLifecycle(store)
248
+ }
249
+ }
250
+ promise.then(() => {
251
+ /*
252
+ Mark settled so SSR snapshot serialization can tell awaited entries
253
+ (resolved by the time render() returns → inline) from {#await} ones
254
+ (still pending → stream). Set before the ttl branches below since a
255
+ ttl=0 server entry stays in the store for the snapshot.
256
+ */
257
+ entry.settled = true
258
+ /* The reload finished — this entry now holds fresh data, no longer refreshing. */
259
+ entry.refreshing = false
260
+ markLifecycle(store)
261
+ if (ttl === 0) {
262
+ if (!keepZeroTtlForSnapshot) {
263
+ deleteIfCurrent()
264
+ }
265
+ return
266
+ }
267
+ if (ttl !== undefined) {
268
+ entry.expiresAt = Date.now() + ttl
269
+ setTimeout(() => {
270
+ if ((entry.expiresAt ?? 0) <= Date.now()) {
271
+ deleteIfCurrent()
272
+ }
273
+ }, ttl).unref?.()
274
+ }
275
+ }, deleteIfCurrent)
276
+ return entry
277
+ }
278
+
279
+ /*
280
+ Returns a promise that resolves to a fresh clone of the underlying Response.
281
+ Multiple readers can each consume the body independently — the stored
282
+ promise's Response is never consumed directly, so clones always succeed.
283
+ */
284
+ function shareable(promise: Promise<Response>): Promise<Response> {
285
+ return promise.then((response) => response.clone())
286
+ }
287
+
288
+ type Selector<Args, Return> =
289
+ | AnyRemote<Args, Return>
290
+ | Producer<Args, Return>
291
+ | Pick<CacheOptions, 'scope'>
292
+
293
+ /*
294
+ Compiles a selector into an entry predicate shared by invalidate() and
295
+ pending() so both interpret the call shapes identically:
296
+ undefined → every entry
297
+ remote fn → that function's calls (method+url prefix). `arg.url` is
298
+ the route template; per-call args appear as `?...`
299
+ (GET/DELETE) or after a space (canonical-json body) —
300
+ see keyForRemoteCall. `fn` and `fn.raw` match the same
301
+ set since they share method+url.
302
+ producer fn → that producer's calls (reference id prefix). Matches
303
+ only if the producer was cached at least once (else it
304
+ has no id and nothing matches).
305
+ { scope } → any entry sharing one of the requested scope tags. An
306
+ empty selector matches nothing.
307
+ */
308
+ function selectorMatcher<Args, Return>(
309
+ arg?: Selector<Args, Return>,
310
+ ): (entry: CacheEntry) => boolean {
311
+ if (arg === undefined) {
312
+ return () => true
313
+ }
314
+ if (typeof arg === 'function') {
315
+ /* Remote fns carry url/method; a producer keys on its reference id. */
316
+ const prefix = 'url' in arg ? `${arg.method} ${arg.url}` : existingProducerId(arg)
317
+ if (prefix === undefined) {
318
+ return () => false
319
+ }
320
+ return (entry) =>
321
+ entry.key === prefix ||
322
+ entry.key.startsWith(`${prefix}?`) ||
323
+ entry.key.startsWith(`${prefix} `)
324
+ }
325
+ if (arg.scope === undefined) {
326
+ return () => false
327
+ }
328
+ const requestedScopes = toScopeSet(arg.scope)
329
+ return (entry) => entry.scope !== undefined && intersects(entry.scope, requestedScopes)
330
+ }
331
+
332
+ /* Active + process-level stores, deduped (one tab store on the client). */
333
+ function cacheStores(): CacheStore[] {
334
+ const active = activeCacheStore()
335
+ const global = globalCacheStore()
336
+ return active === global ? [active] : [active, global]
337
+ }
338
+
339
+ /*
340
+ Invalidates every entry matching the selector (see selectorMatcher) across both
341
+ the request/tab store and the process-level store, and notifies readers. An entry
342
+ with an invalidate throttle/debounce policy is kept and its refetch coalesced (stale served
343
+ until it resolves); every other match is dropped so the next read refetches —
344
+ its key recorded in pendingRefresh so that read reports as a reload (cache.refreshing)
345
+ rather than a first-ever load. An empty or unmatched selector is a no-op on the
346
+ cache; the lifecycle ping still fires but recomputes pending() to the same value.
347
+ */
348
+ function invalidate<Args, Return>(arg?: Selector<Args, Return>): void {
349
+ const matches = selectorMatcher(arg)
350
+ for (const store of cacheStores()) {
351
+ const affected: string[] = []
352
+ /* Deleting the current entry mid-iteration is spec-safe on a Map; no snapshot needed. */
353
+ for (const entry of store.entries.values()) {
354
+ if (!matches(entry)) {
355
+ continue
356
+ }
357
+ if (entry.invalidation) {
358
+ scheduleInvalidationRefetch(store, entry)
359
+ } else {
360
+ store.entries.delete(entry.key)
361
+ /* Mark so the next read of this key reports as a reload via cache.refreshing. */
362
+ store.pendingRefresh.add(entry.key)
363
+ affected.push(entry.key)
364
+ }
365
+ }
366
+ emit(store, affected)
367
+ markLifecycle(store)
368
+ }
369
+ }
370
+
371
+ cache.invalidate = invalidate
372
+
373
+ /*
374
+ Schedules a coalesced refetch per the entry's invalidate policy. debounce: (re)arm
375
+ a timer that fires after N ms of quiet. throttle: fire on the leading edge when a
376
+ full window has elapsed since the last fire, else arm a single trailing timer for
377
+ the remainder — so a continuous invalidation stream refetches at most once per window.
378
+ */
379
+ function scheduleInvalidationRefetch(store: CacheStore, entry: CacheEntry): void {
380
+ const policy = entry.invalidation
381
+ if (!policy) {
382
+ return
383
+ }
384
+ if (policy.debounce !== undefined) {
385
+ clearTimeout(policy.timer)
386
+ policy.timer = armTimer(store, entry, policy.debounce)
387
+ return
388
+ }
389
+ const throttleMs = policy.throttle ?? 0
390
+ const elapsed = Date.now() - (policy.lastFiredAt ?? Number.NEGATIVE_INFINITY)
391
+ if (elapsed >= throttleMs) {
392
+ fireRefetch(store, entry)
393
+ return
394
+ }
395
+ if (policy.timer === undefined) {
396
+ policy.timer = armTimer(store, entry, throttleMs - elapsed)
397
+ }
398
+ }
399
+
400
+ function armTimer(store: CacheStore, entry: CacheEntry, ms: number): ReturnType<typeof setTimeout> {
401
+ const timer = setTimeout(() => {
402
+ if (entry.invalidation) {
403
+ entry.invalidation.timer = undefined
404
+ }
405
+ fireRefetch(store, entry)
406
+ }, ms)
407
+ timer.unref?.()
408
+ return timer
409
+ }
410
+
411
+ /*
412
+ Runs the captured refetch once, keeping the stale value visible until it
413
+ resolves, then swaps the fresh result in and notifies readers. A refetch already
414
+ in flight is left to finish — the key is stable, so it already fetches the latest
415
+ state. A rejected refetch keeps the stale entry (no notify).
416
+ */
417
+ function fireRefetch(store: CacheStore, entry: CacheEntry): void {
418
+ const policy = entry.invalidation
419
+ if (!policy || entry.refreshing) {
420
+ return
421
+ }
422
+ entry.refreshing = true
423
+ policy.lastFiredAt = Date.now()
424
+ /* Ping lifecycle so cache.refreshing re-derives when revalidation begins; the settle handlers ping again when it ends. */
425
+ markLifecycle(store)
426
+ const inflight = policy.refetch()
427
+ inflight.then(
428
+ () => {
429
+ entry.refreshing = false
430
+ /* Dropped or replaced while in flight — discard this result. */
431
+ if (store.entries.get(entry.key) !== entry) {
432
+ return
433
+ }
434
+ entry.promise = inflight
435
+ entry.value = undefined
436
+ entry.settled = true
437
+ markLifecycle(store)
438
+ emit(store, [entry.key])
439
+ },
440
+ () => {
441
+ entry.refreshing = false
442
+ markLifecycle(store)
443
+ },
444
+ )
445
+ }
446
+
447
+ /*
448
+ Reactive in-flight probe sharing invalidate's selector grammar:
449
+ pending() → any rpc in flight (global progress bar)
450
+ pending(fn) → that function's calls (per-route spinner)
451
+ pending({ scope }) → a tagged group
452
+ Returns true while any matching entry's promise is unsettled across the
453
+ request/tab and process-level stores. The read taps each store's lifecycle
454
+ channel (track both before checking, so neither is skipped by short-circuit), so
455
+ a $derived re-runs when a matching call starts or settles. Outside a tracking
456
+ scope (plain client code, SSR) the tap is a no-op and it returns the current
457
+ value — SSR loading state is driven by {#await}, not this.
458
+ */
459
+ function pending<Args, Return>(arg?: Selector<Args, Return>): boolean {
460
+ const matches = selectorMatcher(arg)
461
+ const stores = cacheStores()
462
+ stores.forEach((store) => {
463
+ store.trackLifecycle()
464
+ })
465
+ return stores.some((store) =>
466
+ store.entries.values().some((entry) => entry.settled !== true && matches(entry)),
467
+ )
468
+ }
469
+
470
+ cache.pending = pending
471
+
472
+ /*
473
+ Reactive revalidation probe sharing invalidate's selector grammar:
474
+ refreshing() → any entry reloading data it already had
475
+ refreshing(fn) → that function's calls (per-route "updating…" badge)
476
+ refreshing({ scope }) → a tagged group
477
+ Returns true while any matching entry is reloading data it previously held:
478
+ either a policy stale-while-revalidate refetch (settled, value visible, fresh
479
+ fetch in flight) or the default drop-then-reload (the key was invalidated and
480
+ dropped, this read refetches it — pending is also true here). The distinction
481
+ from cache.pending: pending answers "is any matching call in flight?" (covers
482
+ first-ever loads), refreshing answers "is a matching call reloading data that
483
+ was already loaded once?". Taps each store's lifecycle channel (both before
484
+ checking, so neither is skipped by short-circuit) so a $derived re-runs when a
485
+ refresh starts or ends. Outside a tracking scope it returns the current value.
486
+ */
487
+ function refreshing<Args, Return>(arg?: Selector<Args, Return>): boolean {
488
+ const matches = selectorMatcher(arg)
489
+ const stores = cacheStores()
490
+ stores.forEach((store) => {
491
+ store.trackLifecycle()
492
+ })
493
+ return stores.some((store) =>
494
+ store.entries.values().some((entry) => entry.refreshing === true && matches(entry)),
495
+ )
496
+ }
497
+
498
+ cache.refreshing = refreshing
499
+
500
+ /* Signals cache.pending / cache.refreshing readers that in-flight membership changed. */
501
+ function markLifecycle(store: CacheStore): void {
502
+ store.events.dispatchEvent(new Event('lifecycle'))
503
+ }
504
+
505
+ /*
506
+ Producers have no wire identity, so each is assigned a stable id on first use,
507
+ kept in a WeakMap so it's collected with the function. The cache key is that id
508
+ plus the canonicalised args — a hoisted producer dedupes across calls; an inline
509
+ arrow gets a fresh id every call and never does.
510
+ */
511
+ const producerIds = new WeakMap<object, string>()
512
+ let producerCounter = 0
513
+ function producerKey(producer: object, args: unknown): string {
514
+ let id = producerIds.get(producer)
515
+ if (id === undefined) {
516
+ id = `@producer:${++producerCounter}`
517
+ producerIds.set(producer, id)
518
+ }
519
+ return args === undefined ? id : `${id} ${canonicalJson(args)}`
520
+ }
521
+
522
+ /* The producer's id without assigning one — for selectors matching prior entries. */
523
+ function existingProducerId(producer: object): string | undefined {
524
+ return producerIds.get(producer)
525
+ }
526
+
527
+ /* Normalizes a scope option (one tag or many) to a Set for O(1) membership. */
528
+ function toScopeSet(scope: string | string[]): Set<string> {
529
+ return new Set(typeof scope === 'string' ? [scope] : scope)
530
+ }
531
+
532
+ /* Folds new tags into an entry's existing set without duplicating them. */
533
+ function mergeScopes(existing: Set<string> | undefined, incoming: string | string[]): Set<string> {
534
+ return new Set([...(existing ?? []), ...toScopeSet(incoming)])
535
+ }
536
+
537
+ /*
538
+ Tags an existing entry with a read's scope so a later cache.invalidate({ scope })
539
+ reaches entries hydrated from the SSR snapshot (which carry a value but no scope)
540
+ without a refetch. Merges rather than replaces so a read tagging one group can't
541
+ drop tags another read site already added; a no-op when the read passes no scope.
542
+ */
543
+ function tagScope(entry: CacheEntry, scope: CacheOptions['scope']): void {
544
+ if (scope !== undefined) {
545
+ entry.scope = mergeScopes(entry.scope, scope)
546
+ }
547
+ }
548
+
549
+ /* True when an entry's tags and the requested tags overlap on any tag. */
550
+ function intersects(entryScopes: Set<string>, requestedScopes: Set<string>): boolean {
551
+ return requestedScopes.values().some((scope) => entryScopes.has(scope))
552
+ }
553
+
554
+ function emit(store: ReturnType<typeof activeCacheStore>, keys: string[]): void {
555
+ if (keys.length === 0) {
556
+ return
557
+ }
558
+ store.events.dispatchEvent(invalidateEvent(keys))
559
+ }
@@ -0,0 +1,16 @@
1
+ import type { CacheStore } from './types/CacheStore.ts'
2
+
3
+ /*
4
+ Internal slot the runtime entries register their resolver into. The
5
+ server entry installs an ALS-backed resolver (request-scoped); the
6
+ client entry installs a module-singleton resolver. `fallback` is a
7
+ single lazy store used only when no resolver is registered — keeps
8
+ isolated tests working without forcing them to spin up the runtime.
9
+ */
10
+ export const cacheStoreSlot: {
11
+ resolver: (() => CacheStore | undefined) | undefined
12
+ fallback: CacheStore | undefined
13
+ } = {
14
+ resolver: undefined,
15
+ fallback: undefined,
16
+ }
@@ -0,0 +1,63 @@
1
+ /*
2
+ Deterministic key string for a value. Object keys (and Map keys, Set members)
3
+ are sorted so values differing only in insertion order produce identical
4
+ strings, and every recognised type carries a tag so distinct types never
5
+ collide — a Date never equals the string of its ISO form, a Map never equals a
6
+ plain object. Used to derive cache keys from producer args and from auto-keyed
7
+ POST/PUT/PATCH bodies; the output is a key, not a request body, so it is free to
8
+ encode types JSON.stringify would silently flatten (Map/Set → {}), coerce
9
+ (Date → its ISO string, via toJSON before any replacer sees it) or drop
10
+ (undefined). Covers the value types commonly passed as rpc args: primitives,
11
+ arrays, plain objects, Date, Map, Set, and bigint. Functions and symbols can't
12
+ key anything meaningful but are tagged rather than dropped so a stray one can't
13
+ silently collapse two distinct argument sets onto the same key.
14
+ */
15
+ export function canonicalJson(value: unknown): string {
16
+ if (value === null) {
17
+ return 'null'
18
+ }
19
+ if (value === undefined) {
20
+ return 'undefined'
21
+ }
22
+ const type = typeof value
23
+ if (type === 'string') {
24
+ return JSON.stringify(value)
25
+ }
26
+ if (type === 'bigint') {
27
+ return `${value}n`
28
+ }
29
+ if (type === 'number') {
30
+ // -0 and 0 stringify alike but are distinct keys; NaN/Infinity stay stable (JSON drops them to null).
31
+ return Object.is(value, -0) ? '-0' : String(value)
32
+ }
33
+ if (type === 'boolean') {
34
+ return String(value)
35
+ }
36
+ if (type !== 'object') {
37
+ // function | symbol — not serialisable; tag by type so the key can't crash or alias a real value.
38
+ return `${type}()`
39
+ }
40
+ if (value instanceof Date) {
41
+ return `Date(${value.getTime()})`
42
+ }
43
+ if (Array.isArray(value)) {
44
+ return `[${value.map(canonicalJson).join(',')}]`
45
+ }
46
+ if (value instanceof Map) {
47
+ // Sort by encoded entry so key order doesn't change the result.
48
+ const entries = Array.from(
49
+ value,
50
+ ([key, val]) => `${canonicalJson(key)}=>${canonicalJson(val)}`,
51
+ ).sort()
52
+ return `Map{${entries.join(',')}}`
53
+ }
54
+ if (value instanceof Set) {
55
+ const members = Array.from(value, canonicalJson).sort()
56
+ return `Set{${members.join(',')}}`
57
+ }
58
+ const record = value as Record<string, unknown>
59
+ const entries = Object.keys(record)
60
+ .sort()
61
+ .map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`)
62
+ return `{${entries.join(',')}}`
63
+ }
@@ -0,0 +1,13 @@
1
+ import type { HttpVerb } from './types/HttpVerb.ts'
2
+
3
+ /*
4
+ Whether a verb carries its args in the request body (POST/PUT/PATCH) vs
5
+ on the query string (GET/DELETE/HEAD). Single source for the split so the
6
+ synthesized Request (buildRpcRequest), the handler-side parse (parseArgs),
7
+ the cache key (keyForRemoteCall), and the OpenAPI doc can't disagree.
8
+ */
9
+ const BODY_METHODS = new Set<HttpVerb>(['POST', 'PUT', 'PATCH'])
10
+
11
+ export function carriesBodyArgs(method: HttpVerb): boolean {
12
+ return BODY_METHODS.has(method)
13
+ }
@@ -0,0 +1,7 @@
1
+ import { rm } from 'node:fs/promises'
2
+ import { lastConnectionPath } from './lastConnectionPath.ts'
3
+
4
+ // Forgets the saved connection (the `/disconnect` reset). Missing file is a no-op.
5
+ export async function clearLastConnection(programName: string): Promise<void> {
6
+ await rm(lastConnectionPath(programName), { force: true })
7
+ }
@@ -0,0 +1,17 @@
1
+ /*
2
+ Derives the MCP tool name / CLI subcommand name from an rpc URL. Strips
3
+ the framework's `/rpc/` mount and joins nested folder segments with `-`
4
+ so `users/list.ts` (mounted at `/rpc/users/list`) becomes `users-list`
5
+ across both surfaces. Folder prefixing prevents collisions when two
6
+ files in different folders share the same stem (e.g. `users/list.ts`
7
+ and `posts/list.ts`); `/` is not a valid character in MCP tool names or
8
+ typical CLI subcommands, so the join uses `-`.
9
+ */
10
+ const RPC_PREFIX = '/rpc/'
11
+
12
+ export function commandNameForUrl(url: string): string {
13
+ const trimmed = url.startsWith(RPC_PREFIX)
14
+ ? url.slice(RPC_PREFIX.length)
15
+ : url.replace(/^\//, '')
16
+ return trimmed.replaceAll('/', '-')
17
+ }