@abide/abide 0.28.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 +607 -0
- package/LICENSE +21 -0
- package/README.md +154 -0
- package/bin/abide.ts +212 -0
- package/package.json +155 -0
- package/src/abideLsp.ts +211 -0
- package/src/abideModules.d.ts +8 -0
- package/src/abideResolverPlugin.ts +923 -0
- package/src/appEntry.ts +151 -0
- package/src/assets/app.html +12 -0
- package/src/build.ts +143 -0
- package/src/buildCli.ts +127 -0
- package/src/buildDisconnected.ts +118 -0
- package/src/bundleApp.ts +147 -0
- package/src/bundleDisconnectedEntry.ts +14 -0
- package/src/checkAbide.ts +77 -0
- package/src/cliEntry.ts +25 -0
- package/src/clientBuildPlugins.ts +33 -0
- package/src/clientEntry.ts +17 -0
- package/src/compile.ts +63 -0
- package/src/controlServerWorker.ts +426 -0
- package/src/devEntry.ts +250 -0
- package/src/discoveryEntry.ts +81 -0
- package/src/lib/bundle/BundleMenu.ts +12 -0
- package/src/lib/bundle/BundleMenuItem.ts +25 -0
- package/src/lib/bundle/BundleWindow.ts +37 -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 +34 -0
- package/src/lib/bundle/buildWebviewLib.ts +111 -0
- package/src/lib/bundle/bundled.ts +35 -0
- package/src/lib/bundle/disconnected.abide +236 -0
- package/src/lib/bundle/disconnected.css +9 -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/abideMenu.mm +422 -0
- package/src/lib/bundle/native/webview.h +4557 -0
- package/src/lib/bundle/onMenu.ts +42 -0
- package/src/lib/bundle/openWebview.ts +104 -0
- package/src/lib/bundle/pngToIcns.ts +47 -0
- package/src/lib/bundle/probeAbideServer.ts +57 -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 +37 -0
- package/src/lib/bundle/spawnEmbeddedServer.ts +64 -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 +108 -0
- package/src/lib/cli/dispatchCommand.ts +71 -0
- package/src/lib/cli/loadEnvFromBinaryDir.ts +15 -0
- package/src/lib/cli/parseArgvForRpc.ts +100 -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 +176 -0
- package/src/lib/cli/runSession.ts +108 -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 +102 -0
- package/src/lib/mcp/createMcpServer.ts +48 -0
- package/src/lib/mcp/dispatchMcpRequest.ts +138 -0
- package/src/lib/mcp/mcpResourceServerSlot.ts +18 -0
- package/src/lib/mcp/mcpSurface.ts +295 -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 +47 -0
- package/src/lib/server/DELETE.ts +10 -0
- package/src/lib/server/GET.ts +10 -0
- package/src/lib/server/HEAD.ts +10 -0
- package/src/lib/server/PATCH.ts +10 -0
- package/src/lib/server/POST.ts +10 -0
- package/src/lib/server/PUT.ts +10 -0
- package/src/lib/server/agent.ts +86 -0
- package/src/lib/server/appDataDir.ts +16 -0
- package/src/lib/server/cli/buildEnvContent.ts +19 -0
- package/src/lib/server/cli/createTarGz.ts +77 -0
- package/src/lib/server/cli/handleCliDownload.ts +150 -0
- package/src/lib/server/cli/handleCliInstall.ts +37 -0
- package/src/lib/server/cli/installScript.ts +31 -0
- package/src/lib/server/cli/maxSourceMtime.ts +26 -0
- package/src/lib/server/cookies.ts +30 -0
- package/src/lib/server/env.ts +51 -0
- package/src/lib/server/error.ts +73 -0
- package/src/lib/server/json.ts +42 -0
- package/src/lib/server/jsonl.ts +47 -0
- package/src/lib/server/prompts/definePrompt.ts +21 -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 +17 -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/reachable.ts +45 -0
- package/src/lib/server/redirect.ts +43 -0
- package/src/lib/server/request.ts +19 -0
- package/src/lib/server/rpc/defineVerb.ts +210 -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 +127 -0
- package/src/lib/server/rpc/readBodyWithinLimit.ts +44 -0
- package/src/lib/server/rpc/registerVerb.ts +6 -0
- package/src/lib/server/rpc/runWithVerbTimeout.ts +49 -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 +87 -0
- package/src/lib/server/rpc/types/VerbRegistryEntry.ts +35 -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_READY_MESSAGE.ts +6 -0
- package/src/lib/server/runtime/DEV_REBUILD_MESSAGE.ts +4 -0
- package/src/lib/server/runtime/DEV_RELOAD_CLIENT_SCRIPT.ts +107 -0
- package/src/lib/server/runtime/SSR_SWAP_SCRIPT.ts +16 -0
- package/src/lib/server/runtime/acceptsGzip.ts +24 -0
- package/src/lib/server/runtime/buildCacheSnapshot.ts +61 -0
- package/src/lib/server/runtime/buildHealthPayload.ts +34 -0
- package/src/lib/server/runtime/buildInspectorSurface.ts +37 -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/createAppAssetServer.ts +76 -0
- package/src/lib/server/runtime/createAssetHeaderCache.ts +31 -0
- package/src/lib/server/runtime/createPublicAssetServer.ts +67 -0
- package/src/lib/server/runtime/createReachable.ts +109 -0
- package/src/lib/server/runtime/createRouteDispatcher.ts +127 -0
- package/src/lib/server/runtime/createServer.ts +674 -0
- package/src/lib/server/runtime/createUiPageRenderer.ts +181 -0
- package/src/lib/server/runtime/crossOriginForbidden.ts +17 -0
- package/src/lib/server/runtime/crossOriginGate.ts +29 -0
- package/src/lib/server/runtime/devClientFingerprint.ts +117 -0
- package/src/lib/server/runtime/devHotModuleResponse.ts +40 -0
- package/src/lib/server/runtime/devReloadResponse.ts +41 -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 +21 -0
- package/src/lib/server/runtime/getActiveServer.ts +6 -0
- package/src/lib/server/runtime/globToPathSet.ts +29 -0
- package/src/lib/server/runtime/gzipResponse.ts +46 -0
- package/src/lib/server/runtime/inProcessServer.ts +20 -0
- package/src/lib/server/runtime/internalErrorResponse.ts +25 -0
- package/src/lib/server/runtime/isCrossOriginRequest.ts +23 -0
- package/src/lib/server/runtime/listenOnOpenPort.ts +36 -0
- package/src/lib/server/runtime/logExposedSurfaces.ts +156 -0
- package/src/lib/server/runtime/maybeMountInspector.ts +97 -0
- package/src/lib/server/runtime/mimeForExtension.ts +14 -0
- package/src/lib/server/runtime/pageUrlFromStore.ts +15 -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/resolvePageSnapshot.ts +25 -0
- package/src/lib/server/runtime/respondWithEmbeddedAsset.ts +18 -0
- package/src/lib/server/runtime/runWithRequestScope.ts +150 -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 +83 -0
- package/src/lib/server/runtime/streamCacheResolutions.ts +37 -0
- package/src/lib/server/runtime/streamFromIterator.ts +86 -0
- package/src/lib/server/runtime/types/Assets.ts +6 -0
- package/src/lib/server/runtime/types/DevReloadStamp.ts +18 -0
- package/src/lib/server/runtime/types/InspectorCacheEntry.ts +24 -0
- package/src/lib/server/runtime/types/InspectorCacheSnapshot.ts +11 -0
- package/src/lib/server/runtime/types/InspectorContext.ts +30 -0
- package/src/lib/server/runtime/types/InspectorSocket.ts +17 -0
- package/src/lib/server/runtime/types/InspectorSurface.ts +13 -0
- package/src/lib/server/runtime/types/InspectorVerb.ts +27 -0
- package/src/lib/server/runtime/types/RequestStore.ts +55 -0
- package/src/lib/server/runtime/warnUnguardedMcp.ts +32 -0
- package/src/lib/server/runtime/withResponseDefaults.ts +24 -0
- package/src/lib/server/server.ts +33 -0
- package/src/lib/server/socket.ts +32 -0
- package/src/lib/server/sockets/createSocketDispatcher.ts +337 -0
- package/src/lib/server/sockets/defineSocket.ts +179 -0
- package/src/lib/server/sockets/lookupSocket.ts +6 -0
- package/src/lib/server/sockets/registerSocket.ts +6 -0
- package/src/lib/server/sockets/socketOperations.ts +36 -0
- package/src/lib/server/sockets/socketRegistry.ts +9 -0
- package/src/lib/server/sockets/types/Socket.ts +23 -0
- package/src/lib/server/sockets/types/SocketClientFrame.ts +19 -0
- package/src/lib/server/sockets/types/SocketOperation.ts +22 -0
- package/src/lib/server/sockets/types/SocketOptions.ts +26 -0
- package/src/lib/server/sockets/types/SocketRegistryEntry.ts +19 -0
- package/src/lib/server/sockets/types/SocketRoutes.ts +10 -0
- package/src/lib/server/sockets/types/SocketServerFrame.ts +24 -0
- package/src/lib/server/sse.ts +54 -0
- package/src/lib/shared/ABIDE_PACKAGE_NAME.ts +7 -0
- package/src/lib/shared/ABIDE_VERSION.ts +9 -0
- package/src/lib/shared/CACHE_CONTROL_VALUES.ts +16 -0
- package/src/lib/shared/CACHE_WRAPPED.ts +8 -0
- package/src/lib/shared/CLI_PATH.ts +7 -0
- package/src/lib/shared/DEV_HOT_PREFIX.ts +7 -0
- package/src/lib/shared/DEV_RELOAD_PATH.ts +6 -0
- package/src/lib/shared/HEALTH_PATH.ts +7 -0
- package/src/lib/shared/HttpError.ts +20 -0
- package/src/lib/shared/IDENTITY_PATH.ts +6 -0
- package/src/lib/shared/INSPECTOR_PATH.ts +7 -0
- package/src/lib/shared/NAV_HEADER.ts +8 -0
- package/src/lib/shared/OFFLINE_HEADER.ts +8 -0
- package/src/lib/shared/REMOTE_FUNCTION.ts +8 -0
- package/src/lib/shared/REPLAYABLE_METHODS.ts +12 -0
- package/src/lib/shared/SOCKETS_PATH.ts +7 -0
- package/src/lib/shared/STREAMING_CONTENT_TYPES.ts +11 -0
- package/src/lib/shared/SocketDisconnectedError.ts +13 -0
- package/src/lib/shared/TEXT_PLAIN.ts +7 -0
- package/src/lib/shared/abideImportName.ts +44 -0
- package/src/lib/shared/abideLog.ts +38 -0
- package/src/lib/shared/activeCacheStore.ts +20 -0
- package/src/lib/shared/activePage.ts +25 -0
- package/src/lib/shared/appDataDir.ts +34 -0
- package/src/lib/shared/appNameSlot.ts +10 -0
- package/src/lib/shared/basePath.ts +10 -0
- package/src/lib/shared/basePathFromAppUrl.ts +20 -0
- package/src/lib/shared/baseSlot.ts +14 -0
- package/src/lib/shared/binaryDirEnvPath.ts +12 -0
- package/src/lib/shared/browserClientFlags.ts +10 -0
- package/src/lib/shared/buildRpcProxy.ts +39 -0
- package/src/lib/shared/buildRpcRequest.ts +70 -0
- package/src/lib/shared/buildSocketOverChannel.ts +58 -0
- package/src/lib/shared/bundleLayout.ts +36 -0
- package/src/lib/shared/cache.ts +951 -0
- package/src/lib/shared/cacheEntryFromSnapshot.ts +59 -0
- package/src/lib/shared/cacheStoreSlot.ts +16 -0
- package/src/lib/shared/cacheStores.ts +10 -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 +104 -0
- package/src/lib/shared/createChannelLog.ts +122 -0
- package/src/lib/shared/createLifecycleChannel.ts +56 -0
- package/src/lib/shared/createLivenessWatch.ts +118 -0
- package/src/lib/shared/createPushIterator.ts +127 -0
- package/src/lib/shared/createRemoteFunction.ts +122 -0
- package/src/lib/shared/createSubscriber.ts +55 -0
- package/src/lib/shared/createTraceContext.ts +21 -0
- package/src/lib/shared/dataDirEnvPath.ts +12 -0
- package/src/lib/shared/decodeResponse.ts +47 -0
- package/src/lib/shared/detectTarget.ts +27 -0
- package/src/lib/shared/detectVerbMethod.ts +17 -0
- package/src/lib/shared/emitLogRecord.ts +190 -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 +476 -0
- package/src/lib/shared/formatTraceparent.ts +6 -0
- package/src/lib/shared/forwardHeaders.ts +44 -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/health.ts +179 -0
- package/src/lib/shared/healthReadSlot.ts +11 -0
- package/src/lib/shared/healthSeedSlot.ts +12 -0
- package/src/lib/shared/html.ts +38 -0
- package/src/lib/shared/importNamesToStrip.ts +13 -0
- package/src/lib/shared/invalidateEvent.ts +11 -0
- package/src/lib/shared/invalidateTripwire.ts +40 -0
- package/src/lib/shared/isAbideHealthPayload.ts +11 -0
- package/src/lib/shared/isCompileTarget.ts +15 -0
- package/src/lib/shared/isDebugEnabled.ts +26 -0
- package/src/lib/shared/isDebugNegated.ts +19 -0
- package/src/lib/shared/isModuleNotFound.ts +16 -0
- package/src/lib/shared/isReadOnlyMethod.ts +14 -0
- package/src/lib/shared/isReplayableMethod.ts +7 -0
- package/src/lib/shared/isStreamingResponse.ts +11 -0
- package/src/lib/shared/isSubscribable.ts +15 -0
- package/src/lib/shared/jsonSchemaForPromptArguments.ts +29 -0
- package/src/lib/shared/jsonSchemaForSchema.ts +39 -0
- package/src/lib/shared/jsonlErrorFrame.ts +24 -0
- package/src/lib/shared/keyForRemoteCall.ts +29 -0
- package/src/lib/shared/keyMatchesPrefix.ts +9 -0
- package/src/lib/shared/lastConnectionPath.ts +7 -0
- package/src/lib/shared/layoutChainForRoute.ts +22 -0
- package/src/lib/shared/loadEnvFile.ts +17 -0
- package/src/lib/shared/loadEnvFromDataDir.ts +14 -0
- package/src/lib/shared/log.ts +24 -0
- package/src/lib/shared/logClosingRecord.ts +28 -0
- package/src/lib/shared/logTapSlot.ts +13 -0
- package/src/lib/shared/manifestModule.ts +39 -0
- package/src/lib/shared/matchesDebugPattern.ts +16 -0
- package/src/lib/shared/memoizeByKey.ts +32 -0
- package/src/lib/shared/normalizeTarget.ts +10 -0
- package/src/lib/shared/online.ts +51 -0
- package/src/lib/shared/page.ts +30 -0
- package/src/lib/shared/pageSlot.ts +17 -0
- package/src/lib/shared/pageUrlForFile.ts +14 -0
- package/src/lib/shared/parseBoundedEnvInt.ts +20 -0
- package/src/lib/shared/parseDebugPatterns.ts +21 -0
- package/src/lib/shared/parseEnv.ts +30 -0
- package/src/lib/shared/parsePromptMarkdown.ts +35 -0
- package/src/lib/shared/parseRouteSegments.ts +22 -0
- package/src/lib/shared/parseTraceparent.ts +26 -0
- package/src/lib/shared/pending.ts +30 -0
- package/src/lib/shared/prepareRpcModule.ts +59 -0
- package/src/lib/shared/prepareSocketModule.ts +49 -0
- package/src/lib/shared/probeRegistries.ts +68 -0
- package/src/lib/shared/producerKey.ts +32 -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/randomHexId.ts +14 -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/refreshing.ts +31 -0
- package/src/lib/shared/remoteMetaStore.ts +16 -0
- package/src/lib/shared/requestScopeSlot.ts +15 -0
- package/src/lib/shared/resolveClientFlags.ts +20 -0
- package/src/lib/shared/responseErrorText.ts +9 -0
- package/src/lib/shared/rpcTimeoutSlot.ts +9 -0
- package/src/lib/shared/rpcUrlForFile.ts +19 -0
- package/src/lib/shared/runningAsStandaloneBinary.ts +13 -0
- package/src/lib/shared/selectorMatcher.ts +68 -0
- package/src/lib/shared/selectorPrefix.ts +39 -0
- package/src/lib/shared/serializeEnv.ts +18 -0
- package/src/lib/shared/setAppName.ts +5 -0
- package/src/lib/shared/setBaseResolver.ts +6 -0
- package/src/lib/shared/setCacheStoreResolver.ts +6 -0
- package/src/lib/shared/setGlobalCacheStoreResolver.ts +6 -0
- package/src/lib/shared/setPageResolver.ts +7 -0
- package/src/lib/shared/setRequestScopeResolver.ts +6 -0
- package/src/lib/shared/snippet.ts +25 -0
- package/src/lib/shared/socketNameForFile.ts +11 -0
- package/src/lib/shared/socketTapSlot.ts +12 -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/tailProbeSlot.ts +16 -0
- package/src/lib/shared/toBunRoutePattern.ts +28 -0
- package/src/lib/shared/toScopeSet.ts +4 -0
- package/src/lib/shared/trace.ts +16 -0
- package/src/lib/shared/types/CacheEntry.ts +84 -0
- package/src/lib/shared/types/CacheInvalidation.ts +9 -0
- package/src/lib/shared/types/CacheOnContext.ts +25 -0
- package/src/lib/shared/types/CacheOptions.ts +39 -0
- package/src/lib/shared/types/CacheSelector.ts +17 -0
- package/src/lib/shared/types/CacheSnapshot.ts +16 -0
- package/src/lib/shared/types/CacheSnapshotEntry.ts +17 -0
- package/src/lib/shared/types/CacheStats.ts +13 -0
- package/src/lib/shared/types/CacheStore.ts +39 -0
- package/src/lib/shared/types/ChannelLog.ts +13 -0
- package/src/lib/shared/types/ClientFlags.ts +11 -0
- package/src/lib/shared/types/CompileTarget.ts +6 -0
- package/src/lib/shared/types/FrameworkLog.ts +13 -0
- package/src/lib/shared/types/HttpVerb.ts +1 -0
- package/src/lib/shared/types/LastConnection.ts +9 -0
- package/src/lib/shared/types/Log.ts +13 -0
- package/src/lib/shared/types/LogRecord.ts +42 -0
- package/src/lib/shared/types/LogVoice.ts +7 -0
- package/src/lib/shared/types/PageSnapshot.ts +14 -0
- package/src/lib/shared/types/PromptArgument.ts +12 -0
- package/src/lib/shared/types/RawRemoteFunction.ts +14 -0
- package/src/lib/shared/types/RemoteCallable.ts +12 -0
- package/src/lib/shared/types/RemoteFunction.ts +47 -0
- package/src/lib/shared/types/ReplayableMethod.ts +7 -0
- package/src/lib/shared/types/RequestScopeInfo.ts +16 -0
- package/src/lib/shared/types/RpcInvoker.ts +6 -0
- package/src/lib/shared/types/SocketChannel.ts +17 -0
- package/src/lib/shared/types/SocketSubCallbacks.ts +13 -0
- package/src/lib/shared/types/StandardSchemaV1.ts +56 -0
- package/src/lib/shared/types/StreamedResolution.ts +10 -0
- package/src/lib/shared/types/Subscribable.ts +26 -0
- package/src/lib/shared/types/TailHooks.ts +12 -0
- package/src/lib/shared/types/TailOptions.ts +10 -0
- package/src/lib/shared/types/TraceContext.ts +17 -0
- package/src/lib/shared/url.ts +118 -0
- package/src/lib/shared/withBase.ts +11 -0
- package/src/lib/shared/withBaseUrl.ts +17 -0
- package/src/lib/shared/withJsonSchema.ts +21 -0
- package/src/lib/shared/writeDts.ts +12 -0
- package/src/lib/shared/writeHealthDts.ts +36 -0
- package/src/lib/shared/writeLastConnection.ts +13 -0
- package/src/lib/shared/writePublicAssetsDts.ts +31 -0
- package/src/lib/shared/writeRoutesDts.ts +73 -0
- package/src/lib/shared/writeRpcDts.ts +49 -0
- package/src/lib/shared/writeTestRpcDts.ts +45 -0
- package/src/lib/shared/writeTestSocketsDts.ts +34 -0
- package/src/lib/test/assertAgentFrameConformance.ts +73 -0
- package/src/lib/test/createScriptedSurface.ts +45 -0
- package/src/lib/test/createTestApp.ts +203 -0
- package/src/lib/test/createTestSocketChannel.ts +142 -0
- package/src/lib/ui/README.md +86 -0
- package/src/lib/ui/compile/SSR_ESCAPE.ts +25 -0
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +36 -0
- package/src/lib/ui/compile/VOID_TAGS.ts +21 -0
- package/src/lib/ui/compile/abideUiPlugin.ts +65 -0
- package/src/lib/ui/compile/analyzeComponent.ts +117 -0
- package/src/lib/ui/compile/assetModulesFile.ts +32 -0
- package/src/lib/ui/compile/branchElements.ts +50 -0
- package/src/lib/ui/compile/collectAbideDiagnostics.ts +59 -0
- package/src/lib/ui/compile/compileComponent.ts +20 -0
- package/src/lib/ui/compile/compileModule.ts +116 -0
- package/src/lib/ui/compile/compileSSR.ts +36 -0
- package/src/lib/ui/compile/compileShadow.ts +352 -0
- package/src/lib/ui/compile/createShadowLanguageService.ts +197 -0
- package/src/lib/ui/compile/createShadowProgram.ts +96 -0
- package/src/lib/ui/compile/decodeHtmlEntities.ts +49 -0
- package/src/lib/ui/compile/desugarSignals.ts +133 -0
- package/src/lib/ui/compile/escapeHtml.ts +15 -0
- package/src/lib/ui/compile/generateBuild.ts +638 -0
- package/src/lib/ui/compile/generateSSR.ts +380 -0
- package/src/lib/ui/compile/groupBindParts.ts +28 -0
- package/src/lib/ui/compile/hoistCells.ts +120 -0
- package/src/lib/ui/compile/loadShadowTsConfig.ts +31 -0
- package/src/lib/ui/compile/lowerDocAccess.ts +202 -0
- package/src/lib/ui/compile/nearestProjectRoot.ts +16 -0
- package/src/lib/ui/compile/parseTemplate.ts +396 -0
- package/src/lib/ui/compile/partitionSlots.ts +36 -0
- package/src/lib/ui/compile/prepareNestedScript.ts +42 -0
- package/src/lib/ui/compile/remapShadowDiagnostic.ts +30 -0
- package/src/lib/ui/compile/renameSignalRefs.ts +85 -0
- package/src/lib/ui/compile/resolveAbideImports.ts +29 -0
- package/src/lib/ui/compile/scopeCss.ts +115 -0
- package/src/lib/ui/compile/shadowNaming.ts +11 -0
- package/src/lib/ui/compile/sourceToShadowOffset.ts +24 -0
- package/src/lib/ui/compile/staticAttrValue.ts +13 -0
- package/src/lib/ui/compile/stripEffects.ts +32 -0
- package/src/lib/ui/compile/types/AbideDiagnostic.ts +14 -0
- package/src/lib/ui/compile/types/AnalyzedComponent.ts +25 -0
- package/src/lib/ui/compile/types/CompiledShadow.ts +15 -0
- package/src/lib/ui/compile/types/TemplateAttr.ts +16 -0
- package/src/lib/ui/compile/types/TemplateNode.ts +78 -0
- package/src/lib/ui/compile/types/TextPart.ts +8 -0
- package/src/lib/ui/derived.ts +28 -0
- package/src/lib/ui/doc.ts +15 -0
- package/src/lib/ui/dom/appendSnippet.ts +34 -0
- package/src/lib/ui/dom/appendStatic.ts +27 -0
- package/src/lib/ui/dom/appendText.ts +114 -0
- package/src/lib/ui/dom/applyResolved.ts +72 -0
- package/src/lib/ui/dom/attach.ts +20 -0
- package/src/lib/ui/dom/attr.ts +19 -0
- package/src/lib/ui/dom/awaitBlock.ts +224 -0
- package/src/lib/ui/dom/cloneStatic.ts +52 -0
- package/src/lib/ui/dom/each.ts +115 -0
- package/src/lib/ui/dom/eachAsync.ts +153 -0
- package/src/lib/ui/dom/hydrate.ts +35 -0
- package/src/lib/ui/dom/mount.ts +29 -0
- package/src/lib/ui/dom/mountChild.ts +33 -0
- package/src/lib/ui/dom/on.ts +15 -0
- package/src/lib/ui/dom/openChild.ts +22 -0
- package/src/lib/ui/dom/openRoot.ts +20 -0
- package/src/lib/ui/dom/switchBlock.ts +75 -0
- package/src/lib/ui/dom/text.ts +20 -0
- package/src/lib/ui/dom/tryBlock.ts +112 -0
- package/src/lib/ui/dom/types/EachRow.ts +3 -0
- package/src/lib/ui/dom/types/SwitchCase.ts +6 -0
- package/src/lib/ui/dom/when.ts +73 -0
- package/src/lib/ui/effect.ts +16 -0
- package/src/lib/ui/installHotBridge.ts +73 -0
- package/src/lib/ui/matchRoute.ts +89 -0
- package/src/lib/ui/navigate.ts +17 -0
- package/src/lib/ui/probeNavigation.ts +33 -0
- package/src/lib/ui/remoteProxy.ts +97 -0
- package/src/lib/ui/renderChain.ts +50 -0
- package/src/lib/ui/renderToStream.ts +104 -0
- package/src/lib/ui/router.ts +286 -0
- package/src/lib/ui/runtime/OUTLET_TAG.ts +8 -0
- package/src/lib/ui/runtime/OWNER.ts +8 -0
- package/src/lib/ui/runtime/REACTIVE_CONTEXT.ts +14 -0
- package/src/lib/ui/runtime/RENDER.ts +23 -0
- package/src/lib/ui/runtime/RESUME.ts +16 -0
- package/src/lib/ui/runtime/applyPatchToTree.ts +41 -0
- package/src/lib/ui/runtime/claimChild.ts +10 -0
- package/src/lib/ui/runtime/clientPage.ts +16 -0
- package/src/lib/ui/runtime/createComputedNode.ts +16 -0
- package/src/lib/ui/runtime/createDoc.ts +177 -0
- package/src/lib/ui/runtime/createEffectNode.ts +58 -0
- package/src/lib/ui/runtime/createSignalNode.ts +16 -0
- package/src/lib/ui/runtime/detachLink.ts +21 -0
- package/src/lib/ui/runtime/endTracking.ts +24 -0
- package/src/lib/ui/runtime/enterRenderPass.ts +12 -0
- package/src/lib/ui/runtime/exitRenderPass.ts +7 -0
- package/src/lib/ui/runtime/firstOutlet.ts +22 -0
- package/src/lib/ui/runtime/flushEffects.ts +17 -0
- package/src/lib/ui/runtime/hotInstances.ts +10 -0
- package/src/lib/ui/runtime/hotReloadEnabled.ts +8 -0
- package/src/lib/ui/runtime/hotReplace.ts +25 -0
- package/src/lib/ui/runtime/nextBlockId.ts +11 -0
- package/src/lib/ui/runtime/pathExists.ts +23 -0
- package/src/lib/ui/runtime/readNode.ts +17 -0
- package/src/lib/ui/runtime/registerHotInstance.ts +23 -0
- package/src/lib/ui/runtime/runNode.ts +28 -0
- package/src/lib/ui/runtime/runtimePath.ts +9 -0
- package/src/lib/ui/runtime/scope.ts +24 -0
- package/src/lib/ui/runtime/toTeardown.ts +26 -0
- package/src/lib/ui/runtime/track.ts +58 -0
- package/src/lib/ui/runtime/trigger.ts +44 -0
- package/src/lib/ui/runtime/types/Cell.ts +5 -0
- package/src/lib/ui/runtime/types/Derived.ts +3 -0
- package/src/lib/ui/runtime/types/Doc.ts +19 -0
- package/src/lib/ui/runtime/types/EffectResult.ts +10 -0
- package/src/lib/ui/runtime/types/HotInstance.ts +14 -0
- package/src/lib/ui/runtime/types/NavVerdict.ts +9 -0
- package/src/lib/ui/runtime/types/Patch.ts +11 -0
- package/src/lib/ui/runtime/types/ReactiveLink.ts +21 -0
- package/src/lib/ui/runtime/types/ReactiveNode.ts +25 -0
- package/src/lib/ui/runtime/types/Route.ts +8 -0
- package/src/lib/ui/runtime/types/RouteLoader.ts +7 -0
- package/src/lib/ui/runtime/types/SsrRender.ts +22 -0
- package/src/lib/ui/runtime/types/State.ts +3 -0
- package/src/lib/ui/runtime/types/Teardown.ts +5 -0
- package/src/lib/ui/runtime/types/UiComponent.ts +16 -0
- package/src/lib/ui/runtime/types/UiProps.ts +15 -0
- package/src/lib/ui/runtime/unlinkDeps.ts +20 -0
- package/src/lib/ui/runtime/untrack.ts +20 -0
- package/src/lib/ui/runtime/valueAtPath.ts +18 -0
- package/src/lib/ui/runtime/writeNode.ts +16 -0
- package/src/lib/ui/socketChannel.ts +227 -0
- package/src/lib/ui/socketProxy.ts +25 -0
- package/src/lib/ui/startClient.ts +58 -0
- package/src/lib/ui/state.ts +25 -0
- package/src/lib/ui/tail.ts +324 -0
- package/src/lib/ui/types/Layouts.ts +9 -0
- package/src/lib/ui/types/Pages.ts +8 -0
- package/src/preload.ts +19 -0
- package/src/scaffold.ts +153 -0
- package/src/serverBuildPlugins.ts +19 -0
- package/src/serverEntry.ts +95 -0
- package/template/bunfig.toml +4 -0
- package/template/package.json +18 -0
- package/template/src/app.ts +28 -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 +36 -0
- package/template/src/ui/Layout.abide +19 -0
- package/template/src/ui/app.css +21 -0
- package/template/src/ui/app.html +24 -0
- package/template/src/ui/pages/about/page.abide +9 -0
- package/template/src/ui/pages/page.abide +22 -0
- package/template/test/app.test.ts +30 -0
- package/template/tsconfig.json +18 -0
- package/tsconfig.app.json +17 -0
|
@@ -0,0 +1,951 @@
|
|
|
1
|
+
import { abideLog } from './abideLog.ts'
|
|
2
|
+
import { activeCacheStore } from './activeCacheStore.ts'
|
|
3
|
+
import { CACHE_WRAPPED } from './CACHE_WRAPPED.ts'
|
|
4
|
+
import { cacheStores } from './cacheStores.ts'
|
|
5
|
+
import { decodeResponse } from './decodeResponse.ts'
|
|
6
|
+
import { getRemoteMeta } from './getRemoteMeta.ts'
|
|
7
|
+
import { globalCacheStore } from './globalCacheStore.ts'
|
|
8
|
+
import { HttpError } from './HttpError.ts'
|
|
9
|
+
import { invalidateEvent } from './invalidateEvent.ts'
|
|
10
|
+
import { invalidateTripwire } from './invalidateTripwire.ts'
|
|
11
|
+
import { keyForRemoteCall } from './keyForRemoteCall.ts'
|
|
12
|
+
import { producerKey } from './producerKey.ts'
|
|
13
|
+
import { REMOTE_FUNCTION } from './REMOTE_FUNCTION.ts'
|
|
14
|
+
import { REPLAYABLE_METHODS } from './REPLAYABLE_METHODS.ts'
|
|
15
|
+
import { SocketDisconnectedError } from './SocketDisconnectedError.ts'
|
|
16
|
+
import { selectorMatcher } from './selectorMatcher.ts'
|
|
17
|
+
import { selectorPrefix } from './selectorPrefix.ts'
|
|
18
|
+
import { toScopeSet } from './toScopeSet.ts'
|
|
19
|
+
import type { CacheEntry } from './types/CacheEntry.ts'
|
|
20
|
+
import type { CacheOnContext } from './types/CacheOnContext.ts'
|
|
21
|
+
import type { CacheOptions } from './types/CacheOptions.ts'
|
|
22
|
+
import type { CacheSelector } from './types/CacheSelector.ts'
|
|
23
|
+
import type { CacheStore } from './types/CacheStore.ts'
|
|
24
|
+
import type { RawRemoteFunction } from './types/RawRemoteFunction.ts'
|
|
25
|
+
import type { RemoteFunction } from './types/RemoteFunction.ts'
|
|
26
|
+
import type { Subscribable } from './types/Subscribable.ts'
|
|
27
|
+
|
|
28
|
+
type AnyRemote<Args, Return> = RemoteFunction<Args, Return> | RawRemoteFunction<Args>
|
|
29
|
+
type Producer<Args, Return> = (args?: Args) => Promise<Return>
|
|
30
|
+
|
|
31
|
+
/* Per-read lifecycle diagnostics, opt-in via DEBUG=abide:cache (browser: the abide-debug localStorage key). */
|
|
32
|
+
const cacheLog = abideLog.channel('abide:cache')
|
|
33
|
+
|
|
34
|
+
/*
|
|
35
|
+
Tallies one read and narrates it on the diagnostics channel. The sink is the
|
|
36
|
+
request/tab store even when the data store is the process-level global one —
|
|
37
|
+
attribution follows the asker, so a request's closing record reflects every
|
|
38
|
+
read it made. A settled retained entry (including the warm SSR sync path) is
|
|
39
|
+
a hit; an unsettled entry is a coalesced join of an in-flight call; no entry
|
|
40
|
+
is a miss that invokes the producer/remote.
|
|
41
|
+
*/
|
|
42
|
+
function recordRead(sink: CacheStore, key: string, existing: CacheEntry | undefined): void {
|
|
43
|
+
if (!existing) {
|
|
44
|
+
sink.stats.misses += 1
|
|
45
|
+
cacheLog(`miss ${key}`)
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
if (existing.settled === true) {
|
|
49
|
+
sink.stats.hits += 1
|
|
50
|
+
cacheLog(`hit ${key}`)
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
sink.stats.coalesced += 1
|
|
54
|
+
cacheLog(`coalesced ${key}`)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/*
|
|
58
|
+
Curries a call against a cache store. `cache(fn, options?)` returns an invoker;
|
|
59
|
+
calling that invoker with args checks the store for a prior entry and returns a
|
|
60
|
+
shared promise on hit, or invokes `fn` once and stores its promise on miss.
|
|
61
|
+
Splitting configuration (the outer call) from invocation (the inner call) keeps
|
|
62
|
+
options anchored in a fixed position so they can't collide with arg shapes. TTL
|
|
63
|
+
= undefined → forever; ttl = 0 → dedupe only; ttl > 0 → entry expires `ttl` ms
|
|
64
|
+
after the promise resolves.
|
|
65
|
+
|
|
66
|
+
Coalescing is always on: identical in-flight calls share one flight, so
|
|
67
|
+
`cache(createPost, { ttl: 0 })` is the mutation idiom — double-submit
|
|
68
|
+
coalescing and pending() visibility with nothing retained beyond the store's
|
|
69
|
+
atomic unit (the whole request on the server: one render, one effect; the
|
|
70
|
+
in-flight window in the tab). Caching is the retention `ttl` adds on top.
|
|
71
|
+
|
|
72
|
+
`fn` is either a remote function (a GET/POST/... helper) or a plain producer
|
|
73
|
+
returning a Promise:
|
|
74
|
+
|
|
75
|
+
cache(getPost)({ id }) // → Promise<Post> (decoded body)
|
|
76
|
+
cache(getPost.raw)({ id }) // → Promise<Response> (raw escape hatch)
|
|
77
|
+
cache(fetchRates)() // → Promise<Rates> (plain producer)
|
|
78
|
+
|
|
79
|
+
Remote calls key on fn.method + fn.url + args and store the underlying Response
|
|
80
|
+
(the decoded view is derived on the way out for the non-raw variant; both share
|
|
81
|
+
one entry). Producers have no wire identity, so they key on the producer's
|
|
82
|
+
reference + args — pass a hoisted/stable function to dedupe (an inline arrow is a
|
|
83
|
+
new reference every call and never does; a warning fires once per such call
|
|
84
|
+
site), and the promise is stored and handed back as-is (no Response, no decode,
|
|
85
|
+
no SSR snapshot).
|
|
86
|
+
|
|
87
|
+
`options.global` puts the entry in the process-level store instead of the
|
|
88
|
+
request-scoped one, so a value computed in one request is reused by later
|
|
89
|
+
requests — the memoise-an-external-endpoint case. Default (omitted) is
|
|
90
|
+
request-scoped on the server, which keeps per-user data from leaking across
|
|
91
|
+
requests; on the client there is one tab store either way, so it is a no-op.
|
|
92
|
+
|
|
93
|
+
Reactivity is implicit: the invoker calls `store.subscribe(key)`, which
|
|
94
|
+
registers the surrounding $derived / $effect scope. Invalidating the key
|
|
95
|
+
then re-runs that scope, which calls cache() again and gets a fresh entry.
|
|
96
|
+
Outside a tracking scope subscribe() is a no-op, so cache() works the same
|
|
97
|
+
in server code and plain client code.
|
|
98
|
+
|
|
99
|
+
SSR: how you consume the call decides inline vs streaming (during SSR only the
|
|
100
|
+
pending branch of a `<template await>` renders):
|
|
101
|
+
|
|
102
|
+
const post = await cache(getPost)({ id }) // blocks render → baked into
|
|
103
|
+
// the initial SSR HTML
|
|
104
|
+
<template await={cache(getPost)({ id })}> // renders pending → shell flushes
|
|
105
|
+
// now, value streams in on the
|
|
106
|
+
// same response when it resolves
|
|
107
|
+
|
|
108
|
+
The two don't mix within one component. A top-level `await` flips the async
|
|
109
|
+
render into await-everything mode and sweeps in every promise created
|
|
110
|
+
in that same component instance — so a sibling `<template await>` gets awaited
|
|
111
|
+
and inlined too, buffering the whole response to the slowest read. The markup
|
|
112
|
+
form doesn't change this: an await block renders its pending branch but render()
|
|
113
|
+
still blocks. To get both on one page,
|
|
114
|
+
isolate each blocking (top-level await) read in its own child component and
|
|
115
|
+
keep streaming reads in a parent that never top-level awaits — the
|
|
116
|
+
await-everything mode is per component instance, so a child's await blocks only
|
|
117
|
+
the child.
|
|
118
|
+
*/
|
|
119
|
+
// @readme cache
|
|
120
|
+
export function cache<Args, Return>(
|
|
121
|
+
fn: RemoteFunction<Args, Return>,
|
|
122
|
+
options?: CacheOptions,
|
|
123
|
+
): (args?: Args) => Promise<Return>
|
|
124
|
+
export function cache<Args>(
|
|
125
|
+
fn: RawRemoteFunction<Args>,
|
|
126
|
+
options?: CacheOptions,
|
|
127
|
+
): (args?: Args) => Promise<Response>
|
|
128
|
+
export function cache<Args, Return>(
|
|
129
|
+
fn: Producer<Args, Return>,
|
|
130
|
+
options?: CacheOptions,
|
|
131
|
+
): (args?: Args) => Promise<Return>
|
|
132
|
+
export function cache<Args, Return>(
|
|
133
|
+
fn: AnyRemote<Args, Return> | Producer<Args, Return>,
|
|
134
|
+
options?: CacheOptions,
|
|
135
|
+
): (args?: Args) => Promise<Return | Response> {
|
|
136
|
+
/*
|
|
137
|
+
Re-wrapping loses the remote's identity (no url/method on the wrapper), so
|
|
138
|
+
the inner remote would silently become an anonymous producer — no shared
|
|
139
|
+
key, no SSR snapshot, no write-method guards. Throw where the mistake is.
|
|
140
|
+
*/
|
|
141
|
+
if (CACHE_WRAPPED in fn) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
'[abide] cache(): fn is already a cache() wrapper — wrap the original function once',
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
/*
|
|
147
|
+
A remote function carries the REMOTE_FUNCTION brand (set by
|
|
148
|
+
createRemoteFunction on both variants); a plain producer never does — exact,
|
|
149
|
+
unlike a `url` property check a user function could satisfy by accident.
|
|
150
|
+
Among remotes, the "raw" variant lacks its own `.raw` sibling (only the
|
|
151
|
+
decoded callable carries one), which selects whether the decode step runs
|
|
152
|
+
on the way out.
|
|
153
|
+
*/
|
|
154
|
+
const isRemote = REMOTE_FUNCTION in fn
|
|
155
|
+
const isRaw = isRemote && !('raw' in fn)
|
|
156
|
+
const rawFn = !isRemote
|
|
157
|
+
? undefined
|
|
158
|
+
: isRaw
|
|
159
|
+
? (fn as RawRemoteFunction<Args>)
|
|
160
|
+
: (fn as RemoteFunction<Args, Return>).raw
|
|
161
|
+
validatePolicy(options, isRemote ? (rawFn as RawRemoteFunction<Args>).method : undefined)
|
|
162
|
+
if (!isRemote) {
|
|
163
|
+
warnAnonymousProducer(fn as Producer<Args, Return>)
|
|
164
|
+
}
|
|
165
|
+
const read = (args?: Args): Promise<Return | Response> => {
|
|
166
|
+
const store = options?.global ? globalCacheStore() : activeCacheStore()
|
|
167
|
+
if (!isRemote) {
|
|
168
|
+
return invokeProducer(store, fn as Producer<Args, Return>, args, options)
|
|
169
|
+
}
|
|
170
|
+
const remote = rawFn as RawRemoteFunction<Args>
|
|
171
|
+
const key = keyForRemoteCall(remote.method, remote.url, args)
|
|
172
|
+
store.subscribe(key)
|
|
173
|
+
const existing = store.entries.get(key)
|
|
174
|
+
recordRead(options?.global ? activeCacheStore() : store, key, existing)
|
|
175
|
+
if (existing) {
|
|
176
|
+
tagScope(existing, options?.scope)
|
|
177
|
+
attachPolicy(existing, options, () => remote(args as Args))
|
|
178
|
+
adoptTtl(store, existing, options)
|
|
179
|
+
}
|
|
180
|
+
/*
|
|
181
|
+
Warm path: a value pre-decoded onto the entry — by the SSR cache
|
|
182
|
+
snapshot the client seeds its store from, or by a cache.on().patch
|
|
183
|
+
broadcast — is served without a network round-trip. It resolves on a
|
|
184
|
+
microtask (a settled Promise), not synchronously, so every cache() read
|
|
185
|
+
is uniformly `Promise<Return>` and `.then`/`.catch`/`.finally` chain
|
|
186
|
+
cleanly. Raw callers take the Response path; after an invalidate the
|
|
187
|
+
replacement entry carries no value and falls through to a live fetch.
|
|
188
|
+
|
|
189
|
+
abide-ui hydration is seamless regardless: a `<template await>` adopts
|
|
190
|
+
the server-rendered DOM from the streamed resume manifest (it never calls
|
|
191
|
+
cache() on the first pass), so a microtask warm read costs no flash — the
|
|
192
|
+
snapshot's job is keeping post-hydration reads (reactivity, invalidation,
|
|
193
|
+
navigation) warm, not driving the initial paint.
|
|
194
|
+
|
|
195
|
+
Each warm read resolves to its own clone of the stored value: it is
|
|
196
|
+
decoded once and would otherwise be shared by reference across every
|
|
197
|
+
reader of the key, so one mutating it would corrupt the others. A live
|
|
198
|
+
fetch hands each reader a fresh object; cloning keeps warm reads the same.
|
|
199
|
+
*/
|
|
200
|
+
if (!isRaw && existing?.value !== undefined) {
|
|
201
|
+
return Promise.resolve(structuredClone(existing.value)) as Promise<Return>
|
|
202
|
+
}
|
|
203
|
+
const responsePromise = invokeRemote(
|
|
204
|
+
store,
|
|
205
|
+
key,
|
|
206
|
+
existing,
|
|
207
|
+
rawFn as RawRemoteFunction<Args>,
|
|
208
|
+
args,
|
|
209
|
+
options,
|
|
210
|
+
)
|
|
211
|
+
return isRaw ? responsePromise : (responsePromise.then(decodeResponse) as Promise<Return>)
|
|
212
|
+
}
|
|
213
|
+
/* Non-enumerable brand; selectorMatcher and the re-wrap guard read it. */
|
|
214
|
+
Object.defineProperty(read, CACHE_WRAPPED, { value: fn })
|
|
215
|
+
return read
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/*
|
|
219
|
+
Guards impossible option combinations at wrap time, where the call site is on
|
|
220
|
+
the stack. A policy declares "this call is safe to re-run unprompted", so a
|
|
221
|
+
non-replayable remote method (a write) must never carry one — replaying a write
|
|
222
|
+
through the invalidation grammar would be a state change disguised as a
|
|
223
|
+
refresh. Producers are opaque (no method to check); the same contract is on
|
|
224
|
+
the caller there. ttl: 0 retains nothing, so there is nothing for a policy to
|
|
225
|
+
revalidate; and the two coalescing strategies are exclusive by construction.
|
|
226
|
+
*/
|
|
227
|
+
function validatePolicy(options: CacheOptions | undefined, method: string | undefined): void {
|
|
228
|
+
const policy = options?.invalidate
|
|
229
|
+
if (!policy || (policy.throttle === undefined && policy.debounce === undefined)) {
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
if (policy.throttle !== undefined && policy.debounce !== undefined) {
|
|
233
|
+
throw new Error('[abide] cache(): set invalidate.throttle or invalidate.debounce, not both')
|
|
234
|
+
}
|
|
235
|
+
if (options?.ttl === 0) {
|
|
236
|
+
throw new Error(
|
|
237
|
+
'[abide] cache(): an invalidate policy requires retention — ttl: 0 keeps nothing to revalidate',
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
if (method !== undefined && !REPLAYABLE_METHODS.has(method.toUpperCase())) {
|
|
241
|
+
throw new Error(
|
|
242
|
+
`[abide] cache(): an invalidate policy re-runs the call unprompted — ${method.toUpperCase()} is a write and must not be replayed`,
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/*
|
|
248
|
+
An anonymous producer mints a fresh identity per wrap, so it never coalesces
|
|
249
|
+
and probes never match it — silently. Warn once per distinct source text (the
|
|
250
|
+
function object itself is fresh each time, so reference identity can't dedupe
|
|
251
|
+
the warning).
|
|
252
|
+
*/
|
|
253
|
+
const warnedAnonymousProducers = new Set<string>()
|
|
254
|
+
function warnAnonymousProducer(producer: (args?: never) => unknown): void {
|
|
255
|
+
if (producer.name !== '') {
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
const source = producer.toString()
|
|
259
|
+
if (warnedAnonymousProducers.has(source)) {
|
|
260
|
+
return
|
|
261
|
+
}
|
|
262
|
+
warnedAnonymousProducers.add(source)
|
|
263
|
+
abideLog.warn(
|
|
264
|
+
'cache() received an anonymous function — each call mints a fresh identity, so it never coalesces and pending()/refreshing() never match it. Hoist it to a named binding, or add a scope tag to probe it from elsewhere.',
|
|
265
|
+
)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/*
|
|
269
|
+
Producer path: key on the producer's reference + args, share the
|
|
270
|
+
in-flight/retained promise on hit, and store the value promise as-is on miss — no
|
|
271
|
+
Response, no decode, no SSR request metadata.
|
|
272
|
+
*/
|
|
273
|
+
function invokeProducer<Args, Return>(
|
|
274
|
+
store: CacheStore,
|
|
275
|
+
producer: Producer<Args, Return>,
|
|
276
|
+
args: Args | undefined,
|
|
277
|
+
options: CacheOptions | undefined,
|
|
278
|
+
): Promise<Return> {
|
|
279
|
+
const key = producerKey(producer, args)
|
|
280
|
+
store.subscribe(key)
|
|
281
|
+
const existing = store.entries.get(key)
|
|
282
|
+
recordRead(options?.global ? activeCacheStore() : store, key, existing)
|
|
283
|
+
if (existing) {
|
|
284
|
+
tagScope(existing, options?.scope)
|
|
285
|
+
attachPolicy(existing, options, () => producer(args))
|
|
286
|
+
const shared = existing.promise as Promise<Return>
|
|
287
|
+
/* A coalesced join waits on the in-flight producer — time the block so the
|
|
288
|
+
waterfall shows it; a settled hit returns immediately, so no span. */
|
|
289
|
+
return existing.settled === true
|
|
290
|
+
? shared
|
|
291
|
+
: cacheLog.trace<Return>(`cache wait ${key}`, () => shared)
|
|
292
|
+
}
|
|
293
|
+
/* Miss: time the producer run — where a request's time actually goes (an
|
|
294
|
+
external fetch, a computation). Producer path only; the remote path must
|
|
295
|
+
keep its own promise so getRemoteMeta can read the recorded Request. */
|
|
296
|
+
const promise = cacheLog.trace<Return>(`cache ${key}`, () => producer(args))
|
|
297
|
+
registerEntry(store, key, promise, options, undefined, () => producer(args))
|
|
298
|
+
return promise
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function invokeRemote<Args>(
|
|
302
|
+
store: CacheStore,
|
|
303
|
+
key: string,
|
|
304
|
+
existing: CacheEntry | undefined,
|
|
305
|
+
rawFn: RawRemoteFunction<Args>,
|
|
306
|
+
args: Args | undefined,
|
|
307
|
+
options: CacheOptions | undefined,
|
|
308
|
+
): Promise<Response> {
|
|
309
|
+
if (existing) {
|
|
310
|
+
return shareable(existing.promise as Promise<Response>)
|
|
311
|
+
}
|
|
312
|
+
const promise = rawFn(args as Args)
|
|
313
|
+
const request = getRemoteMeta(promise)
|
|
314
|
+
if (!request) {
|
|
315
|
+
throw new Error(
|
|
316
|
+
'[abide] cache() received a function whose call did not record metadata — was it produced by a verb helper?',
|
|
317
|
+
)
|
|
318
|
+
}
|
|
319
|
+
registerEntry(store, key, promise, options, request, () => rawFn(args as Args))
|
|
320
|
+
return shareable(promise)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/*
|
|
324
|
+
Stores a fresh entry and wires its settle / ttl / eviction lifecycle. Shared by
|
|
325
|
+
the remote and producer paths; `request` is set for remote entries (drives the
|
|
326
|
+
SSR snapshot) and undefined for producers.
|
|
327
|
+
*/
|
|
328
|
+
function registerEntry(
|
|
329
|
+
store: CacheStore,
|
|
330
|
+
key: string,
|
|
331
|
+
promise: Promise<unknown>,
|
|
332
|
+
options: CacheOptions | undefined,
|
|
333
|
+
request: Request | undefined,
|
|
334
|
+
refetch: () => Promise<unknown>,
|
|
335
|
+
): CacheEntry {
|
|
336
|
+
const ttl = options?.ttl
|
|
337
|
+
/* Capture the refetch thunk + policy only when an invalidate window was asked for. */
|
|
338
|
+
const policy = options?.invalidate
|
|
339
|
+
const invalidation =
|
|
340
|
+
policy?.throttle !== undefined || policy?.debounce !== undefined
|
|
341
|
+
? { refetch, throttle: policy.throttle, debounce: policy.debounce }
|
|
342
|
+
: undefined
|
|
343
|
+
/*
|
|
344
|
+
A prior entry for this key was dropped by invalidate() and is awaiting its
|
|
345
|
+
next read — consume the marker so this replacement read reports as a reload
|
|
346
|
+
(refreshing()) until it settles, not as a first-ever load.
|
|
347
|
+
*/
|
|
348
|
+
const refreshing = store.pendingRefresh.delete(key) || undefined
|
|
349
|
+
const entry: CacheEntry = {
|
|
350
|
+
key,
|
|
351
|
+
promise,
|
|
352
|
+
request,
|
|
353
|
+
ttl,
|
|
354
|
+
expiresAt: undefined,
|
|
355
|
+
scope: options?.scope === undefined ? undefined : toScopeSet(options.scope),
|
|
356
|
+
refreshing,
|
|
357
|
+
invalidation,
|
|
358
|
+
}
|
|
359
|
+
store.entries.set(key, entry)
|
|
360
|
+
store.markLifecycle(key)
|
|
361
|
+
/*
|
|
362
|
+
A ttl=0 remote entry in the request-scoped server store is kept until the
|
|
363
|
+
store dies with the response. The request is the server's atomic unit, so
|
|
364
|
+
a ttl=0 entry retains nothing beyond it but coalesces everything within
|
|
365
|
+
it: identical calls during one render — any method — share one effect
|
|
366
|
+
deterministically, regardless of settle timing, and the post-render SSR
|
|
367
|
+
snapshot can still pick up replayable entries (the snapshot applies its
|
|
368
|
+
own method filter; writes never ship). The keep never applies on the
|
|
369
|
+
client (the tab store outlives any unit — a kept write would block every
|
|
370
|
+
future re-submit, so entries evict the moment they settle), to producer
|
|
371
|
+
entries (no request), or to the process-level `global` store (not
|
|
372
|
+
request-scoped — keeping it would leak forever).
|
|
373
|
+
*/
|
|
374
|
+
const keepZeroTtlForRequest =
|
|
375
|
+
request !== undefined && !options?.global && typeof window === 'undefined'
|
|
376
|
+
function deleteIfCurrent() {
|
|
377
|
+
evictIfCurrent(store, entry)
|
|
378
|
+
}
|
|
379
|
+
promise.then((result) => {
|
|
380
|
+
/*
|
|
381
|
+
Mark settled so SSR snapshot serialization can tell awaited entries
|
|
382
|
+
(resolved by the time render() returns → inline) from {#await} ones
|
|
383
|
+
(still pending → stream). Set before the ttl branches below since a
|
|
384
|
+
ttl=0 server entry stays in the store for the snapshot.
|
|
385
|
+
*/
|
|
386
|
+
entry.settled = true
|
|
387
|
+
/* The reload finished — this entry now holds fresh data, no longer refreshing. */
|
|
388
|
+
entry.refreshing = false
|
|
389
|
+
store.markLifecycle(key)
|
|
390
|
+
/*
|
|
391
|
+
An error-status Response is a failed load, not a value to retain: fetch
|
|
392
|
+
resolves (it only rejects on a network fault) on a 4xx/5xx, so without
|
|
393
|
+
this the entry would be served as a hit for the whole ttl, even after the
|
|
394
|
+
backend recovers. Evict it so the next read retries — mirroring fireRefetch,
|
|
395
|
+
which already guards revalidation results with the same `!result.ok` check.
|
|
396
|
+
*/
|
|
397
|
+
if (result instanceof Response && !result.ok) {
|
|
398
|
+
deleteIfCurrent()
|
|
399
|
+
return
|
|
400
|
+
}
|
|
401
|
+
if (ttl === 0) {
|
|
402
|
+
if (!keepZeroTtlForRequest) {
|
|
403
|
+
deleteIfCurrent()
|
|
404
|
+
}
|
|
405
|
+
return
|
|
406
|
+
}
|
|
407
|
+
if (ttl !== undefined) {
|
|
408
|
+
armTtlExpiry(store, entry, ttl)
|
|
409
|
+
}
|
|
410
|
+
}, deleteIfCurrent)
|
|
411
|
+
return entry
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/*
|
|
415
|
+
Evicts `entry` unless a newer entry already owns the key (a concurrent
|
|
416
|
+
invalidate-and-reread must not lose its replacement). Disarms any policy timer
|
|
417
|
+
first — an armed timer would otherwise refetch a key that no longer exists.
|
|
418
|
+
*/
|
|
419
|
+
function evictIfCurrent(store: CacheStore, entry: CacheEntry): void {
|
|
420
|
+
if (store.entries.get(entry.key) === entry) {
|
|
421
|
+
clearTimeout(entry.invalidation?.timer)
|
|
422
|
+
store.entries.delete(entry.key)
|
|
423
|
+
store.markLifecycle(entry.key)
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/* Arms the ttl > 0 expiry sweep; `expiresAt` re-checks at fire time so a refreshed deadline survives. */
|
|
428
|
+
function armTtlExpiry(store: CacheStore, entry: CacheEntry, ttl: number): void {
|
|
429
|
+
entry.expiresAt = Date.now() + ttl
|
|
430
|
+
setTimeout(() => {
|
|
431
|
+
if ((entry.expiresAt ?? 0) <= Date.now()) {
|
|
432
|
+
evictIfCurrent(store, entry)
|
|
433
|
+
}
|
|
434
|
+
}, ttl).unref?.()
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/*
|
|
438
|
+
Mirrors tagScope/attachPolicy for retention: a hydrated snapshot entry ships
|
|
439
|
+
without its wrap options (they live at call sites, not on the wire), so the
|
|
440
|
+
first read adopts its call site's ttl declaration. Omitted = forever, exactly
|
|
441
|
+
as shipped; ttl > 0 = the expiry clock starts at this read; ttl = 0 = the warm
|
|
442
|
+
value exists only to complete the hydration render — the SSR request's atomic
|
|
443
|
+
unit ends here — so eviction is deferred one macrotask (every reader in the
|
|
444
|
+
same hydration pass still gets the warm value, no invalidate event fires, and
|
|
445
|
+
the already-painted DOM stays put) and the next read fetches live. The first
|
|
446
|
+
reader consumes the flag, so its declaration wins; live entries never carry
|
|
447
|
+
the flag and keep the ttl they registered with.
|
|
448
|
+
*/
|
|
449
|
+
function adoptTtl(store: CacheStore, entry: CacheEntry, options: CacheOptions | undefined): void {
|
|
450
|
+
if (entry.hydrated !== true) {
|
|
451
|
+
return
|
|
452
|
+
}
|
|
453
|
+
entry.hydrated = false
|
|
454
|
+
const ttl = options?.ttl
|
|
455
|
+
if (ttl === undefined) {
|
|
456
|
+
return
|
|
457
|
+
}
|
|
458
|
+
entry.ttl = ttl
|
|
459
|
+
if (ttl === 0) {
|
|
460
|
+
setTimeout(() => evictIfCurrent(store, entry), 0).unref?.()
|
|
461
|
+
return
|
|
462
|
+
}
|
|
463
|
+
armTtlExpiry(store, entry, ttl)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/*
|
|
467
|
+
Returns a promise that resolves to a fresh clone of the underlying Response.
|
|
468
|
+
Multiple readers can each consume the body independently — the stored
|
|
469
|
+
promise's Response is never consumed directly, so clones always succeed.
|
|
470
|
+
*/
|
|
471
|
+
function shareable(promise: Promise<Response>): Promise<Response> {
|
|
472
|
+
return promise.then((response) => response.clone())
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/*
|
|
476
|
+
Invalidates every entry matching the selector (see selectorMatcher) across both
|
|
477
|
+
the request/tab store and the process-level store, and notifies readers.
|
|
478
|
+
`args` narrows a fn selector to exactly that call's entry — derived through
|
|
479
|
+
the same encoders the read path uses, so other args variants stay warm. An entry
|
|
480
|
+
with an invalidate throttle/debounce policy is kept and its refetch coalesced (stale served
|
|
481
|
+
until it resolves); every other match is dropped so the next read refetches —
|
|
482
|
+
its key recorded in pendingRefresh so that read reports as a reload (refreshing())
|
|
483
|
+
rather than a first-ever load. An empty or unmatched selector is a no-op on the
|
|
484
|
+
cache; the lifecycle ping still fires but recomputes pending() to the same value.
|
|
485
|
+
*/
|
|
486
|
+
function invalidate<Args, Return>(arg?: CacheSelector<Args, Return>, args?: Args): void {
|
|
487
|
+
/* Resolve the fn-selector prefix once; the matcher and the label both consume it. */
|
|
488
|
+
const prefix = selectorPrefix(arg, args)
|
|
489
|
+
const matches = selectorMatcher(arg, args, prefix)
|
|
490
|
+
invalidateTripwire(selectorLabel(arg, args, prefix))
|
|
491
|
+
for (const store of cacheStores()) {
|
|
492
|
+
const affected: string[] = []
|
|
493
|
+
/* Deleting the current entry mid-iteration is spec-safe on a Map; no snapshot needed. */
|
|
494
|
+
for (const entry of store.entries.values()) {
|
|
495
|
+
if (!matches(entry)) {
|
|
496
|
+
continue
|
|
497
|
+
}
|
|
498
|
+
if (entry.invalidation) {
|
|
499
|
+
scheduleInvalidationRefetch(store, entry)
|
|
500
|
+
} else {
|
|
501
|
+
store.entries.delete(entry.key)
|
|
502
|
+
/* Mark so the next read of this key reports as a reload via refreshing(). */
|
|
503
|
+
store.pendingRefresh.add(entry.key)
|
|
504
|
+
affected.push(entry.key)
|
|
505
|
+
}
|
|
506
|
+
store.markLifecycle(entry.key)
|
|
507
|
+
}
|
|
508
|
+
emit(store, affected)
|
|
509
|
+
store.markLifecycle()
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/*
|
|
514
|
+
Human-readable selector identity for the tripwire and cache.on coverage: the
|
|
515
|
+
key prefix for fn selectors — the exact key when args narrow it — falling
|
|
516
|
+
back to the function's name for a producer never cached, the tag list for
|
|
517
|
+
scopes, `*` for the bare form.
|
|
518
|
+
*/
|
|
519
|
+
function selectorLabel<Args, Return>(
|
|
520
|
+
arg?: CacheSelector<Args, Return>,
|
|
521
|
+
args?: Args,
|
|
522
|
+
prefix?: string,
|
|
523
|
+
): string {
|
|
524
|
+
if (arg === undefined) {
|
|
525
|
+
return '*'
|
|
526
|
+
}
|
|
527
|
+
if (typeof arg === 'function') {
|
|
528
|
+
return prefix ?? selectorPrefix(arg, args) ?? (arg.name || 'anonymous producer')
|
|
529
|
+
}
|
|
530
|
+
return `scope: ${[...toScopeSet(arg.scope ?? [])].join(', ')}`
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
cache.invalidate = invalidate
|
|
534
|
+
|
|
535
|
+
type EntryWrite = { store: CacheStore; entry: CacheEntry; prior: unknown; next: unknown }
|
|
536
|
+
|
|
537
|
+
/*
|
|
538
|
+
Core value-fold shared by the authoritative (cache.on context.patch) and
|
|
539
|
+
optimistic (cache.patch) write paths. Folds `updater` into every decoded remote
|
|
540
|
+
entry matching the selector, writing the result to entry.value so the warm-sync
|
|
541
|
+
read path serves it and emitting the keys so readers re-run (ADR-0007). Only
|
|
542
|
+
entry.value is written; entry.promise is left untouched so raw readers of the
|
|
543
|
+
same key keep reading the wire Response. Producer entries (no request) are
|
|
544
|
+
skipped — patching is a decoded-value operation. The current value is entry.value
|
|
545
|
+
when warm (hydrated or already patched), else the entry's settled Response decoded
|
|
546
|
+
— hence async; the first fold of a live-fetched entry hops a decode, subsequent
|
|
547
|
+
ones are synchronous. Returns the keys touched (the cache.on context registers
|
|
548
|
+
them for reconnect resync) and a `restore` that reverts each write iff it still
|
|
549
|
+
stands — the optimistic path runs it on a rejected call, the authoritative path
|
|
550
|
+
discards it (a broadcast is truth, never undone).
|
|
551
|
+
*/
|
|
552
|
+
async function foldEntries<Args, Return>(
|
|
553
|
+
arg: CacheSelector<Args, Return>,
|
|
554
|
+
updater: (current: Return) => Return,
|
|
555
|
+
args: Args | undefined,
|
|
556
|
+
prefix: string | undefined,
|
|
557
|
+
): Promise<{ touched: string[]; restore: () => void }> {
|
|
558
|
+
const matches = selectorMatcher(arg, args, prefix ?? selectorPrefix(arg, args))
|
|
559
|
+
const touched: string[] = []
|
|
560
|
+
const writes: EntryWrite[] = []
|
|
561
|
+
for (const store of cacheStores()) {
|
|
562
|
+
const affected: string[] = []
|
|
563
|
+
for (const entry of store.entries.values()) {
|
|
564
|
+
if (!matches(entry) || entry.request === undefined) {
|
|
565
|
+
continue
|
|
566
|
+
}
|
|
567
|
+
const prior = entry.value
|
|
568
|
+
const current = (prior ??
|
|
569
|
+
(await decodeResponse(
|
|
570
|
+
await shareable(entry.promise as Promise<Response>),
|
|
571
|
+
))) as Return
|
|
572
|
+
const next = structuredClone(updater(current))
|
|
573
|
+
entry.value = next
|
|
574
|
+
entry.settled = true
|
|
575
|
+
entry.refreshing = false
|
|
576
|
+
store.markLifecycle(entry.key)
|
|
577
|
+
affected.push(entry.key)
|
|
578
|
+
writes.push({ store, entry, prior, next })
|
|
579
|
+
}
|
|
580
|
+
emit(store, affected)
|
|
581
|
+
store.markLifecycle()
|
|
582
|
+
touched.push(...affected)
|
|
583
|
+
}
|
|
584
|
+
return { touched, restore: () => revertWrites(writes) }
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/*
|
|
588
|
+
Reverts each optimistic write iff entry.value still holds it — a refetch or a
|
|
589
|
+
later write that already replaced it is the newer truth and is left intact — then
|
|
590
|
+
notifies readers of the reverted keys per store.
|
|
591
|
+
*/
|
|
592
|
+
function revertWrites(writes: EntryWrite[]): void {
|
|
593
|
+
const reverted = new Map<CacheStore, string[]>()
|
|
594
|
+
for (const { store, entry, prior, next } of writes) {
|
|
595
|
+
if (store.entries.get(entry.key) !== entry || entry.value !== next) {
|
|
596
|
+
continue
|
|
597
|
+
}
|
|
598
|
+
entry.value = prior
|
|
599
|
+
store.markLifecycle(entry.key)
|
|
600
|
+
reverted.set(store, [...(reverted.get(store) ?? []), entry.key])
|
|
601
|
+
}
|
|
602
|
+
for (const [store, keys] of reverted) {
|
|
603
|
+
emit(store, keys)
|
|
604
|
+
store.markLifecycle()
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/*
|
|
609
|
+
The authoritative-broadcast fold: a cache.on frame is the truth, so foldEntries'
|
|
610
|
+
write stands (no rollback) and only the touched keys are returned for coverage.
|
|
611
|
+
*/
|
|
612
|
+
async function patchEntries<Args, Return>(
|
|
613
|
+
arg: CacheSelector<Args, Return>,
|
|
614
|
+
updater: (current: Return) => Return,
|
|
615
|
+
args?: Args,
|
|
616
|
+
/* The caller (on().patch) resolves the prefix for its coverage label; reuse it so selectorPrefix runs once per fold. */
|
|
617
|
+
prefix?: string,
|
|
618
|
+
): Promise<string[]> {
|
|
619
|
+
const { touched } = await foldEntries(arg, updater, args, prefix)
|
|
620
|
+
return touched
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/*
|
|
624
|
+
Event-driven cache maintenance: subscribes to a Subscribable (socket or rpc
|
|
625
|
+
stream) and runs `handler` once per frame — the declarative home for "this
|
|
626
|
+
socket event stales that cached data", replacing the hand-rolled $effect +
|
|
627
|
+
tail() + edge-detection pattern. Bare iteration means live frames only
|
|
628
|
+
(ADR-0004): no replay seed, a frame is an event, nothing is retained.
|
|
629
|
+
|
|
630
|
+
Delivery is sequential: frame N+1 is not pulled until N's handler (sync or
|
|
631
|
+
async) settles, so ordering holds and before/after work sits naturally
|
|
632
|
+
between frames; a slow handler queues frames rather than racing itself.
|
|
633
|
+
The context's `invalidate` is this binding's scoped copy — same grammar and
|
|
634
|
+
effect as cache.invalidate, but each call is recorded in the binding's
|
|
635
|
+
coverage set (attribution by function identity, so it survives awaits).
|
|
636
|
+
|
|
637
|
+
On a transport loss (the typed SocketDisconnectedError) frames may have been
|
|
638
|
+
missed, and a missed frame is a missed invalidation — silently stale data. The
|
|
639
|
+
binding can't know what it missed, so it conservatively re-invalidates its
|
|
640
|
+
whole coverage set, then reopens the source (the channel's backoff owns retry
|
|
641
|
+
timing); over-invalidating costs a refetch, never correctness. A handler
|
|
642
|
+
throw is logged and the binding lives on — one bad frame must not detach
|
|
643
|
+
freshness; a server error frame or clean end is terminal, mirroring tail.
|
|
644
|
+
|
|
645
|
+
No-op on the server (inert dispose): SSR can't hold a stream across the
|
|
646
|
+
request boundary — bindings attach client-side, where the snapshot has
|
|
647
|
+
already seeded the cache. Dispose aborts `signal`, stops delivery, and
|
|
648
|
+
closes the subscription.
|
|
649
|
+
*/
|
|
650
|
+
function on<T>(
|
|
651
|
+
source: Subscribable<T>,
|
|
652
|
+
handler: (frame: T, context: CacheOnContext) => void | Promise<void>,
|
|
653
|
+
): () => void {
|
|
654
|
+
if (typeof window === 'undefined') {
|
|
655
|
+
return () => undefined
|
|
656
|
+
}
|
|
657
|
+
const controller = new AbortController()
|
|
658
|
+
/* Coverage replays on reconnect; keyed by selector identity so repeats dedupe. */
|
|
659
|
+
const coverage = new Map<string, () => void>()
|
|
660
|
+
const context: CacheOnContext = {
|
|
661
|
+
invalidate<Args, Return>(arg?: CacheSelector<Args, Return>, args?: Args): void {
|
|
662
|
+
coverage.set(selectorLabel(arg, args), () => invalidate(arg, args))
|
|
663
|
+
invalidate(arg, args)
|
|
664
|
+
},
|
|
665
|
+
/*
|
|
666
|
+
Register the selector (not the delta) for reconnect: a discarded delta
|
|
667
|
+
can't be replayed, so a transport gap resyncs the patched keys by full
|
|
668
|
+
invalidate — reusing the same coverage machinery as invalidate above.
|
|
669
|
+
*/
|
|
670
|
+
patch<Args, Return>(
|
|
671
|
+
arg: CacheSelector<Args, Return>,
|
|
672
|
+
updater: (current: Return) => Return,
|
|
673
|
+
args?: Args,
|
|
674
|
+
): Promise<string[]> {
|
|
675
|
+
/* Resolve the prefix once; the coverage label and patchEntries both consume it. */
|
|
676
|
+
const prefix = selectorPrefix(arg, args)
|
|
677
|
+
coverage.set(selectorLabel(arg, args, prefix), () => invalidate(arg, args))
|
|
678
|
+
return patchEntries(arg, updater, args, prefix)
|
|
679
|
+
},
|
|
680
|
+
signal: controller.signal,
|
|
681
|
+
}
|
|
682
|
+
/* `let`: the reconnect path swaps in a fresh iterator; dispose closes the current one. */
|
|
683
|
+
let iterator = source[Symbol.asyncIterator]()
|
|
684
|
+
;(async () => {
|
|
685
|
+
while (!controller.signal.aborted) {
|
|
686
|
+
let next: IteratorResult<T>
|
|
687
|
+
try {
|
|
688
|
+
next = await iterator.next()
|
|
689
|
+
} catch (error) {
|
|
690
|
+
if (controller.signal.aborted) {
|
|
691
|
+
return
|
|
692
|
+
}
|
|
693
|
+
if (error instanceof SocketDisconnectedError) {
|
|
694
|
+
coverage.forEach((replay) => {
|
|
695
|
+
replay()
|
|
696
|
+
})
|
|
697
|
+
iterator = source[Symbol.asyncIterator]()
|
|
698
|
+
continue
|
|
699
|
+
}
|
|
700
|
+
abideLog.error(error)
|
|
701
|
+
return
|
|
702
|
+
}
|
|
703
|
+
if (controller.signal.aborted || next.done === true) {
|
|
704
|
+
return
|
|
705
|
+
}
|
|
706
|
+
try {
|
|
707
|
+
await handler(next.value, context)
|
|
708
|
+
} catch (error) {
|
|
709
|
+
abideLog.error(error)
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
})()
|
|
713
|
+
return () => {
|
|
714
|
+
controller.abort()
|
|
715
|
+
iterator.return?.(undefined)?.catch(() => undefined)
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
cache.on = on
|
|
720
|
+
|
|
721
|
+
/*
|
|
722
|
+
Optimistic write: applies `updater` as a prediction now — the reactive read
|
|
723
|
+
shows it immediately — runs `call`, then reconciles. On resolve the server is
|
|
724
|
+
the truth: the prediction is dropped and the selector invalidated, so the value
|
|
725
|
+
refetches authoritatively, coalesced per the read's own invalidate policy
|
|
726
|
+
(cache(fn, { invalidate: { throttle } }) bounds an optimistic-write storm with no
|
|
727
|
+
extra knob here; without a policy it is a plain drop-and-refetch). On reject the
|
|
728
|
+
prediction rolls back. The returned promise is transparent over `call` —
|
|
729
|
+
resolves to `call`'s value (the mutation result, e.g. a created id), rejects
|
|
730
|
+
with its error, settling only after the cache reflects the reconciled state: an
|
|
731
|
+
explicit await reads truth, an ignored call is fire-and-forget (a pre-attached
|
|
732
|
+
catch keeps an un-awaited rejection from surfacing as unhandled, while the await
|
|
733
|
+
still receives it). `call` is required — a global authoritative write with no
|
|
734
|
+
reconciling op would let a caller author cache values, breaking the
|
|
735
|
+
producer-is-the-source invariant (ADR-0001); that path stays cache.on's
|
|
736
|
+
context.patch. Single-flight per key: rollback restores by snapshot, so keep one
|
|
737
|
+
mutation per key in flight (disable the trigger while pending) — concurrent
|
|
738
|
+
same-key optimism wants a layered entry value, deferred (ADR-0009). The reactive
|
|
739
|
+
read holds the value; the return carries the mutation result — separate by
|
|
740
|
+
design (ADR-0009).
|
|
741
|
+
*/
|
|
742
|
+
function patch<Args, Return, Result>(
|
|
743
|
+
arg: CacheSelector<Args, Return>,
|
|
744
|
+
updater: (current: Return) => Return,
|
|
745
|
+
call: Promise<Result>,
|
|
746
|
+
args?: Args,
|
|
747
|
+
): Promise<Result> {
|
|
748
|
+
const prefix = selectorPrefix(arg, args)
|
|
749
|
+
const folded = foldEntries(arg, updater, args, prefix)
|
|
750
|
+
const settled = (async () => {
|
|
751
|
+
try {
|
|
752
|
+
const result = await call
|
|
753
|
+
/* Wait for the prediction to land before reconciling to server truth. */
|
|
754
|
+
await folded
|
|
755
|
+
invalidate(arg, args)
|
|
756
|
+
return result
|
|
757
|
+
} catch (error) {
|
|
758
|
+
const { restore } = await folded
|
|
759
|
+
restore()
|
|
760
|
+
throw error
|
|
761
|
+
}
|
|
762
|
+
})()
|
|
763
|
+
/* Ignore-safe: an un-awaited rejection must not report unhandled; an explicit
|
|
764
|
+
await still receives it (this no-op handler and the await both fire). */
|
|
765
|
+
settled.catch(() => undefined)
|
|
766
|
+
return settled
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
cache.patch = patch
|
|
770
|
+
|
|
771
|
+
/*
|
|
772
|
+
Schedules a coalesced refetch per the entry's invalidate policy. debounce: (re)arm
|
|
773
|
+
a timer that fires after N ms of quiet. throttle: fire on the leading edge when a
|
|
774
|
+
full window has elapsed since the last fire, else arm a single trailing timer for
|
|
775
|
+
the remainder — so a continuous invalidation stream refetches at most once per window.
|
|
776
|
+
*/
|
|
777
|
+
function scheduleInvalidationRefetch(store: CacheStore, entry: CacheEntry): void {
|
|
778
|
+
const policy = entry.invalidation
|
|
779
|
+
if (!policy) {
|
|
780
|
+
return
|
|
781
|
+
}
|
|
782
|
+
if (policy.debounce !== undefined) {
|
|
783
|
+
clearTimeout(policy.timer)
|
|
784
|
+
policy.timer = armTimer(store, entry, policy.debounce)
|
|
785
|
+
return
|
|
786
|
+
}
|
|
787
|
+
const throttleMs = policy.throttle ?? 0
|
|
788
|
+
const elapsed = Date.now() - (policy.lastFiredAt ?? Number.NEGATIVE_INFINITY)
|
|
789
|
+
if (elapsed >= throttleMs) {
|
|
790
|
+
fireRefetch(store, entry)
|
|
791
|
+
return
|
|
792
|
+
}
|
|
793
|
+
if (policy.timer === undefined) {
|
|
794
|
+
policy.timer = armTimer(store, entry, throttleMs - elapsed)
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function armTimer(store: CacheStore, entry: CacheEntry, ms: number): ReturnType<typeof setTimeout> {
|
|
799
|
+
const timer = setTimeout(() => {
|
|
800
|
+
if (entry.invalidation) {
|
|
801
|
+
entry.invalidation.timer = undefined
|
|
802
|
+
}
|
|
803
|
+
fireRefetch(store, entry)
|
|
804
|
+
}, ms)
|
|
805
|
+
timer.unref?.()
|
|
806
|
+
return timer
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/*
|
|
810
|
+
Runs the captured refetch once, keeping the stale value visible until it
|
|
811
|
+
resolves, then swaps the fresh result in and notifies readers. A refetch already
|
|
812
|
+
in flight is left to finish — the key is stable, so it already fetches the latest
|
|
813
|
+
state. Failure arrives on either settle path: a remote refetch resolves with the
|
|
814
|
+
Response even on an error status (fetch rejects only on network loss), a producer
|
|
815
|
+
rejects. Both route to settleRefetchFailure — stale kept, except a 404 evicts.
|
|
816
|
+
*/
|
|
817
|
+
function fireRefetch(store: CacheStore, entry: CacheEntry): void {
|
|
818
|
+
const policy = entry.invalidation
|
|
819
|
+
if (!policy) {
|
|
820
|
+
return
|
|
821
|
+
}
|
|
822
|
+
/* A refetch is already running: record the request so it re-fires on settle
|
|
823
|
+
(the in-flight one may predate this newer invalidation) instead of dropping it. */
|
|
824
|
+
if (entry.refreshing) {
|
|
825
|
+
policy.pending = true
|
|
826
|
+
return
|
|
827
|
+
}
|
|
828
|
+
entry.refreshing = true
|
|
829
|
+
policy.lastFiredAt = Date.now()
|
|
830
|
+
/* Ping lifecycle so refreshing() re-derives when revalidation begins; the settle handlers ping again when it ends. */
|
|
831
|
+
store.markLifecycle(entry.key)
|
|
832
|
+
const inflight = policy.refetch()
|
|
833
|
+
inflight.then(
|
|
834
|
+
(result) => {
|
|
835
|
+
entry.refreshing = false
|
|
836
|
+
reschedulePendingRefetch(store, entry, policy)
|
|
837
|
+
/* Dropped or replaced while in flight — discard this result. */
|
|
838
|
+
if (store.entries.get(entry.key) !== entry) {
|
|
839
|
+
return
|
|
840
|
+
}
|
|
841
|
+
if (result instanceof Response && !result.ok) {
|
|
842
|
+
settleRefetchFailure(store, entry, result.status)
|
|
843
|
+
return
|
|
844
|
+
}
|
|
845
|
+
entry.promise = inflight
|
|
846
|
+
entry.value = undefined
|
|
847
|
+
entry.settled = true
|
|
848
|
+
store.markLifecycle(entry.key)
|
|
849
|
+
emit(store, [entry.key])
|
|
850
|
+
},
|
|
851
|
+
(error) => {
|
|
852
|
+
entry.refreshing = false
|
|
853
|
+
reschedulePendingRefetch(store, entry, policy)
|
|
854
|
+
if (store.entries.get(entry.key) !== entry) {
|
|
855
|
+
return
|
|
856
|
+
}
|
|
857
|
+
settleRefetchFailure(
|
|
858
|
+
store,
|
|
859
|
+
entry,
|
|
860
|
+
error instanceof HttpError ? error.status : undefined,
|
|
861
|
+
)
|
|
862
|
+
},
|
|
863
|
+
)
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/*
|
|
867
|
+
Re-schedules a refetch requested while one was already in flight (fireRefetch
|
|
868
|
+
recorded it on policy.pending). Runs after the in-flight refetch settles so the
|
|
869
|
+
newer invalidation isn't lost; honours the throttle/debounce window since
|
|
870
|
+
lastFiredAt was just stamped. No-op if the entry was dropped or replaced.
|
|
871
|
+
*/
|
|
872
|
+
function reschedulePendingRefetch(
|
|
873
|
+
store: CacheStore,
|
|
874
|
+
entry: CacheEntry,
|
|
875
|
+
policy: NonNullable<CacheEntry['invalidation']>,
|
|
876
|
+
): void {
|
|
877
|
+
if (!policy.pending) {
|
|
878
|
+
return
|
|
879
|
+
}
|
|
880
|
+
policy.pending = false
|
|
881
|
+
if (store.entries.get(entry.key) === entry) {
|
|
882
|
+
scheduleInvalidationRefetch(store, entry)
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/*
|
|
887
|
+
A failed revalidation keeps the stale entry — blanking data a reader is showing
|
|
888
|
+
over a transient error would make every background refresh a risk. 404 is the
|
|
889
|
+
exception: the resource is gone, so the retained value is a ghost an invalidation
|
|
890
|
+
stream would refetch forever. Evict it exactly as invalidate() drops a policy-less
|
|
891
|
+
entry (pendingRefresh marks the next read a reload; the notify re-runs readers),
|
|
892
|
+
so a live read replaces it and surfaces the proper error once.
|
|
893
|
+
*/
|
|
894
|
+
function settleRefetchFailure(store: CacheStore, entry: CacheEntry, status?: number): void {
|
|
895
|
+
if (status === 404) {
|
|
896
|
+
evictIfCurrent(store, entry)
|
|
897
|
+
store.pendingRefresh.add(entry.key)
|
|
898
|
+
emit(store, [entry.key])
|
|
899
|
+
return
|
|
900
|
+
}
|
|
901
|
+
store.markLifecycle(entry.key)
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/* Folds new tags into an entry's existing set without duplicating them. */
|
|
905
|
+
function mergeScopes(existing: Set<string> | undefined, incoming: string | string[]): Set<string> {
|
|
906
|
+
return new Set([...(existing ?? []), ...toScopeSet(incoming)])
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/*
|
|
910
|
+
Tags an existing entry with a read's scope so a later cache.invalidate({ scope })
|
|
911
|
+
reaches entries hydrated from the SSR snapshot (which carry a value but no scope)
|
|
912
|
+
without a refetch. Merges rather than replaces so a read tagging one group can't
|
|
913
|
+
drop tags another read site already added; a no-op when the read passes no scope.
|
|
914
|
+
*/
|
|
915
|
+
function tagScope(entry: CacheEntry, scope: CacheOptions['scope']): void {
|
|
916
|
+
if (scope !== undefined) {
|
|
917
|
+
entry.scope = mergeScopes(entry.scope, scope)
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/*
|
|
922
|
+
Mirrors tagScope for invalidate policies: a read declaring a policy arms an
|
|
923
|
+
existing entry that lacks one. Hydrated snapshot entries carry a value but no
|
|
924
|
+
refetch thunk — without this, the first invalidate after hydration would hard-
|
|
925
|
+
drop the entry (a pending flash) instead of revalidating stale-in-place, and a
|
|
926
|
+
policy-less first read would permanently win over a later read that declared
|
|
927
|
+
one. An entry that already has a policy keeps it (first policy wins; the key
|
|
928
|
+
is the same call, so the thunks are interchangeable).
|
|
929
|
+
*/
|
|
930
|
+
function attachPolicy(
|
|
931
|
+
entry: CacheEntry,
|
|
932
|
+
options: CacheOptions | undefined,
|
|
933
|
+
refetch: () => Promise<unknown>,
|
|
934
|
+
): void {
|
|
935
|
+
const policy = options?.invalidate
|
|
936
|
+
if (
|
|
937
|
+
entry.invalidation ||
|
|
938
|
+
!policy ||
|
|
939
|
+
(policy.throttle === undefined && policy.debounce === undefined)
|
|
940
|
+
) {
|
|
941
|
+
return
|
|
942
|
+
}
|
|
943
|
+
entry.invalidation = { refetch, throttle: policy.throttle, debounce: policy.debounce }
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function emit(store: CacheStore, keys: string[]): void {
|
|
947
|
+
if (keys.length === 0) {
|
|
948
|
+
return
|
|
949
|
+
}
|
|
950
|
+
store.events.dispatchEvent(invalidateEvent(keys))
|
|
951
|
+
}
|