@abide/abide 0.49.0 → 0.50.1

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 (325) hide show
  1. package/AGENTS.md +564 -437
  2. package/CHANGELOG.md +183 -0
  3. package/README.md +165 -202
  4. package/package.json +14 -5
  5. package/src/abideLsp.ts +5 -6
  6. package/src/abideResolverPlugin.ts +197 -169
  7. package/src/build.ts +79 -56
  8. package/src/buildDisconnected.ts +0 -2
  9. package/src/checkAbide.ts +12 -2
  10. package/src/devEntry.ts +113 -28
  11. package/src/discoveryEntry.ts +3 -2
  12. package/src/lib/bundle/disconnected.abide +69 -73
  13. package/src/lib/bundle/infoPlist.ts +13 -7
  14. package/src/lib/bundle/spawnEmbeddedServer.ts +13 -4
  15. package/src/lib/bundle/waitForServer.ts +6 -1
  16. package/src/lib/mcp/mcpTools.ts +15 -6
  17. package/src/lib/server/error.ts +6 -6
  18. package/src/lib/server/json.ts +17 -3
  19. package/src/lib/server/rpc/defineRpc.ts +58 -24
  20. package/src/lib/server/rpc/parseArgs.ts +98 -13
  21. package/src/lib/server/rpc/resolveRpcJsonSchema.ts +28 -0
  22. package/src/lib/server/rpc/types/RpcHelper.ts +75 -105
  23. package/src/lib/server/rpc/types/RpcRegistryEntry.ts +24 -9
  24. package/src/lib/server/rpc/validationError.ts +1 -1
  25. package/src/lib/server/runtime/DEV_RELOAD_CLIENT_SCRIPT.ts +0 -15
  26. package/src/lib/server/runtime/buildCacheSnapshot.ts +10 -9
  27. package/src/lib/server/runtime/buildInspectorSurface.ts +6 -4
  28. package/src/lib/server/runtime/buildOpenApiSpec.ts +55 -11
  29. package/src/lib/server/runtime/buildPreloadManifest.ts +12 -10
  30. package/src/lib/server/runtime/createAppAssetServer.ts +18 -15
  31. package/src/lib/server/runtime/createAppRouteResolver.ts +145 -0
  32. package/src/lib/server/runtime/createPlumbingRouter.ts +212 -0
  33. package/src/lib/server/runtime/createRouteDispatcher.ts +4 -10
  34. package/src/lib/server/runtime/createServer.ts +120 -324
  35. package/src/lib/server/runtime/createUiPageRenderer.ts +137 -26
  36. package/src/lib/server/runtime/devClientFingerprint.ts +19 -34
  37. package/src/lib/server/runtime/installAmbientScopeStore.ts +30 -9
  38. package/src/lib/server/runtime/logExposedSurfaces.ts +8 -7
  39. package/src/lib/server/runtime/pathStore.ts +15 -0
  40. package/src/lib/server/runtime/registryManifests.ts +1 -1
  41. package/src/lib/server/runtime/renderCellBarrierStore.ts +15 -0
  42. package/src/lib/server/runtime/runWithRequestScope.ts +3 -0
  43. package/src/lib/server/runtime/serializeCacheSnapshot.ts +6 -4
  44. package/src/lib/server/runtime/snapshotEntryFromCache.ts +14 -14
  45. package/src/lib/server/runtime/streamCacheResolutions.ts +14 -7
  46. package/src/lib/server/runtime/streamFromIterator.ts +30 -2
  47. package/src/lib/server/runtime/textResponse.ts +23 -0
  48. package/src/lib/server/runtime/types/DevReloadStamp.ts +6 -10
  49. package/src/lib/server/runtime/types/InspectorCacheEntry.ts +1 -1
  50. package/src/lib/server/runtime/types/RequestStore.ts +27 -0
  51. package/src/lib/server/runtime/warnUnguardedMcp.ts +15 -5
  52. package/src/lib/server/sockets/createSocketDispatcher.ts +9 -11
  53. package/src/lib/server/sockets/defineSocket.ts +5 -4
  54. package/src/lib/server/sse.ts +5 -1
  55. package/src/lib/shared/ASYNC_CELL.ts +5 -0
  56. package/src/lib/shared/DEV_REBUILD_PATH.ts +6 -0
  57. package/src/lib/shared/HTTP_METHODS.ts +6 -0
  58. package/src/lib/shared/HttpError.ts +1 -5
  59. package/src/lib/shared/MCP_PATH.ts +6 -0
  60. package/src/lib/shared/PROXIED_SERVER_SUBDIRS.ts +15 -0
  61. package/src/lib/shared/REF_JSON_TAGS.ts +13 -1
  62. package/src/lib/shared/RPC_SHIM_GLOBALS.ts +9 -0
  63. package/src/lib/shared/activeCacheStore.ts +1 -0
  64. package/src/lib/shared/activePage.ts +1 -0
  65. package/src/lib/shared/buildArtifact.ts +4 -1
  66. package/src/lib/shared/buildSocketOverChannel.ts +4 -3
  67. package/src/lib/shared/bundleGraphFromMetafile.ts +68 -0
  68. package/src/lib/shared/cache.ts +156 -131
  69. package/src/lib/shared/changeAffectsClient.ts +12 -7
  70. package/src/lib/shared/createCacheStore.ts +31 -8
  71. package/src/lib/shared/createLifecycleChannel.ts +7 -1
  72. package/src/lib/shared/createReachable.ts +47 -78
  73. package/src/lib/shared/createRemoteFunction.ts +52 -30
  74. package/src/lib/shared/createRpcServerProgram.ts +820 -0
  75. package/src/lib/shared/decodeRefJson.ts +4 -4
  76. package/src/lib/shared/decodeResponse.ts +2 -2
  77. package/src/lib/shared/decodeWireBody.ts +35 -0
  78. package/src/lib/shared/detectRpcMethod.ts +8 -1
  79. package/src/lib/shared/done.ts +8 -2
  80. package/src/lib/shared/encodeRefJson.ts +4 -4
  81. package/src/lib/shared/encodeWireBody.ts +18 -0
  82. package/src/lib/{server/rpc → shared}/fieldErrorsFromIssues.ts +5 -1
  83. package/src/lib/shared/generateDeclarations.ts +72 -0
  84. package/src/lib/shared/hasSeedableRequest.ts +25 -0
  85. package/src/lib/shared/hydrationWindow.ts +53 -0
  86. package/src/lib/shared/isAsyncCell.ts +11 -0
  87. package/src/lib/shared/isAsyncIterable.ts +8 -0
  88. package/src/lib/shared/isSubscribable.ts +3 -3
  89. package/src/lib/shared/isThenable.ts +9 -0
  90. package/src/lib/shared/jsonSchemaForType.ts +258 -0
  91. package/src/lib/shared/lenientDecode.ts +15 -0
  92. package/src/lib/shared/loadProjectTsConfig.ts +28 -0
  93. package/src/lib/shared/markFrameworkSourcesIgnored.ts +1 -1
  94. package/src/lib/shared/matchRoute.ts +4 -14
  95. package/src/lib/shared/peek.ts +14 -0
  96. package/src/lib/shared/pending.ts +9 -6
  97. package/src/lib/shared/pendingAsyncCellsSlot.ts +13 -0
  98. package/src/lib/shared/prepareRpcModule.ts +91 -93
  99. package/src/lib/shared/prepareSocketModule.ts +3 -2
  100. package/src/lib/shared/probeRegistries.ts +5 -18
  101. package/src/lib/shared/reachable.ts +7 -10
  102. package/src/lib/shared/refresh.ts +12 -2
  103. package/src/lib/shared/refreshing.ts +8 -2
  104. package/src/lib/shared/resolvedCellsSlot.ts +13 -0
  105. package/src/lib/shared/reviveWireField.ts +49 -0
  106. package/src/lib/shared/reviveWireOutput.ts +36 -0
  107. package/src/lib/shared/rpcServerForRoot.ts +25 -0
  108. package/src/lib/shared/scanPages.ts +25 -0
  109. package/src/lib/shared/snapshotShippable.ts +8 -7
  110. package/src/lib/shared/streamedCellsSlot.ts +14 -0
  111. package/src/lib/shared/subscribableFromResponse.ts +4 -4
  112. package/src/lib/shared/subscribableProbes.ts +1 -1
  113. package/src/lib/shared/tailProbeSlot.ts +1 -1
  114. package/src/lib/shared/types/AsyncComputed.ts +20 -0
  115. package/src/lib/shared/types/AsyncState.ts +13 -0
  116. package/src/lib/shared/types/CacheEntry.ts +7 -7
  117. package/src/lib/shared/types/CacheOptions.ts +23 -37
  118. package/src/lib/shared/types/CachePolicy.ts +25 -0
  119. package/src/lib/shared/types/CacheSnapshotEntry.ts +6 -5
  120. package/src/lib/shared/types/ErrorJsonSchemas.ts +8 -0
  121. package/src/lib/shared/types/HttpMethod.ts +3 -1
  122. package/src/lib/shared/types/InputCoercion.ts +17 -0
  123. package/src/lib/shared/types/{Subscribable.ts → NamedAsyncIterable.ts} +1 -1
  124. package/src/lib/shared/types/OutputWirePlan.ts +17 -0
  125. package/src/lib/shared/types/PagesScan.ts +7 -0
  126. package/src/lib/shared/types/PendingAsyncCells.ts +10 -0
  127. package/src/lib/shared/types/RawRemoteFunction.ts +6 -0
  128. package/src/lib/shared/types/RemoteCallable.ts +8 -7
  129. package/src/lib/shared/types/RemoteFunction.ts +16 -15
  130. package/src/lib/shared/types/ResolvedCells.ts +11 -0
  131. package/src/lib/shared/types/ReturnBody.ts +15 -0
  132. package/src/lib/shared/types/RpcBuildStamps.ts +23 -0
  133. package/src/lib/shared/types/RpcErrorGuard.ts +3 -8
  134. package/src/lib/shared/types/Socket.ts +4 -4
  135. package/src/lib/shared/types/SsrPayload.ts +5 -1
  136. package/src/lib/shared/types/StreamPolicy.ts +11 -0
  137. package/src/lib/shared/types/StreamedCells.ts +19 -0
  138. package/src/lib/shared/types/StreamedResolution.ts +24 -6
  139. package/src/lib/shared/types/TailHooks.ts +1 -1
  140. package/src/lib/shared/types/WireKind.ts +11 -0
  141. package/src/lib/shared/validationHttpError.ts +33 -0
  142. package/src/lib/shared/warmSeedKey.ts +16 -0
  143. package/src/lib/shared/wireJsonReplacer.ts +30 -0
  144. package/src/lib/shared/writeRpcDts.ts +11 -1
  145. package/src/lib/shared/writeTestSocketsDts.ts +1 -1
  146. package/src/lib/test/createTestApp.ts +19 -1
  147. package/src/lib/ui/README.md +1 -1
  148. package/src/lib/ui/activePendingCells.ts +14 -0
  149. package/src/lib/ui/compile/BLOCK_KEYWORDS.ts +21 -0
  150. package/src/lib/ui/compile/SSR_ESCAPE.ts +4 -1
  151. package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +35 -8
  152. package/src/lib/ui/compile/abideUiPlugin.ts +19 -2
  153. package/src/lib/ui/compile/analyzeComponent.ts +89 -6
  154. package/src/lib/ui/compile/assignmentTargetNames.ts +54 -0
  155. package/src/lib/ui/compile/asyncInterpolationFields.ts +152 -0
  156. package/src/lib/ui/compile/asyncValuePositionError.ts +21 -0
  157. package/src/lib/ui/compile/asyncValuePositionInterpolations.ts +64 -0
  158. package/src/lib/ui/compile/attrLiftPosition.ts +18 -0
  159. package/src/lib/ui/compile/cachedSourceFile.ts +40 -0
  160. package/src/lib/ui/compile/catchBinding.ts +7 -5
  161. package/src/lib/ui/compile/classifyInterpolationType.ts +46 -0
  162. package/src/lib/ui/compile/collectAbideDiagnostics.ts +96 -0
  163. package/src/lib/ui/compile/compileComponent.ts +19 -3
  164. package/src/lib/ui/compile/compileModule.ts +27 -58
  165. package/src/lib/ui/compile/compileSSR.ts +51 -12
  166. package/src/lib/ui/compile/compileShadow.ts +145 -18
  167. package/src/lib/ui/compile/composeProps.ts +12 -2
  168. package/src/lib/ui/compile/createShadowLanguageService.ts +116 -68
  169. package/src/lib/ui/compile/createShadowProgram.ts +35 -6
  170. package/src/lib/ui/compile/declaredNames.ts +36 -0
  171. package/src/lib/ui/compile/desugarSignals.ts +418 -36
  172. package/src/lib/ui/compile/expressionIsPrefixEvaluable.ts +19 -0
  173. package/src/lib/ui/compile/generateBuild.ts +52 -9
  174. package/src/lib/ui/compile/generateSSR.ts +242 -43
  175. package/src/lib/ui/compile/hoistableAwaits.ts +193 -0
  176. package/src/lib/ui/compile/hoistableChildRenders.ts +112 -0
  177. package/src/lib/ui/compile/interpolatedTemplateLiteral.ts +3 -1
  178. package/src/lib/ui/compile/interpolationClassifierForRoot.ts +37 -0
  179. package/src/lib/ui/compile/isSpuriousAsyncReadDiagnostic.ts +174 -0
  180. package/src/lib/ui/compile/liftAsyncSubExpressions.ts +166 -0
  181. package/src/lib/ui/compile/lowerAsyncInterpolations.ts +92 -0
  182. package/src/lib/ui/compile/lowerContext.ts +10 -0
  183. package/src/lib/ui/compile/lowerDocAccess.ts +26 -23
  184. package/src/lib/ui/compile/lowerScript.ts +40 -4
  185. package/src/lib/ui/compile/nodeAtShadowOffset.ts +28 -0
  186. package/src/lib/ui/compile/parseTemplate.ts +18 -1092
  187. package/src/lib/ui/compile/parseTemplateRecovering.ts +1385 -0
  188. package/src/lib/ui/compile/referencedIdentifiers.ts +35 -0
  189. package/src/lib/ui/compile/renameSignalRefs.ts +21 -22
  190. package/src/lib/ui/compile/seedTypeClassifierForRoot.ts +65 -0
  191. package/src/lib/ui/compile/shadowInterpolationClassifier.ts +47 -0
  192. package/src/lib/ui/compile/sourceFileOptionsSignature.ts +20 -0
  193. package/src/lib/ui/compile/structuralHeadTokens.ts +107 -0
  194. package/src/lib/ui/compile/templateSemanticTokens.ts +21 -0
  195. package/src/lib/ui/compile/templateStartOffset.ts +15 -0
  196. package/src/lib/ui/compile/tryPlan.ts +4 -3
  197. package/src/lib/ui/compile/types/AnalyzedComponent.ts +4 -0
  198. package/src/lib/ui/compile/types/AsyncInterpolationField.ts +19 -0
  199. package/src/lib/ui/compile/types/InterpolationClassifier.ts +12 -0
  200. package/src/lib/ui/compile/types/InterpolationKind.ts +7 -0
  201. package/src/lib/ui/compile/types/ParseDiagnostic.ts +8 -0
  202. package/src/lib/ui/compile/types/SeedTypeClassifier.ts +15 -0
  203. package/src/lib/ui/compile/types/TemplateNode.ts +36 -4
  204. package/src/lib/ui/compile/types/ValuePositionInterpolation.ts +13 -0
  205. package/src/lib/ui/compile/writtenTemplateNames.ts +84 -0
  206. package/src/lib/ui/computed.ts +28 -1
  207. package/src/lib/ui/createScope.ts +35 -68
  208. package/src/lib/ui/dom/anchoredBranch.ts +142 -0
  209. package/src/lib/ui/dom/appendText.ts +8 -3
  210. package/src/lib/ui/dom/awaitBlock.ts +43 -94
  211. package/src/lib/ui/dom/bindProp.ts +22 -0
  212. package/src/lib/ui/dom/bindableProp.ts +47 -0
  213. package/src/lib/ui/dom/cellPending.ts +24 -0
  214. package/src/lib/ui/dom/disposeRange.ts +2 -1
  215. package/src/lib/ui/dom/each.ts +26 -4
  216. package/src/lib/ui/dom/eachAsync.ts +39 -3
  217. package/src/lib/ui/dom/fillBoundary.ts +2 -3
  218. package/src/lib/ui/dom/fillRange.ts +4 -4
  219. package/src/lib/ui/dom/hydrate.ts +6 -12
  220. package/src/lib/ui/dom/isComment.ts +1 -1
  221. package/src/lib/ui/dom/matchingRangeClose.ts +29 -0
  222. package/src/lib/ui/dom/mount.ts +1 -2
  223. package/src/lib/ui/dom/mountChild.ts +9 -40
  224. package/src/lib/ui/dom/mountRange.ts +2 -3
  225. package/src/lib/ui/dom/mountStreamedChild.ts +84 -0
  226. package/src/lib/ui/dom/mountSwappableRange.ts +46 -4
  227. package/src/lib/ui/dom/mutateDocContainer.ts +65 -0
  228. package/src/lib/ui/dom/on.ts +2 -2
  229. package/src/lib/ui/dom/readCell.ts +40 -0
  230. package/src/lib/ui/dom/switchBlock.ts +30 -7
  231. package/src/lib/ui/dom/tryBlock.ts +230 -66
  232. package/src/lib/ui/dom/types/SwitchCase.ts +5 -1
  233. package/src/lib/ui/dom/when.ts +12 -2
  234. package/src/lib/ui/dom/withScope.ts +2 -2
  235. package/src/lib/ui/finalizeStreamedChildren.ts +89 -0
  236. package/src/lib/ui/flight.ts +56 -0
  237. package/src/lib/ui/html.ts +12 -1
  238. package/src/lib/ui/isolateCellBarrier.ts +17 -0
  239. package/src/lib/ui/linked.ts +48 -2
  240. package/src/lib/ui/remoteProxy.ts +84 -159
  241. package/src/lib/ui/renderChain.ts +49 -13
  242. package/src/lib/ui/renderToStream.ts +22 -18
  243. package/src/lib/ui/resumeSeedScript.ts +1 -1
  244. package/src/lib/ui/router.ts +63 -36
  245. package/src/lib/ui/runtime/AsyncCellError.ts +20 -0
  246. package/src/lib/ui/runtime/CELL_SEED.ts +14 -0
  247. package/src/lib/ui/runtime/CURRENT_BOUNDARY.ts +11 -0
  248. package/src/lib/ui/runtime/CURRENT_PATH.ts +29 -0
  249. package/src/lib/ui/runtime/RENDER.ts +10 -7
  250. package/src/lib/ui/runtime/RESUME.ts +4 -4
  251. package/src/lib/ui/runtime/STREAMED_CELLS.ts +57 -0
  252. package/src/lib/ui/runtime/ambientPathBacking.ts +32 -0
  253. package/src/lib/ui/runtime/applyPatchToTree.ts +1 -1
  254. package/src/lib/ui/runtime/blockId.ts +31 -0
  255. package/src/lib/ui/runtime/boundaryFor.ts +11 -0
  256. package/src/lib/ui/runtime/cellBarrierBacking.ts +30 -0
  257. package/src/lib/ui/runtime/createAsyncCell.ts +300 -0
  258. package/src/lib/ui/runtime/createDoc.ts +3 -42
  259. package/src/lib/ui/runtime/createEffectNode.ts +9 -0
  260. package/src/lib/ui/runtime/enterRenderPass.ts +5 -4
  261. package/src/lib/ui/runtime/flushEffects.ts +16 -6
  262. package/src/lib/ui/runtime/isAsyncFunction.ts +14 -0
  263. package/src/lib/ui/runtime/nextBlockId.ts +8 -7
  264. package/src/lib/ui/runtime/renderPath.ts +16 -0
  265. package/src/lib/ui/runtime/types/Boundary.ts +9 -0
  266. package/src/lib/ui/runtime/types/RenderContext.ts +9 -7
  267. package/src/lib/ui/runtime/types/SsrRender.ts +9 -2
  268. package/src/lib/ui/runtime/types/UiComponent.ts +1 -5
  269. package/src/lib/ui/runtime/withOptionalPath.ts +8 -0
  270. package/src/lib/ui/runtime/withPath.ts +16 -0
  271. package/src/lib/ui/runtime/withPathFrom.ts +20 -0
  272. package/src/lib/ui/seedStreamedResolution.ts +31 -6
  273. package/src/lib/ui/settleAsyncCells.ts +24 -0
  274. package/src/lib/ui/socketProxy.ts +1 -1
  275. package/src/lib/ui/startClient.ts +16 -31
  276. package/src/lib/ui/trackedComputed.ts +68 -0
  277. package/src/lib/ui/types/Scope.ts +8 -24
  278. package/src/lib/ui/watch.ts +3 -3
  279. package/src/serverEntry.ts +14 -0
  280. package/template/src/server/rpc/getHello.ts +15 -13
  281. package/template/test/app.test.ts +1 -1
  282. package/src/lib/server/runtime/devHotModuleResponse.ts +0 -41
  283. package/src/lib/shared/DEV_HOT_PREFIX.ts +0 -7
  284. package/src/lib/shared/UNREACHABLE_STATUSES.ts +0 -13
  285. package/src/lib/shared/hasReplayableRequest.ts +0 -17
  286. package/src/lib/shared/hydratingSlot.ts +0 -12
  287. package/src/lib/shared/outboxProbeSlot.ts +0 -20
  288. package/src/lib/shared/types/Outbox.ts +0 -9
  289. package/src/lib/shared/types/OutboxEntry.ts +0 -27
  290. package/src/lib/shared/types/SmartReadOptions.ts +0 -36
  291. package/src/lib/shared/wakeHydrationPeeks.ts +0 -20
  292. package/src/lib/ui/COMPONENT_WRAPPER_PREFIX.ts +0 -5
  293. package/src/lib/ui/compile/REACTIVE_CALLEES.ts +0 -11
  294. package/src/lib/ui/compile/assertRuntimeHelpersBound.ts +0 -80
  295. package/src/lib/ui/compile/markupTokens.ts +0 -313
  296. package/src/lib/ui/compile/structuralBlockTokens.ts +0 -100
  297. package/src/lib/ui/dom/applyResolved.ts +0 -87
  298. package/src/lib/ui/dom/mutateDocArray.ts +0 -38
  299. package/src/lib/ui/dom/scopeLabel.ts +0 -21
  300. package/src/lib/ui/dom/text.ts +0 -20
  301. package/src/lib/ui/history.ts +0 -108
  302. package/src/lib/ui/installHotBridge.ts +0 -95
  303. package/src/lib/ui/installInspectorBridge.ts +0 -140
  304. package/src/lib/ui/outbox.ts +0 -35
  305. package/src/lib/ui/persist.ts +0 -115
  306. package/src/lib/ui/rpcOutbox/createOutboxQueue.ts +0 -281
  307. package/src/lib/ui/rpcOutbox/outboxRegistry.ts +0 -69
  308. package/src/lib/ui/runtime/PATCH_BUS.ts +0 -28
  309. package/src/lib/ui/runtime/captureModelDoc.ts +0 -43
  310. package/src/lib/ui/runtime/hotInstances.ts +0 -10
  311. package/src/lib/ui/runtime/hotReloadEnabled.ts +0 -8
  312. package/src/lib/ui/runtime/hotReplace.ts +0 -38
  313. package/src/lib/ui/runtime/liveScopes.ts +0 -15
  314. package/src/lib/ui/runtime/localStoragePersistence.ts +0 -47
  315. package/src/lib/ui/runtime/registerHotInstance.ts +0 -23
  316. package/src/lib/ui/runtime/seedModelDoc.ts +0 -26
  317. package/src/lib/ui/runtime/types/HotInstance.ts +0 -22
  318. package/src/lib/ui/runtime/types/PatchEvent.ts +0 -16
  319. package/src/lib/ui/seedResolved.ts +0 -28
  320. package/src/lib/ui/sync.ts +0 -48
  321. package/src/lib/ui/types/History.ts +0 -14
  322. package/src/lib/ui/types/PersistHandle.ts +0 -11
  323. package/src/lib/ui/types/PersistenceStore.ts +0 -12
  324. package/src/lib/ui/types/ResolvedFrame.ts +0 -15
  325. package/src/lib/ui/types/SyncTransport.ts +0 -13
@@ -4,13 +4,14 @@ import { cacheStores } from './cacheStores.ts'
4
4
  import { decodeResponse } from './decodeResponse.ts'
5
5
  import { getRemoteMeta } from './getRemoteMeta.ts'
6
6
  import { HttpError } from './HttpError.ts'
7
- import { hydratingSlot } from './hydratingSlot.ts'
7
+ import { hydrationWindow } from './hydrationWindow.ts'
8
8
  import { invalidateEvent } from './invalidateEvent.ts'
9
9
  import { invalidateTripwire } from './invalidateTripwire.ts'
10
10
  import { keyForRemoteCall } from './keyForRemoteCall.ts'
11
11
  import { producerKey } from './producerKey.ts'
12
12
  import { REMOTE_FUNCTION } from './REMOTE_FUNCTION.ts'
13
13
  import { REPLAYABLE_METHODS } from './REPLAYABLE_METHODS.ts'
14
+ import { reviveWireOutput } from './reviveWireOutput.ts'
14
15
  import { rpcErrorRegistry } from './rpcErrorRegistry.ts'
15
16
  import { SocketDisconnectedError } from './SocketDisconnectedError.ts'
16
17
  import { selectorMatcher } from './selectorMatcher.ts'
@@ -21,11 +22,13 @@ import { toTagSet } from './toTagSet.ts'
21
22
  import type { CacheEntry } from './types/CacheEntry.ts'
22
23
  import type { CacheOnContext } from './types/CacheOnContext.ts'
23
24
  import type { CacheOptions } from './types/CacheOptions.ts'
25
+ import type { CachePolicy } from './types/CachePolicy.ts'
24
26
  import type { CacheSelector } from './types/CacheSelector.ts'
25
27
  import type { CacheStore } from './types/CacheStore.ts'
28
+ import type { NamedAsyncIterable } from './types/NamedAsyncIterable.ts'
29
+ import type { OutputWirePlan } from './types/OutputWirePlan.ts'
26
30
  import type { RawRemoteFunction } from './types/RawRemoteFunction.ts'
27
31
  import type { RemoteFunction } from './types/RemoteFunction.ts'
28
- import type { Subscribable } from './types/Subscribable.ts'
29
32
  import { withCacheManaged } from './withCacheManaged.ts'
30
33
 
31
34
  type AnyRemote<Args, Return> = RemoteFunction<Args, Return> | RawRemoteFunction<Args>
@@ -58,28 +61,34 @@ function recordRead(sink: CacheStore, key: string, existing: CacheEntry | undefi
58
61
  }
59
62
 
60
63
  /*
61
- Reads a call through a cache store. `cache(fn, args?, options?)` checks the store
62
- for a prior entry and returns a shared promise on hit, or invokes `fn` once and
63
- stores its promise on miss — a direct read-through call, not a curried invoker.
64
- Args lead (the common refinement); options trail in a fixed final position so
65
- they can't collide with arg shapes. ttl = 0 → dedupe only; ttl > 0 → entry
66
- expires `ttl` ms after the promise resolves. Omitted ttl → forever for a
67
- producer and for a remote call on the client; a remote call on the server with
68
- neither ttl nor swr stated defaults to 0 (coalesce-only, see CacheOptions).
64
+ Reads a call through a cache store. `cache(fn, args?)` checks the store for a
65
+ prior entry and returns a shared promise on hit, or invokes `fn` once and stores
66
+ its promise on miss — a direct read-through call, not a curried invoker. Args
67
+ lead (the common refinement).
69
68
 
70
- Coalescing is always on: identical in-flight calls share one flight, so
71
- `cache(createPost, args, { ttl: 0 })` is the mutation idiom — double-submit
72
- coalescing and pending() visibility with nothing retained beyond the store's
73
- atomic unit (the whole request on the server: one render, one effect; the
69
+ A REMOTE rpc carries no call-site options (ADR-0020): its cache policy lives on
70
+ the endpoint definition (`GET(fn, { cache: {...} })`) and readThrough reads it as
71
+ the bottom layer. A plain PRODUCER has no endpoint, so it keeps a trailing
72
+ `options?: CacheOptions` (ttl/tags/throttle/debounce/shared) its only policy
73
+ home. ttl = 0 → dedupe only; ttl > 0 → entry expires `ttl` ms after the promise
74
+ resolves. Omitted ttl → retained for the store's lifetime: a producer or a remote
75
+ READ is kept as long as its store lives (the request on the server, the tab on the
76
+ client), while a remote WRITE coalesces only (ttl 0 — the mutation idiom). A remote
77
+ read made outside any request scope falls back to the process store and coalesces
78
+ only too, so a scopeless read can't leak forever. See CachePolicy.
79
+
80
+ Coalescing is always on: identical in-flight calls share one flight — double-
81
+ submit coalescing and pending() visibility with nothing retained beyond the
82
+ store's atomic unit (the whole request on the server: one render, one effect; the
74
83
  in-flight window in the tab). Caching is the retention `ttl` adds on top.
75
84
 
76
85
  `fn` is either a remote function (a GET/POST/... helper) or a plain producer
77
86
  returning a Promise:
78
87
 
79
- cache(getPost, { id }) // → Promise<Post> (decoded body)
88
+ cache(getPost, { id }) // → Promise<Post> (decoded body; policy on the endpoint)
80
89
  cache(getPost.raw, { id }) // → Promise<Response> (raw escape hatch)
81
90
  cache(fetchRates) // → Promise<Rates> (plain producer, no args)
82
- cache(createPost, body, { ttl: 0 }) // options trail; no-arg-with-options: cache(fn, undefined, opts)
91
+ cache(computeReport, input, { ttl: 60_000 }) // producer options trail
83
92
 
84
93
  Remote calls key on fn.method + fn.url + args and store the underlying Response
85
94
  (the decoded view is derived on the way out for the non-raw variant; both share
@@ -89,11 +98,12 @@ new reference every call and never does; a warning fires once per such call
89
98
  site), and the promise is stored and handed back as-is (no Response, no decode,
90
99
  no SSR snapshot).
91
100
 
92
- `options.shared` puts the entry in the process-level store instead of the
93
- request-scoped one, so a value computed in one request is reused by later
94
- requests — the memoise-an-external-endpoint case. Default (omitted) is
95
- request-scoped on the server, which keeps per-user data from leaking across
96
- requests; on the client there is one tab store either way, so it is a no-op.
101
+ `options.shared` (producer) / `cache.shared` (endpoint) puts the entry in the
102
+ process-level store instead of the request-scoped one, so a value computed in one
103
+ request is reused by later requests — the memoise-an-external-endpoint case.
104
+ Default (omitted) is request-scoped on the server, which keeps per-user data from
105
+ leaking across requests; on the client there is one tab store either way, so it is
106
+ a no-op.
97
107
 
98
108
  Reactivity is implicit: the read calls `store.subscribe(key)`, which registers
99
109
  the surrounding state.computed() / effect() scope. Invalidating the key then
@@ -132,16 +142,8 @@ export function cache<Args>(
132
142
  fn: RemoteFunction<Args, AsyncIterable<unknown>>,
133
143
  ...rest: never[]
134
144
  ): never
135
- export function cache<Args, Return>(
136
- fn: RemoteFunction<Args, Return>,
137
- args?: Args,
138
- options?: CacheOptions,
139
- ): Promise<Return>
140
- export function cache<Args>(
141
- fn: RawRemoteFunction<Args>,
142
- args?: Args,
143
- options?: CacheOptions,
144
- ): Promise<Response>
145
+ export function cache<Args, Return>(fn: RemoteFunction<Args, Return>, args?: Args): Promise<Return>
146
+ export function cache<Args>(fn: RawRemoteFunction<Args>, args?: Args): Promise<Response>
145
147
  export function cache<Args, Return>(
146
148
  fn: Producer<Args, Return>,
147
149
  args?: Args,
@@ -152,7 +154,19 @@ export function cache<Args, Return>(
152
154
  args?: Args,
153
155
  options?: CacheOptions,
154
156
  ): Promise<Return | Response> {
155
- return readThrough(fn, args, options, false)
157
+ const result = readThrough(fn, args, options, false)
158
+ /* Revive the decoded body's structured fields (Set/Map/Date/bigint) so `cache(fn, args)`
159
+ matches the bare `fn(args)` read (createRemoteFunction.callable revives there) — without
160
+ this the two read APIs disagree for a typed field, and both violate the declared Return.
161
+ A producer / a raw `Response` carry no plan and pass through untouched; the decoded value is
162
+ call-private (fresh decode or cloned warm value), so revive mutates it in place. */
163
+ const plan = (fn as { outputWirePlan?: OutputWirePlan }).outputWirePlan
164
+ if (plan === undefined) {
165
+ return result
166
+ }
167
+ return result.then((value) =>
168
+ value instanceof Response ? value : (reviveWireOutput(value, plan) as Return),
169
+ )
156
170
  }
157
171
 
158
172
  /*
@@ -160,15 +174,12 @@ The smart bare rpc call routes here (createRemoteFunction.callable → cache.rea
160
174
  identical to cache() except a replayable read gets unconditional SWR retention —
161
175
  its value is kept for display regardless of ttl, and ttl marks a staleness
162
176
  deadline (the next read past it revalidates in the background) instead of driving
163
- eviction. Always a decoded RemoteFunction read (the callable carries .raw), so it
164
- returns Promise<Return>.
177
+ eviction. No call-site options (ADR-0020): all policy is read off the endpoint
178
+ (fn.cache). Always a decoded RemoteFunction read (the callable carries .raw), so
179
+ it returns Promise<Return>.
165
180
  */
166
- function smartRead<Args, Return>(
167
- fn: RemoteFunction<Args, Return>,
168
- args?: Args,
169
- options?: CacheOptions,
170
- ): Promise<Return> {
171
- return readThrough(fn, args, options, true) as Promise<Return>
181
+ function smartRead<Args, Return>(fn: RemoteFunction<Args, Return>, args?: Args): Promise<Return> {
182
+ return readThrough(fn, args, undefined, true) as Promise<Return>
172
183
  }
173
184
  cache.read = smartRead
174
185
 
@@ -177,6 +188,8 @@ The shared read-through core. `smart` marks the smart bare call, which enables
177
188
  unconditional SWR retention for a replayable read — but only on the client (see
178
189
  smartRead / entry.retain and the `retain` computation below); the public cache()
179
190
  passes false, keeping its explicit drop-on-ttl / drop-on-invalidate old surface.
191
+ A remote's policy is read off the endpoint (fn.cache — ADR-0020); a producer's
192
+ policy is its explicit cache() `options`.
180
193
  */
181
194
  function readThrough<Args, Return>(
182
195
  fn: AnyRemote<Args, Return> | Producer<Args, Return>,
@@ -201,30 +214,42 @@ function readThrough<Args, Return>(
201
214
  : (fn as RemoteFunction<Args, Return>).raw
202
215
  const method = isRemote ? (rawFn as RawRemoteFunction<Args>).method : undefined
203
216
  const replayable = method !== undefined && REPLAYABLE_METHODS.has(method.toUpperCase())
204
- validatePolicy(options, method, smart)
217
+ /* Remote policy is endpoint-declared — the bottom (and only) layer; a producer has no
218
+ endpoint, so its explicit cache() options stand in. */
219
+ const policy = isRemote ? resolveEndpointCache(fn as AnyRemote<Args, Return>, args) : options
205
220
  /* SWR retention is a client concern (the tab store lives; revalidation is
206
221
  visible). On the server there is no live UI to hold stale, so a smart read
207
- never retains — it coalesces only. */
208
- const retain = smart && replayable && options?.swr !== false && typeof window !== 'undefined'
222
+ never retains — it coalesces only. Unconditional for a replayable read now
223
+ (no swr toggle to opt out of). */
224
+ const retain = smart && replayable && typeof window !== 'undefined'
225
+ /*
226
+ ttl defaults to Infinity — an entry is retained for as long as its STORE lives, and
227
+ the store's lifetime (not ttl) is what ends a read: the request-scoped store on the
228
+ server (so a non-shared read dies with the request regardless of ttl), the tab store
229
+ on the client (until invalidate/refresh). `shared` selects the process store, where
230
+ the default Infinity memoises across requests and an explicit ttl bounds it — so
231
+ `shared` alone is "memoise", not a store-selector that retains nothing.
232
+ */
233
+ const store = policy?.shared ? sharedCacheStore() : activeCacheStore()
209
234
  /*
210
- ttl defaults to 0 (coalesce-only) on the server for any remote read/write, and
211
- on the client for a smart write — in both cases only when the caller stated
212
- neither ttl nor swr. The server default makes retention opt-in (via an explicit
213
- ttl, paired with `shared` to survive the request). An explicit swr opts out so
214
- it does not trip validatePolicy's "swr + ttl:0" throw; server-side swr is a
215
- client-only concept with no added server guarantee.
235
+ Two cases force coalesce-only (ttl 0) when the caller stated no ttl nothing retained
236
+ beyond the in-flight window:
237
+ - a write (non-replayable remote): dropped on settle so a re-submit of the same body
238
+ isn't blocked by a retained entry (the mutation idiom);
239
+ - a read that resolved to the process store WITHOUT `shared` — a server read outside any
240
+ request scope, where activeCacheStore() falls back to the shared store. Nothing bounds
241
+ its lifetime there, so retaining it would leak forever.
242
+ Both apply on either side and however the call is made.
216
243
  */
217
- const serverTtlZero =
244
+ const coalesceOnly =
218
245
  isRemote &&
219
- typeof window === 'undefined' &&
220
- options?.ttl === undefined &&
221
- options?.swr === undefined
222
- const smartWriteTtlZero = smart && isRemote && !replayable && options?.ttl === undefined
223
- const effectiveOptions = serverTtlZero || smartWriteTtlZero ? { ...options, ttl: 0 } : options
246
+ policy?.ttl === undefined &&
247
+ (!replayable ||
248
+ (!policy?.shared && typeof window === 'undefined' && store === sharedCacheStore()))
249
+ const effectiveOptions = coalesceOnly ? { ...policy, ttl: 0 } : policy
224
250
  if (!isRemote) {
225
251
  warnAnonymousProducer(fn as Producer<Args, Return>)
226
252
  }
227
- const store = effectiveOptions?.shared ? sharedCacheStore() : activeCacheStore()
228
253
  if (!isRemote) {
229
254
  return invokeProducer(store, fn as Producer<Args, Return>, args, effectiveOptions)
230
255
  }
@@ -318,72 +343,48 @@ function cloneWarmValue(value: unknown): unknown {
318
343
  }
319
344
 
320
345
  /*
321
- Normalises the refetch window, or undefined when off. Precedence: an explicit
322
- `swr` object carries its own throttle/debounce; `swr: true` (or `defaultOn`) uses
323
- the root `throttle`/`debounce` window if present, else `{}` (fire immediately).
324
- `swr: false` is an explicit opt-out and wins over `defaultOn`. `defaultOn` is the
325
- smart bare call's unconditional SWR for replayable reads — it turns the window on
326
- without an `swr` toggle. Collapsing all of this here lets every downstream site
327
- treat "is SWR on" as a single defined/undefined check.
346
+ Resolves an rpc endpoint's declared cache policy (ADR-0020) into a plain
347
+ CacheOptions for the read-through machinery, evaluating the `tags` function form
348
+ against this call's args. Both remote variants (decoded + raw) carry `.cache`, so
349
+ either can be passed. Undefined when the endpoint declared no policy (method
350
+ defaults still apply below).
328
351
  */
329
- function swrWindow(
330
- options: CacheOptions | undefined,
331
- defaultOn: boolean,
332
- ): { throttle?: number; debounce?: number } | undefined {
333
- const swr = options?.swr
334
- if (swr === false) {
352
+ function resolveEndpointCache<Args, Return>(
353
+ fn: AnyRemote<Args, Return>,
354
+ args: Args | undefined,
355
+ ): CacheOptions | undefined {
356
+ const policy = (fn as { cache?: CachePolicy<Args> }).cache
357
+ if (policy === undefined) {
335
358
  return undefined
336
359
  }
337
- const root =
338
- options?.throttle !== undefined || options?.debounce !== undefined
339
- ? { throttle: options?.throttle, debounce: options?.debounce }
340
- : undefined
341
- /* Explicit window object (swr: { throttle | debounce }). */
342
- if (swr !== undefined && swr !== true) {
343
- return swr
360
+ const tags = typeof policy.tags === 'function' ? policy.tags(args as Args) : policy.tags
361
+ return {
362
+ ttl: policy.ttl,
363
+ tags,
364
+ shared: policy.shared,
365
+ throttle: policy.throttle,
366
+ debounce: policy.debounce,
344
367
  }
345
- if (swr === true || defaultOn) {
346
- return root ?? {}
347
- }
348
- return undefined
349
368
  }
350
369
 
351
370
  /*
352
- Guards impossible option combinations at wrap time, where the call site is on
353
- the stack. `swr` declares "this call is safe to re-run unprompted", so a
354
- non-replayable remote method (a write) must never carry it replaying a write
355
- through the invalidation grammar would be a state change disguised as a
356
- refresh. Producers are opaque (no method to check); the same contract is on
357
- the caller there. ttl: 0 retains nothing, so there is nothing to revalidate;
358
- and the two coalescing windows are exclusive by construction.
371
+ The keep-stale-and-revalidate refetch window, or undefined when the entry should
372
+ hard-drop on invalidate instead. Present when the smart bare call retains a
373
+ replayable read (`defaultOn`, SWR unconditional the window is just the endpoint's
374
+ throttle/debounce, or `{}` = fire immediately) OR when a policy explicitly declares
375
+ a throttle/debounce window (producer options / endpoint cache). Collapsing this here
376
+ lets every downstream site treat "is a refetch window armed" as a single
377
+ defined/undefined check.
359
378
  */
360
- function validatePolicy(
379
+ function refetchWindow(
361
380
  options: CacheOptions | undefined,
362
- method: string | undefined,
363
- smart: boolean,
364
- ): void {
365
- const replayable = method !== undefined && REPLAYABLE_METHODS.has(method.toUpperCase())
366
- const policy = swrWindow(options, smart && replayable)
367
- if (!policy) {
368
- return
369
- }
370
- if (policy.throttle !== undefined && policy.debounce !== undefined) {
371
- throw new Error('[abide] cache(): set throttle or debounce, not both')
372
- }
373
- /* An EXPLICIT swr with ttl: 0 is the old error — nothing retained to revalidate.
374
- The smart read's implicit SWR retains unconditionally, so ttl: 0 is fine there. */
375
- const explicitSwr =
376
- options?.swr === true || (typeof options?.swr === 'object' && options?.swr !== null)
377
- if (explicitSwr && options?.ttl === 0) {
378
- throw new Error(
379
- '[abide] cache(): swr requires retention — ttl: 0 keeps nothing to revalidate',
380
- )
381
- }
382
- if (method !== undefined && !replayable) {
383
- throw new Error(
384
- `[abide] cache(): swr re-runs the call unprompted — ${method.toUpperCase()} is a write and must not be replayed`,
385
- )
381
+ defaultOn: boolean,
382
+ ): { throttle?: number; debounce?: number } | undefined {
383
+ const hasWindow = options?.throttle !== undefined || options?.debounce !== undefined
384
+ if (defaultOn || hasWindow) {
385
+ return { throttle: options?.throttle, debounce: options?.debounce }
386
386
  }
387
+ return undefined
387
388
  }
388
389
 
389
390
  /*
@@ -498,9 +499,9 @@ function registerEntry(
498
499
  retain: boolean,
499
500
  ): CacheEntry {
500
501
  const ttl = options?.ttl
501
- /* Capture the refetch thunk + window when swr was asked for OR this is a smart
502
- retained read (SWR unconditional). */
503
- const policy = swrWindow(options, retain)
502
+ /* Capture the refetch thunk + window when a smart retained read (SWR unconditional)
503
+ or an explicit throttle/debounce window is present. */
504
+ const policy = refetchWindow(options, retain)
504
505
  const invalidation = policy
505
506
  ? { refetch, throttle: policy.throttle, debounce: policy.debounce }
506
507
  : undefined
@@ -748,7 +749,7 @@ Invalidates every entry matching the selector (see selectorMatcher) across both
748
749
  the request/tab store and the process-level store, and notifies readers.
749
750
  `args` narrows a fn selector to exactly that call's entry — derived through
750
751
  the same encoders the read path uses, so other args variants stay warm. An entry
751
- with an swr policy is kept and its refetch coalesced (stale served
752
+ with a refetch policy is kept and its refetch coalesced (stale served
752
753
  until it resolves); every other match is dropped so the next read refetches —
753
754
  its key recorded in pendingRefresh so that read reports as a reload (refreshing())
754
755
  rather than a first-ever load. An empty or unmatched selector is a no-op on the
@@ -821,6 +822,12 @@ blanks. This is the old invalidate reborn as *refetch*: because SWR retains the
821
822
  value, there is no "invalidate that drops to pending" for the smart call. Follows
822
823
  invalidate's exact selector grammar (fn / fn+args / { tags } / bare = all).
823
824
 
825
+ Only a match with a live reader refetches: refresh keeps the stale value visible
826
+ until the fresh one swaps in, and with no reader on screen there is nothing to
827
+ swap into — so a match whose reactive scope has torn down ("no longer has
828
+ listeners") drops instead of firing a wasted network refetch, and the next read
829
+ reloads it fresh.
830
+
824
831
  A smart-read match already carries an invalidation policy, so it routes straight
825
832
  through scheduleInvalidationRefetch (throttle/debounce-honouring). A policy-less
826
833
  remote match (an explicit cache() entry, or a hydrated one) gets a refetch armed
@@ -842,6 +849,16 @@ function refresh<Args, Return>(arg?: CacheSelector<Args, Return>, args?: Args):
842
849
  continue
843
850
  }
844
851
  matched.push(entry.key)
852
+ /* No live reader is holding this key — its reactive scope has torn down (or
853
+ it was never read reactively and already dropped). A refresh has no
854
+ on-screen value to keep-stale-and-swap, so firing the network now is
855
+ wasted work that may be stale again before anything shows it. Drop the
856
+ entry so the next read reloads fresh instead — the same lazy path a
857
+ reader-less invalidate() takes. */
858
+ if (!store.hasReader(entry.key)) {
859
+ store.entries.delete(entry.key)
860
+ continue
861
+ }
845
862
  /* Arm a refetch on the fly for a policy-less remote entry by replaying its
846
863
  stored Request — so a refresh always refetches-and-swaps, never blanks. */
847
864
  if (entry.invalidation === undefined && entry.request !== undefined) {
@@ -851,12 +868,11 @@ function refresh<Args, Return>(arg?: CacheSelector<Args, Return>, args?: Args):
851
868
  if (entry.invalidation !== undefined) {
852
869
  scheduleInvalidationRefetch(store, entry)
853
870
  } else {
854
- /* No policy, no request (a producer never cached with swr): drop so the
855
- next read reloads, flagged a reload if a reader is holding it. */
871
+ /* No policy, no request (a producer never cached with a refetch window): drop so the
872
+ next read reloads. A reader is holding it (checked above), so flag the
873
+ next read a reload (refreshing()). */
856
874
  store.entries.delete(entry.key)
857
- if (store.hasReader(entry.key)) {
858
- store.pendingRefresh.add(entry.key)
859
- }
875
+ store.pendingRefresh.add(entry.key)
860
876
  affected.push(entry.key)
861
877
  }
862
878
  }
@@ -921,7 +937,8 @@ function peek<Args, Return>(
921
937
  ? (fn as RemoteFunction<Args, Return>).raw
922
938
  : (fn as RawRemoteFunction<Args>)
923
939
  const key = isRemote
924
- ? keyForRemoteCall(rawFn!.method, rawFn!.url, args)
940
+ ? // biome-ignore lint/style/noNonNullAssertion: rawFn is undefined only when !isRemote, so this branch has it
941
+ keyForRemoteCall(rawFn!.method, rawFn!.url, args)
925
942
  : producerKey(fn as Producer<Args, Return>, args)
926
943
  const active = activeCacheStore()
927
944
  active.subscribe(key)
@@ -940,11 +957,19 @@ function peek<Args, Return>(
940
957
  uniformly undefined and the SSR render shows the fallback. Returning a snapshot-
941
958
  seeded warm value here — mid-hydration — would diverge from that server text and
942
959
  corrupt the claimed text node. Withhold it until the pass ends; the trackLifecycle
943
- tap above lets wakeHydrationPeeks re-run this scope on the congruent value. */
944
- if (hydratingSlot.active) {
960
+ tap above lets hydrationWindow.wake re-run this scope on the congruent value. */
961
+ if (hydrationWindow.active) {
945
962
  return undefined
946
963
  }
947
- return cloneWarmValue(entry.value) as Return
964
+ /* Revive structured fields after cloning: the clone is the honest-JSON wire form, and revive
965
+ turns a wire array into its declared Set/Map and an ISO string into a Date — so peek matches
966
+ fn(args) / cache(fn, args). A raw or producer read carries no plan → the clone passes through
967
+ untouched (revive is a no-op on an undefined plan). */
968
+ const plan =
969
+ isRemote && 'raw' in fn
970
+ ? (fn as { outputWirePlan?: OutputWirePlan }).outputWirePlan
971
+ : undefined
972
+ return reviveWireOutput(cloneWarmValue(entry.value), plan) as Return
948
973
  }
949
974
 
950
975
  cache.peek = peek
@@ -978,7 +1003,7 @@ function applyPatch(
978
1003
  }
979
1004
 
980
1005
  /*
981
- Event-driven cache maintenance: subscribes to a Subscribable (socket or rpc
1006
+ Event-driven cache maintenance: subscribes to a NamedAsyncIterable (socket or rpc
982
1007
  stream) and runs `handler` once per frame — the declarative home for "this
983
1008
  socket event stales that cached data", replacing the hand-rolled $effect +
984
1009
  tail() + edge-detection pattern. Bare iteration means live frames only
@@ -1005,7 +1030,7 @@ already seeded the cache. Dispose aborts `signal`, stops delivery, and
1005
1030
  closes the subscription.
1006
1031
  */
1007
1032
  function on<T>(
1008
- source: Subscribable<T>,
1033
+ source: NamedAsyncIterable<T>,
1009
1034
  handler: (frame: T, context: CacheOnContext) => void | Promise<void>,
1010
1035
  ): () => void {
1011
1036
  if (typeof window === 'undefined') {
@@ -1074,7 +1099,7 @@ function on<T>(
1074
1099
  cache.on = on
1075
1100
 
1076
1101
  /*
1077
- Schedules a coalesced refetch per the entry's swr policy. No window (swr: true):
1102
+ Schedules a coalesced refetch per the entry's refetch policy. No window:
1078
1103
  fire immediately (throttle defaults to 0, so the leading-edge branch always
1079
1104
  takes). debounce: (re)arm a timer that fires after N ms of quiet. throttle: fire
1080
1105
  on the leading edge when a full window has elapsed since the last fire, else arm
@@ -1162,7 +1187,7 @@ function fireRefetch(store: CacheStore, entry: CacheEntry): void {
1162
1187
  entry keeps its original expiresAt and would read as stale (or be evicted
1163
1188
  at the old deadline) despite holding fresh data. Mirrors registerEntry's
1164
1189
  settle path: a retained smart read re-stamps the staleness deadline (the
1165
- next read past it revalidates), an explicit-swr entry re-arms the eviction
1190
+ next read past it revalidates), a windowed producer entry re-arms the eviction
1166
1191
  clock. */
1167
1192
  if (entry.ttl !== undefined && entry.ttl !== 0) {
1168
1193
  if (entry.retain) {
@@ -1269,7 +1294,7 @@ function attachPolicy(
1269
1294
  if (defaultOn) {
1270
1295
  entry.retain = true
1271
1296
  }
1272
- const policy = swrWindow(options, defaultOn)
1297
+ const policy = refetchWindow(options, defaultOn)
1273
1298
  if (entry.invalidation || !policy) {
1274
1299
  return
1275
1300
  }
@@ -1284,7 +1309,7 @@ fire and the pending()/refreshing() probes re-derive; only the keys in `emitted`
1284
1309
  had their VISIBLE value change, so the 'invalidate' event fires and the reading
1285
1310
  scope re-reads. The two sets diverge by design — a refetch start marks but does
1286
1311
  not emit (the stale value is still on screen), an invalidate-drop does both, an
1287
- swr invalidate marks the whole match set while emitting only the dropped subset.
1312
+ a keep-stale (refetch-policy) invalidate marks the whole match set while emitting only the dropped subset.
1288
1313
  The trailing store-wide mark fires even when nothing matched, so a bare probe
1289
1314
  still re-derives (to the same value); marks coalesce per microtask, so a key in
1290
1315
  both lists is not double work.
@@ -1,3 +1,5 @@
1
+ import { PROXIED_SERVER_SUBDIRS } from './PROXIED_SERVER_SUBDIRS.ts'
2
+
1
3
  /*
2
4
  Classifies a watcher change (a path relative to the project's `src/`, with
3
5
  either OS separator) by whether it can alter the client bundle. The dev
@@ -9,8 +11,8 @@ Server/MCP-only — client unaffected, skip the client rebuild:
9
11
  path (rpc url) and the `<METHOD>(...)` wrapper, not the
10
12
  handler body; the rpc manifest is server-only. A body
11
13
  edit produces an identical stub. (Risk: changing the
12
- method, the export name, or `outbox: true` does change
13
- the stub — those need a manual full rebuild.)
14
+ method or the export name does change the stub — those
15
+ need a manual full rebuild.)
14
16
  - `server/sockets/**` — name-only `socketProxy` stubs derived from the file
15
17
  path; socket opts are server-side. Body edits don't
16
18
  change the stub.
@@ -26,10 +28,13 @@ full rebuild rather than risking a stale client.
26
28
  */
27
29
  export function changeAffectsClient(relativePath: string): boolean {
28
30
  const path = relativePath.split('\\').join('/')
29
- const serverOnly =
30
- path.startsWith('server/rpc/') ||
31
- path.startsWith('server/sockets/') ||
32
- path.startsWith('mcp/') ||
33
- path === 'server/config.ts'
31
+ /* The proxied server dirs (rpc/sockets) ship path-derived stubs, so a body edit can't change the
32
+ client bundle. Read from the shared PROXIED_SERVER_SUBDIRS authority so this stays in lockstep
33
+ with the resolver's side-classifier. mcp/** and server/config.ts are additional server-only
34
+ skips specific to this classifier (not proxied — never imported by the client at all). */
35
+ const proxiedServerOnly = PROXIED_SERVER_SUBDIRS.some((subdir) =>
36
+ path.startsWith(`server/${subdir}/`),
37
+ )
38
+ const serverOnly = proxiedServerOnly || path.startsWith('mcp/') || path === 'server/config.ts'
34
39
  return !serverOnly
35
40
  }
@@ -1,3 +1,4 @@
1
+ import { REACTIVE_CONTEXT } from '../ui/runtime/REACTIVE_CONTEXT.ts'
1
2
  import { createLifecycleChannel } from './createLifecycleChannel.ts'
2
3
  import { createSubscriber } from './createSubscriber.ts'
3
4
  import { keyMatchesPrefix } from './keyMatchesPrefix.ts'
@@ -28,6 +29,16 @@ export function createCacheStore(): CacheStore {
28
29
  const pendingRefresh = new Set<string>()
29
30
 
30
31
  function subscribe(key: string): void {
32
+ /* An untracked read (an event handler, a setup-time `await getFoo()`, any
33
+ server render) subscribes to nothing: `track` inside createSubscriber no-ops,
34
+ node.subsHead stays empty, and the resource never opens. Skip the bookkeeping
35
+ too — otherwise a bare read would pin an entry in `subscribers` for the tab's
36
+ lifetime, growing the map and leaving hasReader() over-reporting a reader that
37
+ isn't there. Honouring the tracking seam here makes hasReader() mean exactly
38
+ "a live tracking reader exists". */
39
+ if (REACTIVE_CONTEXT.observer === undefined) {
40
+ return
41
+ }
31
42
  const existing = subscribers.get(key)
32
43
  if (existing) {
33
44
  existing()
@@ -63,9 +74,12 @@ export function createCacheStore(): CacheStore {
63
74
  /*
64
75
  Per-prefix channels arm fn-selector probes (pending(fn) / refreshing(fn))
65
76
  without waking them on unrelated cache events. Keyed by selector prefix
66
- (method+url / producer reference id — see selectorPrefix), so the
67
- population is bounded by probe call sites in code, not by data; a channel
68
- whose last reader tore down is an inert closure, not a leak.
77
+ (method+url / producer reference id — see selectorPrefix). Probes carry the
78
+ FULL call key when args are given (pending(fn, args) / peek(fn, args)), so a
79
+ long session that probes many distinct ids would otherwise accrete one map
80
+ entry per id on the tab-scoped store forever and make markLifecycle's forEach
81
+ O(distinct-keys-ever-probed). Each channel evicts its own entry once its last
82
+ reader tears down (identity-guarded against a concurrent re-track).
69
83
  */
70
84
  const prefixLifecycles = new Map<string, ReturnType<typeof createLifecycleChannel>>()
71
85
 
@@ -76,7 +90,15 @@ export function createCacheStore(): CacheStore {
76
90
  }
77
91
  let channel = prefixLifecycles.get(keyPrefix)
78
92
  if (channel === undefined) {
79
- channel = createLifecycleChannel()
93
+ const created = createLifecycleChannel(() => {
94
+ /* Drop the entry only if it's still THIS channel — a concurrent re-track may have
95
+ replaced it, and createSubscriber's own reader-count guard means onIdle fires
96
+ only when genuinely reader-less. */
97
+ if (prefixLifecycles.get(keyPrefix) === created) {
98
+ prefixLifecycles.delete(keyPrefix)
99
+ }
100
+ })
101
+ channel = created
80
102
  prefixLifecycles.set(keyPrefix, channel)
81
103
  }
82
104
  channel.track()
@@ -103,10 +125,11 @@ export function createCacheStore(): CacheStore {
103
125
  entries,
104
126
  events,
105
127
  subscribe,
106
- /* True while a reactive scope is reading this key — i.e. someone is holding
107
- its value on screen. invalidate() gates its reload marker on this: a key
108
- with no live reader has nothing to reload into, so the next read is a
109
- first-ever load, not a refresh. */
128
+ /* True iff a live tracking scope is reading this key — i.e. someone is holding
129
+ its value on screen right now. An untracked read never registers (subscribe
130
+ gates on the tracking seam), so a bare handler/setup read does not make this
131
+ true. invalidate()/refresh() gate on this: a key with no live reader has
132
+ nothing to reload into, so the next read is a first-ever load, not a refresh. */
110
133
  hasReader: (key: string) => subscribers.has(key),
111
134
  trackLifecycle,
112
135
  markLifecycle,
@@ -21,7 +21,10 @@ writing the subscriber's version source throws state_unsafe_mutation. The
21
21
  probes scan the registry at re-derive time, so a deferred ping reads state
22
22
  that is already current; marks within one tick coalesce into one notify.
23
23
  */
24
- export function createLifecycleChannel(): { track: () => void; mark: () => void } {
24
+ export function createLifecycleChannel(onIdle?: () => void): {
25
+ track: () => void
26
+ mark: () => void
27
+ } {
25
28
  let notify: (() => void) | undefined
26
29
  let tracker: (() => void) | undefined
27
30
  let marked = false
@@ -35,6 +38,9 @@ export function createLifecycleChannel(): { track: () => void; mark: () => void
35
38
  if (tracker === created) {
36
39
  tracker = undefined
37
40
  }
41
+ /* Last reader torn down — let an owner (the cache store's per-prefix map)
42
+ evict this now-inert channel instead of retaining it for the tab's life. */
43
+ onIdle?.()
38
44
  }
39
45
  })
40
46
  tracker = created