@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.
Files changed (562) hide show
  1. package/CHANGELOG.md +607 -0
  2. package/LICENSE +21 -0
  3. package/README.md +154 -0
  4. package/bin/abide.ts +212 -0
  5. package/package.json +155 -0
  6. package/src/abideLsp.ts +211 -0
  7. package/src/abideModules.d.ts +8 -0
  8. package/src/abideResolverPlugin.ts +923 -0
  9. package/src/appEntry.ts +151 -0
  10. package/src/assets/app.html +12 -0
  11. package/src/build.ts +143 -0
  12. package/src/buildCli.ts +127 -0
  13. package/src/buildDisconnected.ts +118 -0
  14. package/src/bundleApp.ts +147 -0
  15. package/src/bundleDisconnectedEntry.ts +14 -0
  16. package/src/checkAbide.ts +77 -0
  17. package/src/cliEntry.ts +25 -0
  18. package/src/clientBuildPlugins.ts +33 -0
  19. package/src/clientEntry.ts +17 -0
  20. package/src/compile.ts +63 -0
  21. package/src/controlServerWorker.ts +426 -0
  22. package/src/devEntry.ts +250 -0
  23. package/src/discoveryEntry.ts +81 -0
  24. package/src/lib/bundle/BundleMenu.ts +12 -0
  25. package/src/lib/bundle/BundleMenuItem.ts +25 -0
  26. package/src/lib/bundle/BundleWindow.ts +37 -0
  27. package/src/lib/bundle/WEBVIEW_BUILD_REVISION.ts +9 -0
  28. package/src/lib/bundle/WEBVIEW_VERSION.ts +7 -0
  29. package/src/lib/bundle/bindConnectedFlag.ts +29 -0
  30. package/src/lib/bundle/bindRequestNavigate.ts +34 -0
  31. package/src/lib/bundle/buildWebviewLib.ts +111 -0
  32. package/src/lib/bundle/bundled.ts +35 -0
  33. package/src/lib/bundle/disconnected.abide +236 -0
  34. package/src/lib/bundle/disconnected.css +9 -0
  35. package/src/lib/bundle/ensureWebviewLib.ts +20 -0
  36. package/src/lib/bundle/exitWithParent.ts +28 -0
  37. package/src/lib/bundle/infoPlist.ts +46 -0
  38. package/src/lib/bundle/installDownloads.ts +24 -0
  39. package/src/lib/bundle/installMacMenu.ts +39 -0
  40. package/src/lib/bundle/listenLocalControlServer.ts +19 -0
  41. package/src/lib/bundle/native/abideMenu.mm +422 -0
  42. package/src/lib/bundle/native/webview.h +4557 -0
  43. package/src/lib/bundle/onMenu.ts +42 -0
  44. package/src/lib/bundle/openWebview.ts +104 -0
  45. package/src/lib/bundle/pngToIcns.ts +47 -0
  46. package/src/lib/bundle/probeAbideServer.ts +57 -0
  47. package/src/lib/bundle/resolveServerBinary.ts +12 -0
  48. package/src/lib/bundle/resolveWebviewLib.ts +53 -0
  49. package/src/lib/bundle/serverBinaryFilename.ts +8 -0
  50. package/src/lib/bundle/signMacApp.ts +37 -0
  51. package/src/lib/bundle/spawnEmbeddedServer.ts +64 -0
  52. package/src/lib/bundle/stableLocalPort.ts +19 -0
  53. package/src/lib/bundle/waitForServer.ts +23 -0
  54. package/src/lib/bundle/webviewCachePath.ts +23 -0
  55. package/src/lib/bundle/webviewLibName.ts +11 -0
  56. package/src/lib/cli/connectToServer.ts +23 -0
  57. package/src/lib/cli/createClient.ts +108 -0
  58. package/src/lib/cli/dispatchCommand.ts +71 -0
  59. package/src/lib/cli/loadEnvFromBinaryDir.ts +15 -0
  60. package/src/lib/cli/parseArgvForRpc.ts +100 -0
  61. package/src/lib/cli/printHelp.ts +119 -0
  62. package/src/lib/cli/printSessionHelp.ts +27 -0
  63. package/src/lib/cli/printSessionStatus.ts +21 -0
  64. package/src/lib/cli/printTrimmed.ts +8 -0
  65. package/src/lib/cli/printValue.ts +10 -0
  66. package/src/lib/cli/resolveCliTarget.ts +48 -0
  67. package/src/lib/cli/runCli.ts +176 -0
  68. package/src/lib/cli/runSession.ts +108 -0
  69. package/src/lib/cli/startLocalInstance.ts +14 -0
  70. package/src/lib/cli/tokenizeLine.ts +51 -0
  71. package/src/lib/cli/types/CliManifest.ts +9 -0
  72. package/src/lib/cli/types/CliManifestEntry.ts +17 -0
  73. package/src/lib/cli/types/CliTarget.ts +13 -0
  74. package/src/lib/mcp/annotationsForMethod.ts +29 -0
  75. package/src/lib/mcp/createMcpResourceServer.ts +102 -0
  76. package/src/lib/mcp/createMcpServer.ts +48 -0
  77. package/src/lib/mcp/dispatchMcpRequest.ts +138 -0
  78. package/src/lib/mcp/mcpResourceServerSlot.ts +18 -0
  79. package/src/lib/mcp/mcpSurface.ts +295 -0
  80. package/src/lib/mcp/toolResultFromResponse.ts +66 -0
  81. package/src/lib/mcp/types/JsonRpcRequest.ts +12 -0
  82. package/src/lib/mcp/types/JsonRpcResponse.ts +20 -0
  83. package/src/lib/mcp/types/McpResourceContents.ts +10 -0
  84. package/src/lib/mcp/types/McpResourceDescriptor.ts +6 -0
  85. package/src/lib/mcp/types/McpResourceServer.ts +12 -0
  86. package/src/lib/mcp/types/McpServer.ts +9 -0
  87. package/src/lib/mcp/types/McpServerOptions.ts +16 -0
  88. package/src/lib/server/AppModule.ts +47 -0
  89. package/src/lib/server/DELETE.ts +10 -0
  90. package/src/lib/server/GET.ts +10 -0
  91. package/src/lib/server/HEAD.ts +10 -0
  92. package/src/lib/server/PATCH.ts +10 -0
  93. package/src/lib/server/POST.ts +10 -0
  94. package/src/lib/server/PUT.ts +10 -0
  95. package/src/lib/server/agent.ts +86 -0
  96. package/src/lib/server/appDataDir.ts +16 -0
  97. package/src/lib/server/cli/buildEnvContent.ts +19 -0
  98. package/src/lib/server/cli/createTarGz.ts +77 -0
  99. package/src/lib/server/cli/handleCliDownload.ts +150 -0
  100. package/src/lib/server/cli/handleCliInstall.ts +37 -0
  101. package/src/lib/server/cli/installScript.ts +31 -0
  102. package/src/lib/server/cli/maxSourceMtime.ts +26 -0
  103. package/src/lib/server/cookies.ts +30 -0
  104. package/src/lib/server/env.ts +51 -0
  105. package/src/lib/server/error.ts +73 -0
  106. package/src/lib/server/json.ts +42 -0
  107. package/src/lib/server/jsonl.ts +47 -0
  108. package/src/lib/server/prompts/definePrompt.ts +21 -0
  109. package/src/lib/server/prompts/promptRegistry.ts +9 -0
  110. package/src/lib/server/prompts/registerPrompt.ts +6 -0
  111. package/src/lib/server/prompts/renderPromptTemplate.ts +17 -0
  112. package/src/lib/server/prompts/types/Prompt.ts +13 -0
  113. package/src/lib/server/prompts/types/PromptOptions.ts +12 -0
  114. package/src/lib/server/prompts/types/PromptRegistryEntry.ts +13 -0
  115. package/src/lib/server/prompts/types/PromptRoutes.ts +10 -0
  116. package/src/lib/server/reachable.ts +45 -0
  117. package/src/lib/server/redirect.ts +43 -0
  118. package/src/lib/server/request.ts +19 -0
  119. package/src/lib/server/rpc/defineVerb.ts +210 -0
  120. package/src/lib/server/rpc/dispatchVerbInProcess.ts +46 -0
  121. package/src/lib/server/rpc/findVerbByCommandName.ts +18 -0
  122. package/src/lib/server/rpc/parseArgs.ts +127 -0
  123. package/src/lib/server/rpc/readBodyWithinLimit.ts +44 -0
  124. package/src/lib/server/rpc/registerVerb.ts +6 -0
  125. package/src/lib/server/rpc/runWithVerbTimeout.ts +49 -0
  126. package/src/lib/server/rpc/types/RemoteHandler.ts +27 -0
  127. package/src/lib/server/rpc/types/RemoteRoutes.ts +13 -0
  128. package/src/lib/server/rpc/types/TypedResponse.ts +18 -0
  129. package/src/lib/server/rpc/types/VerbHelper.ts +87 -0
  130. package/src/lib/server/rpc/types/VerbRegistryEntry.ts +35 -0
  131. package/src/lib/server/rpc/unprocessed.ts +14 -0
  132. package/src/lib/server/rpc/verbRegistry.ts +11 -0
  133. package/src/lib/server/runtime/DEFAULT_PORT.ts +6 -0
  134. package/src/lib/server/runtime/DEV_READY_MESSAGE.ts +6 -0
  135. package/src/lib/server/runtime/DEV_REBUILD_MESSAGE.ts +4 -0
  136. package/src/lib/server/runtime/DEV_RELOAD_CLIENT_SCRIPT.ts +107 -0
  137. package/src/lib/server/runtime/SSR_SWAP_SCRIPT.ts +16 -0
  138. package/src/lib/server/runtime/acceptsGzip.ts +24 -0
  139. package/src/lib/server/runtime/buildCacheSnapshot.ts +61 -0
  140. package/src/lib/server/runtime/buildHealthPayload.ts +34 -0
  141. package/src/lib/server/runtime/buildInspectorSurface.ts +37 -0
  142. package/src/lib/server/runtime/buildOpenApiSpec.ts +106 -0
  143. package/src/lib/server/runtime/cacheControlForAsset.ts +22 -0
  144. package/src/lib/server/runtime/containsTraversal.ts +37 -0
  145. package/src/lib/server/runtime/createAppAssetServer.ts +76 -0
  146. package/src/lib/server/runtime/createAssetHeaderCache.ts +31 -0
  147. package/src/lib/server/runtime/createPublicAssetServer.ts +67 -0
  148. package/src/lib/server/runtime/createReachable.ts +109 -0
  149. package/src/lib/server/runtime/createRouteDispatcher.ts +127 -0
  150. package/src/lib/server/runtime/createServer.ts +674 -0
  151. package/src/lib/server/runtime/createUiPageRenderer.ts +181 -0
  152. package/src/lib/server/runtime/crossOriginForbidden.ts +17 -0
  153. package/src/lib/server/runtime/crossOriginGate.ts +29 -0
  154. package/src/lib/server/runtime/devClientFingerprint.ts +117 -0
  155. package/src/lib/server/runtime/devHotModuleResponse.ts +40 -0
  156. package/src/lib/server/runtime/devReloadResponse.ts +41 -0
  157. package/src/lib/server/runtime/disableIdleTimeoutForStream.ts +27 -0
  158. package/src/lib/server/runtime/envSchemaStore.ts +15 -0
  159. package/src/lib/server/runtime/findOpenPort.ts +21 -0
  160. package/src/lib/server/runtime/getActiveServer.ts +6 -0
  161. package/src/lib/server/runtime/globToPathSet.ts +29 -0
  162. package/src/lib/server/runtime/gzipResponse.ts +46 -0
  163. package/src/lib/server/runtime/inProcessServer.ts +20 -0
  164. package/src/lib/server/runtime/internalErrorResponse.ts +25 -0
  165. package/src/lib/server/runtime/isCrossOriginRequest.ts +23 -0
  166. package/src/lib/server/runtime/listenOnOpenPort.ts +36 -0
  167. package/src/lib/server/runtime/logExposedSurfaces.ts +156 -0
  168. package/src/lib/server/runtime/maybeMountInspector.ts +97 -0
  169. package/src/lib/server/runtime/mimeForExtension.ts +14 -0
  170. package/src/lib/server/runtime/pageUrlFromStore.ts +15 -0
  171. package/src/lib/server/runtime/parseIdleTimeout.ts +10 -0
  172. package/src/lib/server/runtime/parsePort.ts +11 -0
  173. package/src/lib/server/runtime/registryManifests.ts +66 -0
  174. package/src/lib/server/runtime/requestContext.ts +5 -0
  175. package/src/lib/server/runtime/resolvePageSnapshot.ts +25 -0
  176. package/src/lib/server/runtime/respondWithEmbeddedAsset.ts +18 -0
  177. package/src/lib/server/runtime/runWithRequestScope.ts +150 -0
  178. package/src/lib/server/runtime/safeJsonForScript.ts +17 -0
  179. package/src/lib/server/runtime/serializeCacheSnapshot.ts +45 -0
  180. package/src/lib/server/runtime/serverSlot.ts +13 -0
  181. package/src/lib/server/runtime/setActiveServer.ts +6 -0
  182. package/src/lib/server/runtime/snapshotEntryFromCache.ts +83 -0
  183. package/src/lib/server/runtime/streamCacheResolutions.ts +37 -0
  184. package/src/lib/server/runtime/streamFromIterator.ts +86 -0
  185. package/src/lib/server/runtime/types/Assets.ts +6 -0
  186. package/src/lib/server/runtime/types/DevReloadStamp.ts +18 -0
  187. package/src/lib/server/runtime/types/InspectorCacheEntry.ts +24 -0
  188. package/src/lib/server/runtime/types/InspectorCacheSnapshot.ts +11 -0
  189. package/src/lib/server/runtime/types/InspectorContext.ts +30 -0
  190. package/src/lib/server/runtime/types/InspectorSocket.ts +17 -0
  191. package/src/lib/server/runtime/types/InspectorSurface.ts +13 -0
  192. package/src/lib/server/runtime/types/InspectorVerb.ts +27 -0
  193. package/src/lib/server/runtime/types/RequestStore.ts +55 -0
  194. package/src/lib/server/runtime/warnUnguardedMcp.ts +32 -0
  195. package/src/lib/server/runtime/withResponseDefaults.ts +24 -0
  196. package/src/lib/server/server.ts +33 -0
  197. package/src/lib/server/socket.ts +32 -0
  198. package/src/lib/server/sockets/createSocketDispatcher.ts +337 -0
  199. package/src/lib/server/sockets/defineSocket.ts +179 -0
  200. package/src/lib/server/sockets/lookupSocket.ts +6 -0
  201. package/src/lib/server/sockets/registerSocket.ts +6 -0
  202. package/src/lib/server/sockets/socketOperations.ts +36 -0
  203. package/src/lib/server/sockets/socketRegistry.ts +9 -0
  204. package/src/lib/server/sockets/types/Socket.ts +23 -0
  205. package/src/lib/server/sockets/types/SocketClientFrame.ts +19 -0
  206. package/src/lib/server/sockets/types/SocketOperation.ts +22 -0
  207. package/src/lib/server/sockets/types/SocketOptions.ts +26 -0
  208. package/src/lib/server/sockets/types/SocketRegistryEntry.ts +19 -0
  209. package/src/lib/server/sockets/types/SocketRoutes.ts +10 -0
  210. package/src/lib/server/sockets/types/SocketServerFrame.ts +24 -0
  211. package/src/lib/server/sse.ts +54 -0
  212. package/src/lib/shared/ABIDE_PACKAGE_NAME.ts +7 -0
  213. package/src/lib/shared/ABIDE_VERSION.ts +9 -0
  214. package/src/lib/shared/CACHE_CONTROL_VALUES.ts +16 -0
  215. package/src/lib/shared/CACHE_WRAPPED.ts +8 -0
  216. package/src/lib/shared/CLI_PATH.ts +7 -0
  217. package/src/lib/shared/DEV_HOT_PREFIX.ts +7 -0
  218. package/src/lib/shared/DEV_RELOAD_PATH.ts +6 -0
  219. package/src/lib/shared/HEALTH_PATH.ts +7 -0
  220. package/src/lib/shared/HttpError.ts +20 -0
  221. package/src/lib/shared/IDENTITY_PATH.ts +6 -0
  222. package/src/lib/shared/INSPECTOR_PATH.ts +7 -0
  223. package/src/lib/shared/NAV_HEADER.ts +8 -0
  224. package/src/lib/shared/OFFLINE_HEADER.ts +8 -0
  225. package/src/lib/shared/REMOTE_FUNCTION.ts +8 -0
  226. package/src/lib/shared/REPLAYABLE_METHODS.ts +12 -0
  227. package/src/lib/shared/SOCKETS_PATH.ts +7 -0
  228. package/src/lib/shared/STREAMING_CONTENT_TYPES.ts +11 -0
  229. package/src/lib/shared/SocketDisconnectedError.ts +13 -0
  230. package/src/lib/shared/TEXT_PLAIN.ts +7 -0
  231. package/src/lib/shared/abideImportName.ts +44 -0
  232. package/src/lib/shared/abideLog.ts +38 -0
  233. package/src/lib/shared/activeCacheStore.ts +20 -0
  234. package/src/lib/shared/activePage.ts +25 -0
  235. package/src/lib/shared/appDataDir.ts +34 -0
  236. package/src/lib/shared/appNameSlot.ts +10 -0
  237. package/src/lib/shared/basePath.ts +10 -0
  238. package/src/lib/shared/basePathFromAppUrl.ts +20 -0
  239. package/src/lib/shared/baseSlot.ts +14 -0
  240. package/src/lib/shared/binaryDirEnvPath.ts +12 -0
  241. package/src/lib/shared/browserClientFlags.ts +10 -0
  242. package/src/lib/shared/buildRpcProxy.ts +39 -0
  243. package/src/lib/shared/buildRpcRequest.ts +70 -0
  244. package/src/lib/shared/buildSocketOverChannel.ts +58 -0
  245. package/src/lib/shared/bundleLayout.ts +36 -0
  246. package/src/lib/shared/cache.ts +951 -0
  247. package/src/lib/shared/cacheEntryFromSnapshot.ts +59 -0
  248. package/src/lib/shared/cacheStoreSlot.ts +16 -0
  249. package/src/lib/shared/cacheStores.ts +10 -0
  250. package/src/lib/shared/canonicalJson.ts +63 -0
  251. package/src/lib/shared/carriesBodyArgs.ts +13 -0
  252. package/src/lib/shared/clearLastConnection.ts +7 -0
  253. package/src/lib/shared/commandNameForUrl.ts +17 -0
  254. package/src/lib/shared/createCacheStore.ts +104 -0
  255. package/src/lib/shared/createChannelLog.ts +122 -0
  256. package/src/lib/shared/createLifecycleChannel.ts +56 -0
  257. package/src/lib/shared/createLivenessWatch.ts +118 -0
  258. package/src/lib/shared/createPushIterator.ts +127 -0
  259. package/src/lib/shared/createRemoteFunction.ts +122 -0
  260. package/src/lib/shared/createSubscriber.ts +55 -0
  261. package/src/lib/shared/createTraceContext.ts +21 -0
  262. package/src/lib/shared/dataDirEnvPath.ts +12 -0
  263. package/src/lib/shared/decodeResponse.ts +47 -0
  264. package/src/lib/shared/detectTarget.ts +27 -0
  265. package/src/lib/shared/detectVerbMethod.ts +17 -0
  266. package/src/lib/shared/emitLogRecord.ts +190 -0
  267. package/src/lib/shared/exeSuffix.ts +9 -0
  268. package/src/lib/shared/exitOnBuildFailure.ts +17 -0
  269. package/src/lib/shared/extraForwardHeaders.ts +16 -0
  270. package/src/lib/shared/fileStem.ts +9 -0
  271. package/src/lib/shared/findExportCallSite.ts +476 -0
  272. package/src/lib/shared/formatTraceparent.ts +6 -0
  273. package/src/lib/shared/forwardHeaders.ts +44 -0
  274. package/src/lib/shared/getRemoteMeta.ts +5 -0
  275. package/src/lib/shared/globalCacheStore.ts +15 -0
  276. package/src/lib/shared/globalCacheStoreSlot.ts +14 -0
  277. package/src/lib/shared/health.ts +179 -0
  278. package/src/lib/shared/healthReadSlot.ts +11 -0
  279. package/src/lib/shared/healthSeedSlot.ts +12 -0
  280. package/src/lib/shared/html.ts +38 -0
  281. package/src/lib/shared/importNamesToStrip.ts +13 -0
  282. package/src/lib/shared/invalidateEvent.ts +11 -0
  283. package/src/lib/shared/invalidateTripwire.ts +40 -0
  284. package/src/lib/shared/isAbideHealthPayload.ts +11 -0
  285. package/src/lib/shared/isCompileTarget.ts +15 -0
  286. package/src/lib/shared/isDebugEnabled.ts +26 -0
  287. package/src/lib/shared/isDebugNegated.ts +19 -0
  288. package/src/lib/shared/isModuleNotFound.ts +16 -0
  289. package/src/lib/shared/isReadOnlyMethod.ts +14 -0
  290. package/src/lib/shared/isReplayableMethod.ts +7 -0
  291. package/src/lib/shared/isStreamingResponse.ts +11 -0
  292. package/src/lib/shared/isSubscribable.ts +15 -0
  293. package/src/lib/shared/jsonSchemaForPromptArguments.ts +29 -0
  294. package/src/lib/shared/jsonSchemaForSchema.ts +39 -0
  295. package/src/lib/shared/jsonlErrorFrame.ts +24 -0
  296. package/src/lib/shared/keyForRemoteCall.ts +29 -0
  297. package/src/lib/shared/keyMatchesPrefix.ts +9 -0
  298. package/src/lib/shared/lastConnectionPath.ts +7 -0
  299. package/src/lib/shared/layoutChainForRoute.ts +22 -0
  300. package/src/lib/shared/loadEnvFile.ts +17 -0
  301. package/src/lib/shared/loadEnvFromDataDir.ts +14 -0
  302. package/src/lib/shared/log.ts +24 -0
  303. package/src/lib/shared/logClosingRecord.ts +28 -0
  304. package/src/lib/shared/logTapSlot.ts +13 -0
  305. package/src/lib/shared/manifestModule.ts +39 -0
  306. package/src/lib/shared/matchesDebugPattern.ts +16 -0
  307. package/src/lib/shared/memoizeByKey.ts +32 -0
  308. package/src/lib/shared/normalizeTarget.ts +10 -0
  309. package/src/lib/shared/online.ts +51 -0
  310. package/src/lib/shared/page.ts +30 -0
  311. package/src/lib/shared/pageSlot.ts +17 -0
  312. package/src/lib/shared/pageUrlForFile.ts +14 -0
  313. package/src/lib/shared/parseBoundedEnvInt.ts +20 -0
  314. package/src/lib/shared/parseDebugPatterns.ts +21 -0
  315. package/src/lib/shared/parseEnv.ts +30 -0
  316. package/src/lib/shared/parsePromptMarkdown.ts +35 -0
  317. package/src/lib/shared/parseRouteSegments.ts +22 -0
  318. package/src/lib/shared/parseTraceparent.ts +26 -0
  319. package/src/lib/shared/pending.ts +30 -0
  320. package/src/lib/shared/prepareRpcModule.ts +59 -0
  321. package/src/lib/shared/prepareSocketModule.ts +49 -0
  322. package/src/lib/shared/probeRegistries.ts +68 -0
  323. package/src/lib/shared/producerKey.ts +32 -0
  324. package/src/lib/shared/programNameForPackage.ts +14 -0
  325. package/src/lib/shared/promptNameForFile.ts +10 -0
  326. package/src/lib/shared/queryStringFromArgs.ts +27 -0
  327. package/src/lib/shared/randomHexId.ts +14 -0
  328. package/src/lib/shared/readEnvFile.ts +15 -0
  329. package/src/lib/shared/readLastConnection.ts +18 -0
  330. package/src/lib/shared/readPackageJson.ts +9 -0
  331. package/src/lib/shared/recordRemoteMeta.ts +5 -0
  332. package/src/lib/shared/refreshing.ts +31 -0
  333. package/src/lib/shared/remoteMetaStore.ts +16 -0
  334. package/src/lib/shared/requestScopeSlot.ts +15 -0
  335. package/src/lib/shared/resolveClientFlags.ts +20 -0
  336. package/src/lib/shared/responseErrorText.ts +9 -0
  337. package/src/lib/shared/rpcTimeoutSlot.ts +9 -0
  338. package/src/lib/shared/rpcUrlForFile.ts +19 -0
  339. package/src/lib/shared/runningAsStandaloneBinary.ts +13 -0
  340. package/src/lib/shared/selectorMatcher.ts +68 -0
  341. package/src/lib/shared/selectorPrefix.ts +39 -0
  342. package/src/lib/shared/serializeEnv.ts +18 -0
  343. package/src/lib/shared/setAppName.ts +5 -0
  344. package/src/lib/shared/setBaseResolver.ts +6 -0
  345. package/src/lib/shared/setCacheStoreResolver.ts +6 -0
  346. package/src/lib/shared/setGlobalCacheStoreResolver.ts +6 -0
  347. package/src/lib/shared/setPageResolver.ts +7 -0
  348. package/src/lib/shared/setRequestScopeResolver.ts +6 -0
  349. package/src/lib/shared/snippet.ts +25 -0
  350. package/src/lib/shared/socketNameForFile.ts +11 -0
  351. package/src/lib/shared/socketTapSlot.ts +12 -0
  352. package/src/lib/shared/sseErrorFrame.ts +29 -0
  353. package/src/lib/shared/streamResponse.ts +169 -0
  354. package/src/lib/shared/stripImport.ts +27 -0
  355. package/src/lib/shared/subscribableFromResponse.ts +51 -0
  356. package/src/lib/shared/tailProbeSlot.ts +16 -0
  357. package/src/lib/shared/toBunRoutePattern.ts +28 -0
  358. package/src/lib/shared/toScopeSet.ts +4 -0
  359. package/src/lib/shared/trace.ts +16 -0
  360. package/src/lib/shared/types/CacheEntry.ts +84 -0
  361. package/src/lib/shared/types/CacheInvalidation.ts +9 -0
  362. package/src/lib/shared/types/CacheOnContext.ts +25 -0
  363. package/src/lib/shared/types/CacheOptions.ts +39 -0
  364. package/src/lib/shared/types/CacheSelector.ts +17 -0
  365. package/src/lib/shared/types/CacheSnapshot.ts +16 -0
  366. package/src/lib/shared/types/CacheSnapshotEntry.ts +17 -0
  367. package/src/lib/shared/types/CacheStats.ts +13 -0
  368. package/src/lib/shared/types/CacheStore.ts +39 -0
  369. package/src/lib/shared/types/ChannelLog.ts +13 -0
  370. package/src/lib/shared/types/ClientFlags.ts +11 -0
  371. package/src/lib/shared/types/CompileTarget.ts +6 -0
  372. package/src/lib/shared/types/FrameworkLog.ts +13 -0
  373. package/src/lib/shared/types/HttpVerb.ts +1 -0
  374. package/src/lib/shared/types/LastConnection.ts +9 -0
  375. package/src/lib/shared/types/Log.ts +13 -0
  376. package/src/lib/shared/types/LogRecord.ts +42 -0
  377. package/src/lib/shared/types/LogVoice.ts +7 -0
  378. package/src/lib/shared/types/PageSnapshot.ts +14 -0
  379. package/src/lib/shared/types/PromptArgument.ts +12 -0
  380. package/src/lib/shared/types/RawRemoteFunction.ts +14 -0
  381. package/src/lib/shared/types/RemoteCallable.ts +12 -0
  382. package/src/lib/shared/types/RemoteFunction.ts +47 -0
  383. package/src/lib/shared/types/ReplayableMethod.ts +7 -0
  384. package/src/lib/shared/types/RequestScopeInfo.ts +16 -0
  385. package/src/lib/shared/types/RpcInvoker.ts +6 -0
  386. package/src/lib/shared/types/SocketChannel.ts +17 -0
  387. package/src/lib/shared/types/SocketSubCallbacks.ts +13 -0
  388. package/src/lib/shared/types/StandardSchemaV1.ts +56 -0
  389. package/src/lib/shared/types/StreamedResolution.ts +10 -0
  390. package/src/lib/shared/types/Subscribable.ts +26 -0
  391. package/src/lib/shared/types/TailHooks.ts +12 -0
  392. package/src/lib/shared/types/TailOptions.ts +10 -0
  393. package/src/lib/shared/types/TraceContext.ts +17 -0
  394. package/src/lib/shared/url.ts +118 -0
  395. package/src/lib/shared/withBase.ts +11 -0
  396. package/src/lib/shared/withBaseUrl.ts +17 -0
  397. package/src/lib/shared/withJsonSchema.ts +21 -0
  398. package/src/lib/shared/writeDts.ts +12 -0
  399. package/src/lib/shared/writeHealthDts.ts +36 -0
  400. package/src/lib/shared/writeLastConnection.ts +13 -0
  401. package/src/lib/shared/writePublicAssetsDts.ts +31 -0
  402. package/src/lib/shared/writeRoutesDts.ts +73 -0
  403. package/src/lib/shared/writeRpcDts.ts +49 -0
  404. package/src/lib/shared/writeTestRpcDts.ts +45 -0
  405. package/src/lib/shared/writeTestSocketsDts.ts +34 -0
  406. package/src/lib/test/assertAgentFrameConformance.ts +73 -0
  407. package/src/lib/test/createScriptedSurface.ts +45 -0
  408. package/src/lib/test/createTestApp.ts +203 -0
  409. package/src/lib/test/createTestSocketChannel.ts +142 -0
  410. package/src/lib/ui/README.md +86 -0
  411. package/src/lib/ui/compile/SSR_ESCAPE.ts +25 -0
  412. package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +36 -0
  413. package/src/lib/ui/compile/VOID_TAGS.ts +21 -0
  414. package/src/lib/ui/compile/abideUiPlugin.ts +65 -0
  415. package/src/lib/ui/compile/analyzeComponent.ts +117 -0
  416. package/src/lib/ui/compile/assetModulesFile.ts +32 -0
  417. package/src/lib/ui/compile/branchElements.ts +50 -0
  418. package/src/lib/ui/compile/collectAbideDiagnostics.ts +59 -0
  419. package/src/lib/ui/compile/compileComponent.ts +20 -0
  420. package/src/lib/ui/compile/compileModule.ts +116 -0
  421. package/src/lib/ui/compile/compileSSR.ts +36 -0
  422. package/src/lib/ui/compile/compileShadow.ts +352 -0
  423. package/src/lib/ui/compile/createShadowLanguageService.ts +197 -0
  424. package/src/lib/ui/compile/createShadowProgram.ts +96 -0
  425. package/src/lib/ui/compile/decodeHtmlEntities.ts +49 -0
  426. package/src/lib/ui/compile/desugarSignals.ts +133 -0
  427. package/src/lib/ui/compile/escapeHtml.ts +15 -0
  428. package/src/lib/ui/compile/generateBuild.ts +638 -0
  429. package/src/lib/ui/compile/generateSSR.ts +380 -0
  430. package/src/lib/ui/compile/groupBindParts.ts +28 -0
  431. package/src/lib/ui/compile/hoistCells.ts +120 -0
  432. package/src/lib/ui/compile/loadShadowTsConfig.ts +31 -0
  433. package/src/lib/ui/compile/lowerDocAccess.ts +202 -0
  434. package/src/lib/ui/compile/nearestProjectRoot.ts +16 -0
  435. package/src/lib/ui/compile/parseTemplate.ts +396 -0
  436. package/src/lib/ui/compile/partitionSlots.ts +36 -0
  437. package/src/lib/ui/compile/prepareNestedScript.ts +42 -0
  438. package/src/lib/ui/compile/remapShadowDiagnostic.ts +30 -0
  439. package/src/lib/ui/compile/renameSignalRefs.ts +85 -0
  440. package/src/lib/ui/compile/resolveAbideImports.ts +29 -0
  441. package/src/lib/ui/compile/scopeCss.ts +115 -0
  442. package/src/lib/ui/compile/shadowNaming.ts +11 -0
  443. package/src/lib/ui/compile/sourceToShadowOffset.ts +24 -0
  444. package/src/lib/ui/compile/staticAttrValue.ts +13 -0
  445. package/src/lib/ui/compile/stripEffects.ts +32 -0
  446. package/src/lib/ui/compile/types/AbideDiagnostic.ts +14 -0
  447. package/src/lib/ui/compile/types/AnalyzedComponent.ts +25 -0
  448. package/src/lib/ui/compile/types/CompiledShadow.ts +15 -0
  449. package/src/lib/ui/compile/types/TemplateAttr.ts +16 -0
  450. package/src/lib/ui/compile/types/TemplateNode.ts +78 -0
  451. package/src/lib/ui/compile/types/TextPart.ts +8 -0
  452. package/src/lib/ui/derived.ts +28 -0
  453. package/src/lib/ui/doc.ts +15 -0
  454. package/src/lib/ui/dom/appendSnippet.ts +34 -0
  455. package/src/lib/ui/dom/appendStatic.ts +27 -0
  456. package/src/lib/ui/dom/appendText.ts +114 -0
  457. package/src/lib/ui/dom/applyResolved.ts +72 -0
  458. package/src/lib/ui/dom/attach.ts +20 -0
  459. package/src/lib/ui/dom/attr.ts +19 -0
  460. package/src/lib/ui/dom/awaitBlock.ts +224 -0
  461. package/src/lib/ui/dom/cloneStatic.ts +52 -0
  462. package/src/lib/ui/dom/each.ts +115 -0
  463. package/src/lib/ui/dom/eachAsync.ts +153 -0
  464. package/src/lib/ui/dom/hydrate.ts +35 -0
  465. package/src/lib/ui/dom/mount.ts +29 -0
  466. package/src/lib/ui/dom/mountChild.ts +33 -0
  467. package/src/lib/ui/dom/on.ts +15 -0
  468. package/src/lib/ui/dom/openChild.ts +22 -0
  469. package/src/lib/ui/dom/openRoot.ts +20 -0
  470. package/src/lib/ui/dom/switchBlock.ts +75 -0
  471. package/src/lib/ui/dom/text.ts +20 -0
  472. package/src/lib/ui/dom/tryBlock.ts +112 -0
  473. package/src/lib/ui/dom/types/EachRow.ts +3 -0
  474. package/src/lib/ui/dom/types/SwitchCase.ts +6 -0
  475. package/src/lib/ui/dom/when.ts +73 -0
  476. package/src/lib/ui/effect.ts +16 -0
  477. package/src/lib/ui/installHotBridge.ts +73 -0
  478. package/src/lib/ui/matchRoute.ts +89 -0
  479. package/src/lib/ui/navigate.ts +17 -0
  480. package/src/lib/ui/probeNavigation.ts +33 -0
  481. package/src/lib/ui/remoteProxy.ts +97 -0
  482. package/src/lib/ui/renderChain.ts +50 -0
  483. package/src/lib/ui/renderToStream.ts +104 -0
  484. package/src/lib/ui/router.ts +286 -0
  485. package/src/lib/ui/runtime/OUTLET_TAG.ts +8 -0
  486. package/src/lib/ui/runtime/OWNER.ts +8 -0
  487. package/src/lib/ui/runtime/REACTIVE_CONTEXT.ts +14 -0
  488. package/src/lib/ui/runtime/RENDER.ts +23 -0
  489. package/src/lib/ui/runtime/RESUME.ts +16 -0
  490. package/src/lib/ui/runtime/applyPatchToTree.ts +41 -0
  491. package/src/lib/ui/runtime/claimChild.ts +10 -0
  492. package/src/lib/ui/runtime/clientPage.ts +16 -0
  493. package/src/lib/ui/runtime/createComputedNode.ts +16 -0
  494. package/src/lib/ui/runtime/createDoc.ts +177 -0
  495. package/src/lib/ui/runtime/createEffectNode.ts +58 -0
  496. package/src/lib/ui/runtime/createSignalNode.ts +16 -0
  497. package/src/lib/ui/runtime/detachLink.ts +21 -0
  498. package/src/lib/ui/runtime/endTracking.ts +24 -0
  499. package/src/lib/ui/runtime/enterRenderPass.ts +12 -0
  500. package/src/lib/ui/runtime/exitRenderPass.ts +7 -0
  501. package/src/lib/ui/runtime/firstOutlet.ts +22 -0
  502. package/src/lib/ui/runtime/flushEffects.ts +17 -0
  503. package/src/lib/ui/runtime/hotInstances.ts +10 -0
  504. package/src/lib/ui/runtime/hotReloadEnabled.ts +8 -0
  505. package/src/lib/ui/runtime/hotReplace.ts +25 -0
  506. package/src/lib/ui/runtime/nextBlockId.ts +11 -0
  507. package/src/lib/ui/runtime/pathExists.ts +23 -0
  508. package/src/lib/ui/runtime/readNode.ts +17 -0
  509. package/src/lib/ui/runtime/registerHotInstance.ts +23 -0
  510. package/src/lib/ui/runtime/runNode.ts +28 -0
  511. package/src/lib/ui/runtime/runtimePath.ts +9 -0
  512. package/src/lib/ui/runtime/scope.ts +24 -0
  513. package/src/lib/ui/runtime/toTeardown.ts +26 -0
  514. package/src/lib/ui/runtime/track.ts +58 -0
  515. package/src/lib/ui/runtime/trigger.ts +44 -0
  516. package/src/lib/ui/runtime/types/Cell.ts +5 -0
  517. package/src/lib/ui/runtime/types/Derived.ts +3 -0
  518. package/src/lib/ui/runtime/types/Doc.ts +19 -0
  519. package/src/lib/ui/runtime/types/EffectResult.ts +10 -0
  520. package/src/lib/ui/runtime/types/HotInstance.ts +14 -0
  521. package/src/lib/ui/runtime/types/NavVerdict.ts +9 -0
  522. package/src/lib/ui/runtime/types/Patch.ts +11 -0
  523. package/src/lib/ui/runtime/types/ReactiveLink.ts +21 -0
  524. package/src/lib/ui/runtime/types/ReactiveNode.ts +25 -0
  525. package/src/lib/ui/runtime/types/Route.ts +8 -0
  526. package/src/lib/ui/runtime/types/RouteLoader.ts +7 -0
  527. package/src/lib/ui/runtime/types/SsrRender.ts +22 -0
  528. package/src/lib/ui/runtime/types/State.ts +3 -0
  529. package/src/lib/ui/runtime/types/Teardown.ts +5 -0
  530. package/src/lib/ui/runtime/types/UiComponent.ts +16 -0
  531. package/src/lib/ui/runtime/types/UiProps.ts +15 -0
  532. package/src/lib/ui/runtime/unlinkDeps.ts +20 -0
  533. package/src/lib/ui/runtime/untrack.ts +20 -0
  534. package/src/lib/ui/runtime/valueAtPath.ts +18 -0
  535. package/src/lib/ui/runtime/writeNode.ts +16 -0
  536. package/src/lib/ui/socketChannel.ts +227 -0
  537. package/src/lib/ui/socketProxy.ts +25 -0
  538. package/src/lib/ui/startClient.ts +58 -0
  539. package/src/lib/ui/state.ts +25 -0
  540. package/src/lib/ui/tail.ts +324 -0
  541. package/src/lib/ui/types/Layouts.ts +9 -0
  542. package/src/lib/ui/types/Pages.ts +8 -0
  543. package/src/preload.ts +19 -0
  544. package/src/scaffold.ts +153 -0
  545. package/src/serverBuildPlugins.ts +19 -0
  546. package/src/serverEntry.ts +95 -0
  547. package/template/bunfig.toml +4 -0
  548. package/template/package.json +18 -0
  549. package/template/src/app.ts +28 -0
  550. package/template/src/bundle/icon.png +0 -0
  551. package/template/src/cli/banner.txt +3 -0
  552. package/template/src/cli/footer.txt +1 -0
  553. package/template/src/server/config.ts +17 -0
  554. package/template/src/server/rpc/getHello.ts +36 -0
  555. package/template/src/ui/Layout.abide +19 -0
  556. package/template/src/ui/app.css +21 -0
  557. package/template/src/ui/app.html +24 -0
  558. package/template/src/ui/pages/about/page.abide +9 -0
  559. package/template/src/ui/pages/page.abide +22 -0
  560. package/template/test/app.test.ts +30 -0
  561. package/template/tsconfig.json +18 -0
  562. 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
+ }