@abide/abide 0.49.0 → 0.50.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 (325) hide show
  1. package/AGENTS.md +70 -52
  2. package/CHANGELOG.md +177 -0
  3. package/README.md +12 -7
  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
@@ -1,9 +1,12 @@
1
1
  import { appNameSlot } from '../../shared/appNameSlot.ts'
2
2
  import { SSR_CACHE_CONTROL } from '../../shared/CACHE_CONTROL_VALUES.ts'
3
+ import { encodeRefJson } from '../../shared/encodeRefJson.ts'
3
4
  import { formatTraceparent } from '../../shared/formatTraceparent.ts'
5
+ import { hasSeedableRequest } from '../../shared/hasSeedableRequest.ts'
4
6
  import { layoutChainForRoute } from '../../shared/layoutChainForRoute.ts'
7
+ import { resolvedCellsSlot } from '../../shared/resolvedCellsSlot.ts'
5
8
  import { safeJsonForScript } from '../../shared/safeJsonForScript.ts'
6
- import { snapshotShippable } from '../../shared/snapshotShippable.ts'
9
+ import type { CacheEntry } from '../../shared/types/CacheEntry.ts'
7
10
  import type { CacheSnapshotEntry } from '../../shared/types/CacheSnapshotEntry.ts'
8
11
  import type { SsrPayload } from '../../shared/types/SsrPayload.ts'
9
12
  import type { StreamedResolution } from '../../shared/types/StreamedResolution.ts'
@@ -48,16 +51,18 @@ function resolveChunk(resolution: StreamedResolution): string {
48
51
  The abide-ui SSR document renderer. A matched route + params in, a finished HTML
49
52
  Response out.
50
53
 
51
- A page with no `await` block renders synchronously and ships buffered. A page with
52
- await blocks STREAMS: the pending shell flushes first, then each resolved fragment
53
- (`<abide-resolve>` carrying a JSON `<script>`) as its promise settles, swapped into its boundary
54
- by the inline SSR_SWAP_SCRIPT which also registers the value into the resume
54
+ A page with no `await` block AND no pending triggered bare read renders synchronously and
55
+ ships buffered. A page with await blocks or a triggered BARE async read still pending at
56
+ render-return (ADR-0024) STREAMS: the pending shell flushes first, then each resolved
57
+ fragment (`<abide-resolve>` carrying a JSON `<script>`) as its promise settles, swapped into
58
+ its boundary by the inline SSR_SWAP_SCRIPT — which also registers the value into the resume
55
59
  manifest so client hydration adopts it without re-fetching (see abide/ui/awaitBlock).
56
60
  Then, once the stream has run every `{#await}` thunk (creating and settling its cache
57
- entry mid-stream, after the render-return snapshot), each such entry streams an inline
58
- `__abideResolve(...)` chunk a warm snapshot, or a `{ key, miss }` marker for an
59
- unshippable body (→ live refetch) seeding the client store before the deferred bundle
60
- so the block's subscription read is warm (no refetch).
61
+ entry mid-stream, after the render-return snapshot), each such entry plus any bare-read
62
+ entry triggered during the sync render streams an inline `__abideResolve(...)` chunk: a
63
+ warm snapshot, or a `{ key, miss }` marker for an unshippable body — e.g. a read 504'd by its
64
+ endpoint `timeout` (→ live refetch) — seeding the client store before the deferred bundle
65
+ so the block's / bare read's subscription is warm (no refetch).
61
66
 
62
67
  `__SSR__` carries the route/params, mount base, trace, app name, client timeout,
63
68
  and the render-return cache snapshot (top-level `await` reads; the client seeds its tab
@@ -97,6 +102,29 @@ export function createUiPageRenderer({
97
102
  stack?: string,
98
103
  ) => Promise<Response | undefined>
99
104
  } {
105
+ /* The `cells` partition of __SSR__: every async cell that RESOLVED during this render (drained
106
+ from the request-scoped `resolvedCellsSlot` populated by `createAsyncCell.settleValue`),
107
+ keyed by its render-path id, each value ref-json-encoded so an in-process graph survives.
108
+ An unserializable value is dropped (a warn) — that one cell falls back to a client re-run
109
+ rather than blanking the payload. Undefined when no cell resolved (the common static page). */
110
+ function resolvedCellCells(): Record<string, string> | undefined {
111
+ const entries = resolvedCellsSlot.get()?.entries ?? []
112
+ if (entries.length === 0) {
113
+ return undefined
114
+ }
115
+ const cells: Record<string, string> = {}
116
+ for (const { key, value } of entries) {
117
+ try {
118
+ cells[key] = encodeRefJson(value)
119
+ } catch {
120
+ console.warn(
121
+ `[abide] async cell "${key}" resolved to an unserializable value — it will re-run on the client instead of hydrating warm.`,
122
+ )
123
+ }
124
+ }
125
+ return Object.keys(cells).length > 0 ? cells : undefined
126
+ }
127
+
100
128
  /* Build the __SSR__ <script> the client (startClient) reads on boot. The inline
101
129
  (settled) cache partition is computed once by the caller and threaded in, so the
102
130
  streaming branch can also drain the pending partition over the same render. */
@@ -111,6 +139,7 @@ export function createUiPageRenderer({
111
139
  route: routeUrl,
112
140
  params,
113
141
  cache: inline,
142
+ cells: resolvedCellCells(),
114
143
  base: base || undefined,
115
144
  trace: formatTraceparent(store.trace),
116
145
  app: appNameSlot.name,
@@ -183,13 +212,26 @@ export function createUiPageRenderer({
183
212
  /* Outermost layout → … → page: load every applicable layout chunk plus the
184
213
  page, then render the chain as one document (shared block-id pass). */
185
214
  const chainKeys = chainKeysForRoute(routeUrl)
186
- const views = await Promise.all([
187
- ...chainKeys.map((key) => layouts[key]?.().then((module) => module.default)),
188
- loadPage().then((module) => module.default),
215
+ /* Resolve each view PAIRED with its render-path key (layouts then page), then filter the
216
+ pairs together. Zipping in one pass makes the key↔view alignment structural rather than
217
+ an asserted "no view is ever dropped" invariant: were a layout to resolve to no default
218
+ export, dropping it would otherwise shift every later key by one and mis-key the page's
219
+ cells (a silent warm-seed mismatch — the class this render-path id exists to kill). The
220
+ keys stay byte-identical to the client router's `chainKeys`/`pageKey`. */
221
+ const resolved = await Promise.all([
222
+ ...chainKeys.map((key) =>
223
+ layouts[key]?.().then((module) => ({ key, view: module.default })),
224
+ ),
225
+ loadPage().then((module) => ({ key: routeUrl, view: module.default })),
189
226
  ])
227
+ const kept = resolved.filter(
228
+ (entry): entry is { key: string; view: UiComponent } =>
229
+ entry !== undefined && entry.view !== undefined,
230
+ )
190
231
  const ssr = await renderChain(
191
- views.filter((view): view is UiComponent => view !== undefined),
232
+ kept.map((entry) => entry.view),
192
233
  params,
234
+ kept.map((entry) => entry.key),
193
235
  )
194
236
 
195
237
  /* Snapshot the cache settled by render-return — top-level `await` reads and blocking
@@ -199,10 +241,24 @@ export function createUiPageRenderer({
199
241
  const inline = await serializeCacheSnapshot(store.cache)
200
242
  const inlinedKeys = new Set(inline.map((entry) => entry.key))
201
243
 
202
- /* No STREAMING await blocks ship buffered. Blocking `{#await … then}` blocks
203
- rendered inline (their markup is already in `ssr.html`); seed their resolved
204
- values so hydration adopts them without a refetch. */
205
- if (ssr.awaits.length === 0) {
244
+ /* ADR-0024: a bare async read (`{user}`, no `{#await}`) that TRIGGERED its fetch during
245
+ the sync render leaves a still-pending replayable entry in the store at render-return
246
+ not yet settled, so it missed the inline snapshot above. Detecting one here is what
247
+ opens the streaming gate for a page with no await block, so its value streams in
248
+ instead of shipping `undefined` buffered. `hasSeedableRequest` (the request half of
249
+ shippability — not `snapshotShippable`, which also demands `settled`) is the gate: it
250
+ excludes producers and stream cells (a `NamedAsyncIterable` cell holds no wire
251
+ request), which must stay `peek()`-at-flush and buffered. `!inlinedKeys` drops
252
+ anything already baked into `__SSR__` (a Tier-2 top-level `await`) so a value never
253
+ double-ships. */
254
+ const pendingBareReads = Array.from(store.cache.entries.values()).filter(
255
+ (entry) => hasSeedableRequest(entry) && !inlinedKeys.has(entry.key),
256
+ )
257
+
258
+ /* No STREAMING await blocks AND no triggered bare read pending → ship buffered exactly
259
+ as before. Blocking `{#await … then}` blocks rendered inline (their markup is already
260
+ in `ssr.html`); seed their resolved values so hydration adopts them without a refetch. */
261
+ if (ssr.awaits.length === 0 && pendingBareReads.length === 0) {
206
262
  const html = injectRoutePreloads(
207
263
  shell.replace(SSR_MARKER, (_match, key: string) =>
208
264
  key === 'body' ? ssr.html : key === 'state' ? '' : '',
@@ -232,6 +288,11 @@ export function createUiPageRenderer({
232
288
  const head =
233
289
  `<script>${SSR_SWAP_SCRIPT}${CACHE_RESOLVE_SCRIPT}</script>` +
234
290
  `${await stateTag(routeUrl, params, store, inline)}`
291
+ /* ADR-0039: the async-cell keys already baked into the head `__SSR__.cells` snapshot. A
292
+ streamed child's render is deferred to the drain, so its BLOCKING cells resolve AFTER this
293
+ and must ship as post-body `{cellSeed}` chunks instead — the delta below is keys NOT here.
294
+ Read `store` directly (the stream body runs outside the request ALS). */
295
+ const seededCellKeys = new Set(store.resolvedCells.entries.map((entry) => entry.key))
235
296
  const filled = injectRoutePreloads(
236
297
  shell.replace(HEAD_STATE_MARKER, (_match, key: string) => (key === 'head' ? head : '')),
237
298
  routeUrl,
@@ -256,22 +317,72 @@ export function createUiPageRenderer({
256
317
  )
257
318
  first = false
258
319
  }
259
- /* The {#await} reads created (and settled) their cache entries DURING the
260
- stream — the await expression is a thunk `renderToStream` ran lazily —
261
- so they missed the render-return snapshot. Drain them now: each lands an
262
- inline `__abideResolve(...)` chunk (a warm snapshot, or a `{ key, miss }`
263
- marker for an unshippable body live refetch) before the deferred
264
- bundle, so startClient seeds the store and the block's subscription read
265
- is warm. Skip keys already shipped inline in __SSR__. */
266
- const streamedEntries = Array.from(store.cache.entries.values()).filter(
267
- (entry) => snapshotShippable(entry) && !inlinedKeys.has(entry.key),
320
+ /* Two kinds of pending entry land here. (1) `{#await}` reads created (and
321
+ settled) their cache entries DURING the stream — the await expression is a
322
+ thunk `renderToStream` ran lazily — so they missed the render-return
323
+ snapshot. (2) A triggered BARE read (ADR-0024) created its entry during the
324
+ sync render but was never awaited, so it may still be in flight now. Drain
325
+ both: each lands an inline `__abideResolve(...)` chunk (a warm snapshot, or a
326
+ `{ key, miss }` marker for an unshippable body — e.g. a read 504'd by its
327
+ endpoint timeout live refetch) before the deferred bundle, so startClient seeds
328
+ the store and the read's subscription is warm. Skip keys already shipped inline in
329
+ __SSR__. */
330
+ const streamedEntries: CacheEntry[] = Array.from(
331
+ store.cache.entries.values(),
332
+ ).filter(
333
+ (entry) => hasSeedableRequest(entry) && !inlinedKeys.has(entry.key),
268
334
  )
335
+ /* `hasSeedableRequest`, NOT `snapshotShippable`: a triggered bare read
336
+ (ADR-0024) is drained here still-pending (never awaited by an `{#await}`
337
+ thunk), so gating on `settled` up front would skip it — streamCacheResolutions
338
+ awaits each entry's body itself (see snapshotEntryFromCache). A pending read is
339
+ bounded by its OWN endpoint `timeout` (which 504s the in-process handler during
340
+ SSR), so there is no separate SSR-stream deadline. A late-settling `{#await}`
341
+ entry is already settled by now, so the set is unchanged for the Tier-3 path. */
269
342
  for await (const resolution of streamCacheResolutions(
270
343
  store.cache,
271
344
  streamedEntries,
272
345
  )) {
273
346
  controller.enqueue(encoder.encode(resolveChunk(resolution)))
274
347
  }
348
+ /* ADR-0035: stream each STREAMING cell that SETTLED during this render as an
349
+ `__abideResolve({ cellKey, value })` chunk (keyed by its render-path id),
350
+ so the client adopts it post-hydration instead of only cold-re-running the
351
+ seed — the flash the pre-mount `__SSR__.cells` warm-seed can't fix without
352
+ diverging from the pending shell. These are already-settled VALUES
353
+ (recorded in createAsyncCell.settleValue), never awaited here: a cell that
354
+ stays pending through the request (`{#if getFoo()}` holds) is simply not
355
+ streamed and the client cold-runs it. An unserializable value emits nothing.
356
+ Read from `store` directly since the stream body runs outside the request ALS. */
357
+ for (const { key, value } of store.streamedCells.entries) {
358
+ let encoded: string
359
+ try {
360
+ encoded = encodeRefJson(value)
361
+ } catch {
362
+ continue
363
+ }
364
+ controller.enqueue(
365
+ encoder.encode(resolveChunk({ cellKey: key, value: encoded })),
366
+ )
367
+ }
368
+ /* ADR-0039: a STREAMED CHILD's BLOCKING async cells resolved during this drain,
369
+ after the head `__SSR__.cells` snapshot — ship the delta (keys not already
370
+ seeded) as `{cellSeed}` chunks so startClient seeds CELL_SEED before the
371
+ child's deferred mount and its cell constructs resolved (no flash/re-run). */
372
+ for (const { key, value } of store.resolvedCells.entries) {
373
+ if (seededCellKeys.has(key)) {
374
+ continue
375
+ }
376
+ let encoded: string
377
+ try {
378
+ encoded = encodeRefJson(value)
379
+ } catch {
380
+ continue
381
+ }
382
+ controller.enqueue(
383
+ encoder.encode(resolveChunk({ cellSeed: key, value: encoded })),
384
+ )
385
+ }
275
386
  controller.enqueue(encoder.encode(after ?? ''))
276
387
  controller.close()
277
388
  } catch (streamError) {
@@ -1,7 +1,5 @@
1
1
  import { relative } from 'node:path'
2
- import { fileName } from '../../shared/fileName.ts'
3
2
  import { isLayoutFile } from '../../shared/isLayoutFile.ts'
4
- import { analyzeComponent } from '../../ui/compile/analyzeComponent.ts'
5
3
  import { compileComponent } from '../../ui/compile/compileComponent.ts'
6
4
  import { nearestProjectRoot } from '../../ui/compile/nearestProjectRoot.ts'
7
5
  import { globToPathSet } from './globToPathSet.ts'
@@ -12,36 +10,29 @@ const CSS_HREF = /href=(["'])([^"']*\/_app\/[^"']*\.css)\1/
12
10
  // Every `/_app/<hashed-asset>` ref in the shell (the JS entry, the stylesheet) —
13
11
  // content-hashed names that move on any rebuild. Normalised out of `structure` so
14
12
  // a component/CSS edit (which re-hashes the bundle) doesn't read as a reload; the
15
- // source-based signals (components, non-component hashes, cssHref) catch real changes.
13
+ // source-based signals (component bodies, non-component hashes, cssHref) catch real changes.
16
14
  const APP_ASSET = /\/_app\/[^"'\s)]*/g
17
15
  // src/.abide is the build's own generated output (route d.ts) — rewritten every
18
16
  // rebuild, so it must stay out of the hash or the page would always reload.
19
17
  const GENERATED = /(^|\/)\.abide\//
20
18
 
21
- // page.abide / layout.abide are router-mounted, not `mountChild`-tracked, so they
22
- // can't hot-swap — they fold into `structure` (a reload) instead.
23
- function isPageOrLayout(moduleId: string): boolean {
24
- return fileName(moduleId) === 'page.abide' || isLayoutFile(moduleId)
25
- }
26
-
27
19
  /*
28
20
  The dev live-reload stamp, computed once at worker boot from SOURCE (the dev
29
21
  watcher only watches src/, so source is what a rebuild can reflect). Splits the
30
22
  edit space so the browser keeps its page where it can:
31
23
 
32
- - Each `.abide` is hashed by its client BUILD (`compileComponent`, which carries
33
- no CSS), so a style-only edit doesn't change it. A leaf component with no
34
- imports that isn't a page/layout is hot-swappable: its hash goes in
35
- `components`, keyed by the same module id the loader stamps (so it matches the
36
- registry). Every other component (page, layout, import-bearing) folds its hash
37
- into `structure` editing it reloads.
38
- - `structure` also covers non-`.abide` source, public stamps, the shell (with
39
- the stylesheet href normalised out), and the sorted component-id set (so
40
- adding/removing a component reloads).
41
- - `cssHref` is the entry stylesheet's current URL, read from the shell.
24
+ - `structure` fingerprints every source signal a reload must react to: each
25
+ `.abide`'s client BUILD hash (`compileComponent`, which carries NO CSS so a
26
+ style-only edit leaves it unchanged), the component-id set (adding/removing a
27
+ component reloads), non-`.abide` source, public assets, and the shell (with the
28
+ content-hashed `/_app/*` asset names normalised out, so a rebuild's re-hash isn't
29
+ itself a change). Any change here reloads.
30
+ - `cssHref` is the entry stylesheet's content-hashed URL; a change here ALONE swaps
31
+ the `<link>` in place a CSS edit restyles the live page with no reload and no
32
+ state loss.
42
33
 
43
- A compile failure (mid-edit syntax error) falls back to a raw-source hash marked
44
- non-hot, so the worst case is a reload, never a throw at boot.
34
+ A compile failure (mid-edit syntax error) falls back to a raw-source hash, so the
35
+ worst case is a reload, never a throw at boot.
45
36
  */
46
37
  export async function devClientFingerprint({
47
38
  srcDir,
@@ -58,8 +49,7 @@ export async function devClientFingerprint({
58
49
  .filter((file) => !GENERATED.test(file))
59
50
  .sort()
60
51
 
61
- const components: Record<string, string> = {}
62
- const nonHotComponents: string[] = []
52
+ const componentBodies: string[] = []
63
53
  const componentIds: string[] = []
64
54
  const otherSource: string[] = []
65
55
 
@@ -79,19 +69,14 @@ export async function devClientFingerprint({
79
69
  const moduleId = relative(nearestProjectRoot(full, projectRoot), full)
80
70
  componentIds.push(moduleId)
81
71
  let bodyHash: string
82
- let hot = false
83
72
  try {
84
- const isLayout = isLayoutFile(moduleId)
85
- bodyHash = Bun.hash(compileComponent(source, isLayout, moduleId)).toString(36)
86
- hot = analyzeComponent(source).imports === '' && !isPageOrLayout(moduleId)
73
+ bodyHash = Bun.hash(
74
+ compileComponent(source, isLayoutFile(moduleId), moduleId),
75
+ ).toString(36)
87
76
  } catch {
88
77
  bodyHash = Bun.hash(source).toString(36)
89
78
  }
90
- if (hot) {
91
- components[moduleId] = bodyHash
92
- } else {
93
- nonHotComponents.push(`${moduleId}:${bodyHash}`)
94
- }
79
+ componentBodies.push(`${moduleId}:${bodyHash}`)
95
80
  }),
96
81
  )
97
82
 
@@ -111,8 +96,8 @@ export async function devClientFingerprint({
111
96
  publicStamps.join('\n'),
112
97
  structureShell,
113
98
  componentIds.sort().join(','),
114
- nonHotComponents.sort().join('\n'),
99
+ componentBodies.sort().join('\n'),
115
100
  ].join('\0'),
116
101
  ).toString(36)
117
- return { structure, cssHref, components }
102
+ return { structure, cssHref }
118
103
  }
@@ -1,19 +1,27 @@
1
+ import { ambientPathBacking } from '../../ui/runtime/ambientPathBacking.ts'
1
2
  import { ambientScopeBacking } from '../../ui/runtime/ambientScopeBacking.ts'
3
+ import { cellBarrierBacking } from '../../ui/runtime/cellBarrierBacking.ts'
2
4
  import type { Scope } from '../../ui/types/Scope.ts'
5
+ import { pathStore } from './pathStore.ts'
6
+ import { renderCellBarrierStore } from './renderCellBarrierStore.ts'
3
7
  import { requestContext } from './requestContext.ts'
4
8
 
5
- /* Ambient scope when no request is in flight (server boot, a standalone render). */
9
+ /* Ambient scope when no request is in flight (server boot, a standalone render). The path needs
10
+ no such fallback: `pathStore.run` establishes its value with or without a request in flight,
11
+ and `getStore()` fails open to `''` outside any push. */
6
12
  let outsideRequest: Scope | undefined
7
13
 
8
14
  /*
9
- Swaps the ambient-scope backing to one keyed off the per-request AsyncLocalStorage
10
- store, so `CURRENT_SCOPE.current` isolates per request instead of living in one
11
- module global (see CURRENT_SCOPE). SSR is partly async a render `await`s inline
12
- between its `enterScope`/`exitScope` bracketand the async context propagates the
13
- right store across those awaits, so a resumed render reads ITS OWN scope even while
14
- another request renders concurrently. Called once at server boot (createServer),
15
- alongside the request-scope resolver install. Reads/writes outside any request fall
16
- back to a module variable (unchanged behaviour for standalone renders).
15
+ Swaps the ambient-scope backing to one keyed off the per-request AsyncLocalStorage store and the
16
+ ambient-PATH backing to the dedicated `pathStore.run` (ADR-0033 D1), so `CURRENT_SCOPE.current` and
17
+ `CURRENT_PATH.current` isolate per request instead of living in one module global (see CURRENT_SCOPE
18
+ / CURRENT_PATH). SSR is partly async a render `await`s inline between its `enterScope`/`exitScope`
19
+ bracket — and the async context propagates the right value across those awaits, so a resumed render
20
+ reads ITS OWN scope/path even while another request renders concurrently. For the path, `run`'s value
21
+ is ALSO inherited by the render's own post-await continuation (a mutable slot restored in `finally`
22
+ was not), which is the fix ADR-0033 buys. Called once at server boot (createServer), alongside the
23
+ request-scope resolver install. The scope backing still falls back to a module variable outside any
24
+ request (unchanged behaviour for standalone renders); the path backing needs no such fallback.
17
25
  */
18
26
  export function installAmbientScopeStore(): void {
19
27
  ambientScopeBacking.active = {
@@ -30,4 +38,17 @@ export function installAmbientScopeStore(): void {
30
38
  }
31
39
  },
32
40
  }
41
+ ambientPathBacking.active = {
42
+ run: (composedPath, build) => pathStore.run(composedPath, build),
43
+ get: () => pathStore.getStore() ?? '',
44
+ }
45
+ /* Per-render async-cell barrier isolation (ADR-0037 Phase 2): a hoisted concurrent child render
46
+ runs under its own pending-cells list via `renderCellBarrierStore.run`, and the barrier
47
+ consumers read it through `cellBarrierBacking.current`. ALS so the fresh list survives the
48
+ child render's own awaits; `undefined` outside any isolated render leaves the page/request
49
+ drain untouched. */
50
+ cellBarrierBacking.active = {
51
+ current: () => renderCellBarrierStore.getStore(),
52
+ run: (list, render) => renderCellBarrierStore.run(list, render),
53
+ }
33
54
  }
@@ -15,9 +15,10 @@ const redden = (text: string): string =>
15
15
  hasColor ? `${Bun.color('red', 'ansi-256')}${text}\x1b[39m` : text
16
16
 
17
17
  /*
18
- A declared inputSchema is what makes mcp/cli safe to advertise (see defineRpc /
19
- defineSocket), so a missing schema gets a red `·` to flag the declaration whose
20
- machine surfaces are gated behind it.
18
+ A declared inputSchema OR a build-projected input-type schema (ADR-0030 input
19
+ side), which advertises the same shape is what makes mcp/cli safe to advertise
20
+ (see defineRpc / defineSocket), so a declaration with neither gets a red `·` to
21
+ flag that its machine surfaces are gated behind a missing contract.
21
22
  */
22
23
  const schemaCell = (hasSchema: boolean): string => (hasSchema ? PRESENT : redden(ABSENT))
23
24
  const flag = (on: boolean): string => (on ? PRESENT : ABSENT)
@@ -133,10 +134,10 @@ export async function logExposedSurfaces(routing: { pages: Pages }): Promise<voi
133
134
 
134
135
  const rpcRows = Array.from(rpcRegistry.values(), (entry) => [
135
136
  withMethod(entry.remote.method, entry.remote.url),
136
- schemaCell(Boolean(entry.inputSchema)),
137
- flag(entry.clients.browser),
138
- flag(entry.clients.mcp),
139
- flag(entry.clients.cli),
137
+ schemaCell(entry.inputSchema !== undefined || entry.inputJsonSchema !== undefined),
138
+ flag(entry.remote.clients.browser),
139
+ flag(entry.remote.clients.mcp),
140
+ flag(entry.remote.clients.cli),
140
141
  ]).sort()
141
142
 
142
143
  logTables([
@@ -0,0 +1,15 @@
1
+ // AsyncLocalStorage is canonical via node:async_hooks — Bun has no separate API
2
+ import { AsyncLocalStorage } from 'node:async_hooks'
3
+
4
+ /*
5
+ The server's dedicated render-path store (ADR-0033 D1). Backs `CURRENT_PATH.current` during an
6
+ SSR pass with the COMPOSED path as the store value, pushed by `.run(compose(base, segment), build)`
7
+ at each nesting site (route → layout → row → branch → child ordinal). Unlike a mutable slot restored
8
+ in `finally`, `run`'s value is inherited by every async continuation spawned inside it — so a render
9
+ body resuming after an `await` still reads ITS OWN path, and sibling rows/children (continuations of
10
+ the enclosing render body) read the enclosing render's path. Per-request isolation falls out for free
11
+ (each route render is its own `run` tree), which is why this subsumes the path half of the request
12
+ store. Reads outside any `run` fail open to `''` (a detached scope then takes the counter fallback).
13
+ Server-only, kept out of `lib/ui` so the browser bundle never drags in `node:async_hooks`.
14
+ */
15
+ export const pathStore = new AsyncLocalStorage<string>()
@@ -52,7 +52,7 @@ export function ensureRegistriesLoaded(): Promise<void> {
52
52
  .then(() => undefined)
53
53
  /*
54
54
  Clear the memo on failure so a transient import error (a
55
- module that throws at load, fixed by the next HMR pass)
55
+ module that throws at load, fixed by the next rebuild)
56
56
  doesn't poison every later enumeration request for the
57
57
  process lifetime. The rejection still propagates to this
58
58
  caller; the reset only affects subsequent calls.
@@ -0,0 +1,15 @@
1
+ // AsyncLocalStorage is canonical via node:async_hooks — Bun has no separate API
2
+ import { AsyncLocalStorage } from 'node:async_hooks'
3
+ import type { PendingAsyncCells } from '../../shared/types/PendingAsyncCells.ts'
4
+
5
+ /*
6
+ The server's per-RENDER async-cell barrier store (ADR-0037 Phase 2). Backs `cellBarrierBacking` with
7
+ a fresh pending-cells list pushed by `isolateCellBarrier` around a hoisted concurrent child render.
8
+ Unlike the per-request `pendingAsyncCells` (one list a whole request shares), `run`'s value is
9
+ inherited by the child render's own async continuations — so a child that suspends on its script
10
+ await or its `$$settleAsyncCells` barrier keeps reading ITS list, and two sibling renders in flight
11
+ at once never splice each other's cells. `getStore()` is `undefined` outside any isolated render, so
12
+ the page's own cells fall through to the request list. Server-only, kept out of `lib/ui` so the
13
+ browser bundle never drags in `node:async_hooks`. Mirrors `pathStore` (ADR-0033 D1).
14
+ */
15
+ export const renderCellBarrierStore = new AsyncLocalStorage<PendingAsyncCells>()
@@ -31,6 +31,9 @@ export function runWithRequestScope(
31
31
  url,
32
32
  req,
33
33
  cache: createCacheStore(),
34
+ pendingAsyncCells: { promises: [] },
35
+ resolvedCells: { entries: [] },
36
+ streamedCells: { entries: [] },
34
37
  trace: createTraceContext(req.headers.get('traceparent')),
35
38
  start: Bun.nanoseconds(),
36
39
  }
@@ -4,10 +4,12 @@ import type { CacheStore } from '../../shared/types/CacheStore.ts'
4
4
  import { snapshotEntryFromCache } from './snapshotEntryFromCache.ts'
5
5
 
6
6
  /*
7
- Snapshots the request-scoped cache for SSR at a single instant: every replayable
8
- (GET-only) entry settled by now, serialized to a wire-safe CacheSnapshotEntry the
9
- client seeds its store from. Unsettled and non-replayable entries are skipped; a body
10
- that can't ship (binary / streaming / rejected) drops out via snapshotEntryFromCache.
7
+ Snapshots the request-scoped cache for SSR at a single instant: every entry
8
+ (any method) called inline and settled by now, serialized to a wire-safe
9
+ CacheSnapshotEntry the client seeds its store from so an inline POST hydrates
10
+ warm rather than re-firing after render. Unsettled entries and producers/stream
11
+ cells (no wire request) are skipped; a body that can't ship (binary / streaming /
12
+ rejected) drops out via snapshotEntryFromCache.
11
13
 
12
14
  Snapshots concurrently — the awaits are immediate (entries are already settled), but
13
15
  their body reads run in parallel. Never blocks on an unsettled entry.
@@ -1,20 +1,22 @@
1
1
  import { contentBodyKind } from '../../shared/contentBodyKind.ts'
2
2
  import { contentTypeOf } from '../../shared/contentTypeOf.ts'
3
- import { hasReplayableRequest } from '../../shared/hasReplayableRequest.ts'
3
+ import { hasSeedableRequest } from '../../shared/hasSeedableRequest.ts'
4
4
  import { isStreamingResponse } from '../../shared/isStreamingResponse.ts'
5
5
  import type { CacheEntry } from '../../shared/types/CacheEntry.ts'
6
6
  import type { CacheSnapshotEntry } from '../../shared/types/CacheSnapshotEntry.ts'
7
7
  import type { CacheStore } from '../../shared/types/CacheStore.ts'
8
- import type { ReplayableMethod } from '../../shared/types/ReplayableMethod.ts'
8
+ import type { HttpMethod } from '../../shared/types/HttpMethod.ts'
9
9
 
10
10
  /*
11
11
  Awaits one cache entry and turns it into a wire-safe snapshot, or undefined
12
12
  when it can't ship. Shared by the inline snapshot path (settled entries,
13
13
  resolves immediately) and the streaming drain (pending {#await} entries,
14
- resolves whenever the underlying fetch lands). Only replayable methods (see
15
- REPLAYABLE_METHODS) with a textual Content-Type survive writes must not
16
- re-fire from a snapshot, body-carrying methods can't be replayed without the
17
- original request body, and binary bodies don't round-trip through JSON.
14
+ resolves whenever the underlying fetch lands). Any entry carrying a wire request
15
+ (any method an inline POST seeds too, so it hydrates warm rather than re-firing
16
+ after render) with a textual Content-Type survives; producers/stream cells (no
17
+ request) and binary bodies (don't round-trip through JSON) drop out. Seeding is
18
+ not replay: the seeded value is read warm once, never auto-re-fired (that stays
19
+ GET-only — see REPLAYABLE_METHODS).
18
20
 
19
21
  Reads the body once and replaces the entry's promise with a string-bodied
20
22
  Response so later `shareable()` clones operate on a buffered body instead of
@@ -27,16 +29,14 @@ export async function snapshotEntryFromCache(
27
29
  store: CacheStore,
28
30
  entry: CacheEntry,
29
31
  ): Promise<CacheSnapshotEntry | undefined> {
30
- /* The request half of the shared shippability gate: a replayable wire request.
31
- Producer entries (no request) and non-replayable methods never snapshot — the
32
- streaming-drain caller hands still-pending entries here, so `settled` is NOT gated
33
- (the caller's snapshotShippable filter already required it for the entries it picks). */
34
- if (!hasReplayableRequest(entry) || !entry.request) {
32
+ /* The request half of the shared shippability gate: a seedable wire request (any method).
33
+ Producer entries and stream cells (no request) never snapshot — the streaming-drain
34
+ caller hands still-pending entries here, so `settled` is NOT gated (the caller's
35
+ snapshotShippable filter already required it for the entries it picks). */
36
+ if (!hasSeedableRequest(entry) || !entry.request) {
35
37
  return undefined
36
38
  }
37
- /* `hasReplayableRequest` already verified the uppercased method is replayable; assert it
38
- for the snapshot's typed `method` field rather than re-running the same Set check. */
39
- const method = entry.request.method.toUpperCase() as ReplayableMethod
39
+ const method = entry.request.method.toUpperCase() as HttpMethod
40
40
  const response = await readSettled(entry.promise as Promise<Response>)
41
41
  if (!response) {
42
42
  return undefined
@@ -1,21 +1,21 @@
1
1
  import type { CacheEntry } from '../../shared/types/CacheEntry.ts'
2
2
  import type { CacheSnapshotEntry } from '../../shared/types/CacheSnapshotEntry.ts'
3
3
  import type { CacheStore } from '../../shared/types/CacheStore.ts'
4
- import type { StreamedResolution } from '../../shared/types/StreamedResolution.ts'
4
+ import type { CacheResolution } from '../../shared/types/StreamedResolution.ts'
5
5
  import { snapshotEntryFromCache } from './snapshotEntryFromCache.ts'
6
6
 
7
7
  /*
8
8
  Drains the pending ({#await}) cache entries in resolution order — whichever
9
9
  fetch lands next is yielded next, so a slow entry never blocks a fast one
10
- behind it. Yields exactly one StreamedResolution per entry: the snapshot when
10
+ behind it. Yields exactly one CacheResolution per entry: the snapshot when
11
11
  serialization succeeds, or a `{ key, miss }` marker when the body can't ship so
12
12
  the client placeholder re-fetches instead of hanging on a deferred that never
13
- settles.
13
+ settles. (Never the cell arm of StreamedResolution — that is emitted separately.)
14
14
  */
15
15
  export async function* streamCacheResolutions(
16
16
  store: CacheStore,
17
17
  pending: CacheEntry[],
18
- ): AsyncIterable<StreamedResolution> {
18
+ ): AsyncIterable<CacheResolution> {
19
19
  /*
20
20
  Tag each pending serialization with its key so the loop can drop exactly
21
21
  the one that just settled. Deleting inside the `.then` instead would race
@@ -29,9 +29,16 @@ export async function* streamCacheResolutions(
29
29
  snapshotEntryFromCache(store, entry).then((snapshot) => ({ key: entry.key, snapshot })),
30
30
  )
31
31
  }
32
+ /*
33
+ Drain in settle order. A pending BARE read (ADR-0024 auto-streaming) is bounded by its OWN
34
+ endpoint `timeout` — which 504s the in-process handler during SSR, settling the entry into an
35
+ error that snapshots as a `{ key, miss }` (the client refetches on hydrate). So there is no
36
+ separate SSR-stream deadline (ADR-0024 option a): a read with a declared timeout self-settles;
37
+ a read that declares none is unbounded — the author bounds it with a `timeout`.
38
+ */
32
39
  while (inflight.size > 0) {
33
- const { key, snapshot } = await Promise.race(inflight.values())
34
- inflight.delete(key)
35
- yield snapshot ?? { key, miss: true }
40
+ const settled = await Promise.race(inflight.values())
41
+ inflight.delete(settled.key)
42
+ yield settled.snapshot ?? { key: settled.key, miss: true }
36
43
  }
37
44
  }