@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,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
|
+
}
|