@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
@@ -9,6 +9,7 @@ import { composeProps } from './composeProps.ts'
9
9
  import { eachPlan } from './eachPlan.ts'
10
10
  import { elementPlan } from './elementPlan.ts'
11
11
  import { groupBindParts } from './groupBindParts.ts'
12
+ import { hoistableChildRenders } from './hoistableChildRenders.ts'
12
13
  import { ifPlan } from './ifPlan.ts'
13
14
  import { interpolatedTemplateLiteral } from './interpolatedTemplateLiteral.ts'
14
15
  import { isControlFlow } from './isControlFlow.ts'
@@ -74,8 +75,19 @@ export function generateBuild(
74
75
  derivedNames: ReadonlySet<string>,
75
76
  computedNames: ReadonlySet<string>,
76
77
  isLayout = false,
78
+ /* `linked` / async `computed` names, lowered to `$$readCell(name)` in template exprs. */
79
+ cellReadNames: ReadonlySet<string> = new Set(),
77
80
  ): string {
78
81
  const nextVar = makeVarNamer()
82
+ /* A per-body source-order ordinal for each `<Child/>` mount site, so two same-type siblings
83
+ get distinct render-path segments. Combined with the ambient branch/row path a control-flow
84
+ ancestor pushes, this makes every mounted instance's id unique and stable (ADR render-path
85
+ identity). Drawn in document-order walk, matching the SSR back-end's identical walk. */
86
+ let childOrdinal = 0
87
+ /* ADR-0039: the SAME hoistable-child set the SSR back-end computes (identical inputs → identical
88
+ node identities), so the client emits the dual-mode `$$mountStreamedChild` for exactly the
89
+ children the server may stream. A non-hoistable child keeps the plain `$$mountChild`. */
90
+ const streamCapable = hoistableChildRenders(nodes, cellReadNames)
79
91
 
80
92
  /* In a layout, `<slot/>` outlets are rewritten to `OUTLET_TAG` elements up front
81
93
  (`asOutlet`) so the static-clone path carries them as ordinary structure. `asOutlet`
@@ -109,7 +121,7 @@ export function generateBuild(
109
121
  withShadow,
110
122
  bindRead,
111
123
  bindWrite,
112
- } = lowerContext(stateNames, derivedNames, computedNames)
124
+ } = lowerContext(stateNames, derivedNames, computedNames, cellReadNames)
113
125
 
114
126
  /* Maps a plan `Binding`'s classification to the client `ShadowKind`: a `reactive` value
115
127
  derefs as a `.value` cell (`derived`), a `plain` value as the bare local (`plain`). The
@@ -544,6 +556,12 @@ export function generateBuild(
544
556
  return `{ match: ${match}, render: ${branchThunk(branch.children)} }`
545
557
  })
546
558
  .join(', ')
559
+ /* A bare async subject holds the whole switch while the cell is pending — no case, not
560
+ even the default — then matches on the settled value's peek. */
561
+ if (node.asyncSubject === true) {
562
+ const cell = node.subject.trim()
563
+ return `$$switchBlock(${parentVar}, () => $$readCell(${cell}), [${cases}], ${before}, () => $$cellPending(${cell}));\n`
564
+ }
547
565
  return `$$switchBlock(${parentVar}, () => (${lowerExpression(node.subject)}), [${cases}], ${before});\n`
548
566
  }
549
567
 
@@ -575,15 +593,14 @@ export function generateBuild(
575
593
  /* The props bag a child mount receives — composed by the shared `composeProps` so the
576
594
  build and SSR back-ends emit the same last-wins layering. */
577
595
  function propsArg(node: Extract<TemplateNode, { kind: 'component' }>): string {
578
- return composeProps(node.props, lowerExpression, slotPart(node))
596
+ return composeProps(node.props, lowerExpression, slotPart(node), bindRead, bindWrite)
579
597
  }
580
598
 
581
599
  /* Mounts a child component as a marker-bounded range on `parentVar`, positioned at
582
600
  `before` (a skeleton anchor's `anchorCursor`, or `null` for a standalone child).
583
601
  `mountRange` opens the `[`/`]` markers and builds the child between them — no
584
602
  wrapper element — so the child's root is a true direct child of `parentVar`.
585
- Hydration stays ambient, so the child claims its server range in place. The
586
- component name passes as the scope label (the inspector's `<Counter>` name). */
603
+ Hydration stays ambient, so the child claims its server range in place. */
587
604
  function generateChildComponent(
588
605
  node: Extract<TemplateNode, { kind: 'component' }>,
589
606
  parentVar: string,
@@ -595,7 +612,11 @@ export function generateBuild(
595
612
  cell, so `<Icon>` from `{#for {icon: Icon} of …}` mounts the component the cell
596
613
  holds, not the cell object (whose `.build` is undefined → `build is not a
597
614
  function`). SSR emits the same lowering for congruence. */
598
- return `$$mountChild(${parentVar}, ${lowerExpression(node.name)}, ${propsArg(node)}, ${before}, ${JSON.stringify(node.name)});\n`
615
+ /* A hoistable child may have STREAMED (server chose per-render): emit the dual-mode adopter,
616
+ which probes the cursor to adopt an inlined `[ … ]` range OR a streamed `abide:await`
617
+ boundary (ADR-0039). Same args as mountChild. */
618
+ const mountFn = streamCapable.has(node) ? '$$mountStreamedChild' : '$$mountChild'
619
+ return `${mountFn}(${parentVar}, ${lowerExpression(node.name)}, ${propsArg(node)}, ${before}, ${childOrdinal++});\n`
599
620
  }
600
621
 
601
622
  /* An await block: pending → resolved(value) / error branches. Each branch is a
@@ -717,16 +738,35 @@ export function generateBuild(
717
738
  const thenThunk = branchThunk(plan.thenChildren)
718
739
  const elseThunk =
719
740
  plan.elseBranch === undefined ? 'undefined' : branchThunk(plan.elseBranch.children)
741
+ /* A bare async subject (`{#if getX()}`) passes `when` a pending probe so a
742
+ still-loading cell renders NO branch; the condition itself is the plain peek read
743
+ (`$$readCell`), evaluated only once no longer pending. */
744
+ if (node.asyncSubject === true) {
745
+ const cell = node.condition.trim()
746
+ return `$$when(${parentVar}, () => $$readCell(${cell}), ${thenThunk}, ${elseThunk}, ${before}, () => $$cellPending(${cell}));\n`
747
+ }
720
748
  return `$$when(${parentVar}, () => (${lowerExpression(node.condition)}), ${thenThunk}, ${elseThunk}, ${before});\n`
721
749
  }
722
750
  /* if/elseif/else is a cond-chain — reuse `switchBlock` over a constant `true`
723
751
  subject with `Boolean`-coerced match thunks, so the first truthy branch wins
724
- (`else` is the match-less default). */
752
+ (`else` is the match-less default). A branch whose own condition is a bare async
753
+ subject carries a `pending` probe: `switchBlock` holds the chain there while it loads,
754
+ so sync and async branches interleave and each resolves at render time — a pending
755
+ async branch never lets a later branch render on its not-yet-known condition. */
756
+ const condEntry = (
757
+ condition: string,
758
+ asyncSubject: boolean | undefined,
759
+ children: TemplateNode[],
760
+ ): string => {
761
+ const pending =
762
+ asyncSubject === true ? `, pending: () => $$cellPending(${condition.trim()})` : ''
763
+ return `{ match: () => Boolean(${lowerExpression(condition)}), render: ${branchThunk(children)}${pending} }`
764
+ }
725
765
  const entries = [
726
- `{ match: () => Boolean(${lowerExpression(node.condition)}), render: ${branchThunk(plan.thenChildren)} }`,
766
+ condEntry(node.condition, node.asyncSubject, plan.thenChildren),
727
767
  ...plan.branches.map((branch) =>
728
768
  branch.condition !== undefined
729
- ? `{ match: () => Boolean(${lowerExpression(branch.condition)}), render: ${branchThunk(branch.children)} }`
769
+ ? condEntry(branch.condition, branch.asyncSubject, branch.children)
730
770
  : `{ match: undefined, render: ${branchThunk(branch.children)} }`,
731
771
  ),
732
772
  ]
@@ -787,9 +827,12 @@ export function generateBuild(
787
827
  const catchArg = plan.async
788
828
  ? `, ${plan.hasCatch ? branchThunk(plan.catchChildren, plan.catchBindings) : 'undefined'}`
789
829
  : ''
830
+ /* Whether the each has an explicit `by` key — the row then keys its render-path segment
831
+ on the stable key rather than its position (see `each`/`eachAsync`). */
832
+ const keyed = plan.key !== undefined
790
833
  return (
791
834
  `${fn}(${parentVar}, () => (${lowerExpression(plan.items)}), ` +
792
- `(${keyParam}) => (${keyExpression}), (${rowParam}, ${itemWiring.param}${indexParam}) => {\n${itemWiring.prefix}${rowBody}}${catchArg}, ${before});\n`
835
+ `(${keyParam}) => (${keyExpression}), (${rowParam}, ${itemWiring.param}${indexParam}) => {\n${itemWiring.prefix}${rowBody}}${catchArg}, ${before}, ${keyed});\n`
793
836
  )
794
837
  }
795
838
 
@@ -13,6 +13,8 @@ import { composeProps } from './composeProps.ts'
13
13
  import { eachPlan } from './eachPlan.ts'
14
14
  import { elementPlan } from './elementPlan.ts'
15
15
  import { groupBindParts } from './groupBindParts.ts'
16
+ import { hoistableAwaits } from './hoistableAwaits.ts'
17
+ import { hoistableChildRenders } from './hoistableChildRenders.ts'
16
18
  import { ifPlan } from './ifPlan.ts'
17
19
  import { interpolatedTemplateLiteral } from './interpolatedTemplateLiteral.ts'
18
20
  import { isAnchorPositioned } from './isAnchorPositioned.ts'
@@ -66,10 +68,16 @@ export function generateSSR(
66
68
  derivedNames: ReadonlySet<string>,
67
69
  computedNames: ReadonlySet<string>,
68
70
  isLayout = false,
69
- ): string {
71
+ /* `linked` / async `computed` names, lowered to `$$readCell(name)` in template exprs. */
72
+ cellReadNames: ReadonlySet<string> = new Set(),
73
+ ): { body: string; flightDecls: string; hasStagedChildren: boolean } {
70
74
  /* Unique temp var names (child render results); runtime block ids are
71
75
  allocated separately at runtime via `$ctx.next++`. */
72
76
  const nextVar = makeVarNamer()
77
+ /* A per-body source-order ordinal for each `<Child/>` render site — the render-path segment the
78
+ child roots under, matching the client `mountChild`'s `childOrdinal` (`generateBuild`) drawn
79
+ in the identical document-order walk, so both compose the same cell scope ids. */
80
+ let childOrdinal = 0
73
81
 
74
82
  /* The enclosing `<select bind:value>`s, innermost last: each carries the JS var holding
75
83
  its bound value and whether it's a `multiple` (array) select, so an `<option>` rendered
@@ -84,7 +92,8 @@ export function generateSSR(
84
92
  withNestedScripts,
85
93
  withShadow,
86
94
  bindRead,
87
- } = lowerContext(stateNames, derivedNames, computedNames)
95
+ bindWrite,
96
+ } = lowerContext(stateNames, derivedNames, computedNames, cellReadNames)
88
97
 
89
98
  /* SSR has no cells, so every plan `Binding` — `reactive` or `plain` — renders as a `plain`
90
99
  shadow (a real JS local / loop var / arrow param, read as the bare identifier). The one
@@ -150,6 +159,28 @@ export function generateSSR(
150
159
  through the generic element path exactly as the client clones it. */
151
160
  const rootNodes = isLayout ? nodes.map(asOutlet) : nodes
152
161
 
162
+ /* ADR-0034: the await blocks whose promise-start hoists to the synchronous render prefix so
163
+ independent flights overlap instead of serializing. Server-only — this rewires only the SSR
164
+ promise SOURCE (a blocking `await $flightN`, a streaming `promise: () => $flightN`); the
165
+ block's markers, `$ctx.next++` id, RESUME wire, and the whole client build stay byte-identical.
166
+ The `flightDecls` are emitted by compileSSR after the lowered script and BEFORE the barrier, so
167
+ a hoisted flight is already in-flight while the barrier awaits any unrelated blocking cell. */
168
+ const flightNameByNode = new Map<Extract<TemplateNode, { kind: 'await' }>, string>()
169
+ for (const flight of hoistableAwaits(rootNodes, cellReadNames)) {
170
+ flightNameByNode.set(flight.node, flight.name)
171
+ }
172
+
173
+ /* ADR-0037 Phase 2: the top-level-spine `<Child/>` renders whose start hoists to the prefix so
174
+ sibling renders overlap instead of serializing behind each other's `await`. The component walk
175
+ below emits each hoisted child's flight decl into `childFlightDecls` (with its childOrdinal and
176
+ lowered props computed at the SAME site as the body's `await`, so the two can't drift) and
177
+ awaits the flight const at the structural position. Each hoisted render starts under
178
+ `$$isolateCellBarrier` so its async cells drain in their own list, not a concurrent sibling's
179
+ (the request-scoped barrier is `splice(0)`-drained). */
180
+ const hoistableChildSet = hoistableChildRenders(rootNodes, cellReadNames)
181
+ const childFlightDecls: string[] = []
182
+ let childFlightCounter = 0
183
+
153
184
  /* A snippet name (any identifier, `$` included) interpolated into a RegExp must have its
154
185
  regex metacharacters escaped, or e.g. a trailing `$` would read as an end-anchor and the
155
186
  call site would never match — leaving an un-awaited Promise stringified as `[object
@@ -266,6 +297,26 @@ export function generateSSR(
266
297
  const openRange = (target: string): string => push(target, RANGE_OPEN)
267
298
  const closeRange = (target: string): string => push(target, RANGE_CLOSE)
268
299
 
300
+ /* Wrap a control-flow branch/row body in the render-path segment the CLIENT pushes for it (the
301
+ `withPathFrom`/`withPath` call the `each`/`when`/`switchBlock` dom runtimes make), so a scope
302
+ created inside — a nested `<Child/>`, always an inline `await` in SSR — composes the SAME
303
+ serialization-stable id on both sides (the warm-seed key). Emitted ONLY when the subtree can
304
+ create such a scope: a component render is the sole scope-creating construct in a branch/row
305
+ and always lowers to an inline `await`, so `subtreeAwaits` is the gate. A purely-synchronous
306
+ branch creates no scope, so its segment is inert — skipping the wrap keeps the static render
307
+ synchronous (no per-row closure/await, matching today's output byte-for-byte). `segment` is a
308
+ JS expression (a literal branch key, or the each row's key/index); `$$withPath` escapes it,
309
+ exactly like the client. The row/branch RANGE markers stay OUTSIDE this call (they create no
310
+ scope), so only the content composes the path — mirroring the client, where the markers are
311
+ built outside `withPathFrom`. */
312
+ const withPathBranch = (segment: string, children: TemplateNode[], target: string): string => {
313
+ const content = branchContent(children, target)
314
+ if (!subtreeAwaits(children)) {
315
+ return content
316
+ }
317
+ return `await $$withPath(${segment}, async () => {\n${content}});\n`
318
+ }
319
+
269
320
  /* In a skeleton, a control-flow block or slot is positioned by an `<!--a-->` anchor
270
321
  (cloned into the located parent), so it can sit anywhere among static siblings.
271
322
  Emitted both sides in document order — the client's anchor scan lines up with it.
@@ -303,31 +354,98 @@ export function generateSSR(
303
354
  .join('')
304
355
  }
305
356
  if (node.kind === 'if') {
306
- /* `case` children are the `elseif`/`else` branches in source order; the rest are
307
- the `then` content. Each `elseif` becomes an `else if`, the match-less `else`
308
- the trailing `else`. */
357
+ /* `case` children are the `elseif`/`else` branches in source order; the rest are the
358
+ `then` content. The whole `if`/`elseif`/`else` chain desugars to a run of `if` /
359
+ `else if` clauses. A branch whose condition is a bare async subject contributes TWO
360
+ clauses in order — an empty `$$cellPending` guard (renders nothing, and stops the
361
+ chain: a still-loading branch holds, so nothing later renders on its unknown value)
362
+ then the `$$readCell` truthy test — so sync and async branches interleave and each
363
+ resolves at render time, mirroring the client `switchBlock` cond-chain. */
309
364
  const plan = ifPlan(node)
310
- let code = `if (${lowerExpression(node.condition)}) {\n${branchContent(plan.thenChildren, target)}}`
311
- for (const branch of plan.branches) {
312
- code +=
313
- branch.condition !== undefined
314
- ? ` else if (${lowerExpression(branch.condition)}) {\n${branchContent(branch.children, target)}}`
315
- : ` else {\n${branchContent(branch.children, target)}}`
365
+ /* The client build lowers a simple `if`/`else` through `when` (branch keys `'then'` /
366
+ `'else'`) and an `if` with any `elseif` through `switchBlock` over `[then, ...branches]`
367
+ (branch key = the case's array index). Mirror whichever the client picks so a nested
368
+ `<Child/>`'s scope id composes identically. */
369
+ const clauses: string[] = []
370
+ const conditional = (
371
+ condition: string,
372
+ asyncSubject: boolean | undefined,
373
+ children: TemplateNode[],
374
+ segment: string,
375
+ ): void => {
376
+ if (asyncSubject === true) {
377
+ const cell = condition.trim()
378
+ clauses.push(`($$cellPending(${cell})) {\n}`)
379
+ clauses.push(
380
+ `($$readCell(${cell})) {\n${withPathBranch(segment, children, target)}}`,
381
+ )
382
+ } else {
383
+ clauses.push(
384
+ `(${lowerExpression(condition)}) {\n${withPathBranch(segment, children, target)}}`,
385
+ )
386
+ }
387
+ }
388
+ /* `then` is the `when` `'then'` branch, or the `switchBlock` case at index 0. */
389
+ conditional(
390
+ node.condition,
391
+ node.asyncSubject,
392
+ plan.thenChildren,
393
+ JSON.stringify(plan.hasElseif ? '0' : 'then'),
394
+ )
395
+ for (let index = 0; index < plan.branches.length; index += 1) {
396
+ const branch = plan.branches[index] as Extract<TemplateNode, { kind: 'case' }>
397
+ if (branch.condition !== undefined) {
398
+ /* An `elseif` is the `switchBlock` case at `[then, ...branches]` index `index + 1`
399
+ (a chain with any condition always lowers to `switchBlock`). */
400
+ conditional(
401
+ branch.condition,
402
+ branch.asyncSubject,
403
+ branch.children,
404
+ JSON.stringify(String(index + 1)),
405
+ )
406
+ }
407
+ }
408
+ let code = clauses
409
+ .map((clause, index) => `${index === 0 ? 'if' : ' else if'} ${clause}`)
410
+ .join('')
411
+ if (plan.elseBranch !== undefined) {
412
+ /* `else` is the `when` `'else'` branch, or (for a cond-chain) the `switchBlock`
413
+ default at its own `[then, ...branches]` index. */
414
+ const elseSegment = plan.hasElseif
415
+ ? String(plan.branches.indexOf(plan.elseBranch) + 1)
416
+ : 'else'
417
+ code += ` else {\n${withPathBranch(JSON.stringify(elseSegment), plan.elseBranch.children, target)}}`
316
418
  }
317
419
  return `${anchor}${openRange(target)}${code}\n${closeRange(target)}`
318
420
  }
319
421
  if (node.kind === 'switch') {
320
422
  const plan = switchPlan(node)
321
- let code = `{ const $s = (${lowerExpression(node.subject)});\n`
423
+ /* A bare async subject: read the peek, but only match once the cell has settled —
424
+ a pending subject renders no case (matching the client's `switchBlock`). */
425
+ const subjectExpr =
426
+ node.asyncSubject === true
427
+ ? `$$readCell(${node.subject.trim()})`
428
+ : `(${lowerExpression(node.subject)})`
429
+ let code = `{ const $s = ${subjectExpr};\n`
430
+ if (node.asyncSubject === true) {
431
+ code += `if (!$$cellPending(${node.subject.trim()})) {\n`
432
+ }
322
433
  let started = false
323
- for (const branch of plan.cases) {
434
+ /* The client `switchBlock` keys each branch by its index in `plan.cases` (source order,
435
+ the default at its own position); mirror that index so a nested `<Child/>` matches. */
436
+ for (let index = 0; index < plan.cases.length; index += 1) {
437
+ const branch = plan.cases[index] as Extract<TemplateNode, { kind: 'case' }>
324
438
  if (branch.match !== undefined) {
325
- code += `${started ? 'else ' : ''}if ($s === (${lowerExpression(branch.match)})) {\n${branchContent(branch.children, target)}}\n`
439
+ code += `${started ? 'else ' : ''}if ($s === (${lowerExpression(branch.match)})) {\n${withPathBranch(JSON.stringify(String(index)), branch.children, target)}}\n`
326
440
  started = true
327
441
  }
328
442
  }
329
443
  if (plan.fallback !== undefined) {
330
- code += `${started ? 'else ' : ''}{\n${branchContent(plan.fallback.children, target)}}\n`
444
+ const fallbackSegment = String(plan.cases.indexOf(plan.fallback))
445
+ code += `${started ? 'else ' : ''}{\n${withPathBranch(JSON.stringify(fallbackSegment), plan.fallback.children, target)}}\n`
446
+ }
447
+ if (node.asyncSubject === true) {
448
+ code += `}\n`
331
449
  }
332
450
  return `${anchor}${openRange(target)}${code}}\n${closeRange(target)}`
333
451
  }
@@ -369,26 +487,45 @@ export function generateSSR(
369
487
  if (plan.async) {
370
488
  return anchor
371
489
  }
490
+ /* The client `each` pushes a render-path segment per row (keyed → the `by` key evaluated
491
+ on the raw item, exactly like its `keyOf`; keyless → the row position), so a nested
492
+ `<Child/>`'s scope id composes identically. Mirror it only when the row can create a
493
+ scope (`subtreeAwaits` — it holds a component); a static row needs no segment and stays
494
+ synchronous. A keyless row that needs its position reuses a bound `index`, else a
495
+ synthesized loop index — the same 0-based `entries()` position the client's row cell
496
+ carries, so the segments agree. */
497
+ const rowSegments = subtreeAwaits(plan.children)
498
+ const keylessIndex =
499
+ plan.key === undefined && plan.index === undefined && rowSegments
500
+ ? nextVar('$i')
501
+ : undefined
502
+ const indexBinding = plan.index ?? keylessIndex
372
503
  /* The row item (and index) are real `for`-loop locals, so the body must lower
373
504
  references to them as the bare identifier — `withBindings` registers the plan's
374
505
  row bindings (under `plain`, SSR's only kind) so a row binding that shadows a
375
506
  same-named component signal reads the loop value, not the (whole-list) signal it
376
507
  shadows. The names come straight from `plan.bindings` (the single source the
377
508
  client also reads); the items expression stays outside the shadow. */
378
- const rowBody = withBindings(
379
- withShadow,
380
- plan.bindings,
381
- ssrBindingKind,
382
- () =>
383
- `${openRange(target)}${branchContent(plan.children, target)}${closeRange(target)}`,
384
- )
509
+ const rowBody = withBindings(withShadow, plan.bindings, ssrBindingKind, () => {
510
+ /* The row's render-path segment, lowered inside the row's plain shadow so a keyed
511
+ `by` reads the raw loop item exactly like the client `keyOf` (a keyless row uses
512
+ its position). Unused when `rowSegments` is false — `withPathBranch` then skips it. */
513
+ const segment =
514
+ plan.key === undefined
515
+ ? (indexBinding ?? '0')
516
+ : `(${lowerExpression(plan.key)})`
517
+ return `${openRange(target)}${withPathBranch(segment, plan.children, target)}${closeRange(target)}`
518
+ })
385
519
  /* `index="i"` binds the row position. SSR reads it as a plain number from
386
520
  `entries()` over a materialized array; the client reads the same number from a
387
521
  cell, so first paint is congruent. No index → a plain `for…of` over the items. */
522
+ /* An undefined source renders no rows, not a throw — a `{#for x in promise}`
523
+ whose lifted source peeks undefined while pending (ADR-0032 D3). Mirrors the
524
+ client `each`'s undefined-as-empty guard. */
388
525
  const header =
389
- plan.index === undefined
390
- ? `for (const ${plan.as} of (${lowerExpression(plan.items)}))`
391
- : `for (const [${plan.index}, ${plan.as}] of [...(${lowerExpression(plan.items)})].entries())`
526
+ indexBinding === undefined
527
+ ? `for (const ${plan.as} of ((${lowerExpression(plan.items)}) ?? []))`
528
+ : `for (const [${indexBinding}, ${plan.as}] of [...((${lowerExpression(plan.items)}) ?? [])].entries())`
392
529
  return `${anchor}${header} {\n${rowBody}}\n`
393
530
  }
394
531
  if (node.kind === 'await') {
@@ -430,22 +567,54 @@ export function generateSSR(
430
567
  : `"children": () => (async () => { const $slot = []; ${slotCode}return $snip($slot.join('')); })`
431
568
  /* The same last-wins layering the client build emits (`composeProps`), so SSR
432
569
  and hydration read the same prop bag. */
433
- const propsExpr = composeProps(node.props, lowerExpression, slotPart)
570
+ const propsExpr = composeProps(
571
+ node.props,
572
+ lowerExpression,
573
+ slotPart,
574
+ bindRead,
575
+ bindWrite,
576
+ )
434
577
  /* Render the child (awaited — render is async) sharing this render's `$ctx`,
435
578
  so its `await`/`try` block ids draw from the same depth-first counter,
436
579
  unique across page + children, and the streamed fragments resolve into the
437
580
  right boundaries. MERGE its streaming awaits into `$awaits` and its inline
438
581
  blocking values into `$resume`. ($awaits/$resume are captured from the
439
582
  enclosing render body, including from branch closures.) */
583
+ /* The tag lowers like any reference (see generateBuild): a static import is left bare, a
584
+ reactive/loop/await binding derefs — SSR registers such a binding as `plain`, so it
585
+ reads the bare local holding the resolved component, keeping SSR and client congruent.
586
+ Root the child's render-path at this mount site's source-order ordinal — the same
587
+ segment the client's `mountChild` pushes — so the child's cells and block ids (ADR-0037)
588
+ get an id matching the client's. `$$withPath` sets the path across the child's awaits. */
589
+ const ordinal = childOrdinal++
590
+ const renderExpr = `$$withPath(${ordinal}, () => ${lowerExpression(node.name)}.render(${propsExpr}, $ctx))`
591
+ if (hoistableChildSet.has(node)) {
592
+ /* Hoistable (ADR-0037/0039): start the render as an isolated flight in the prefix, and
593
+ RESERVE its output position rather than awaiting inline. After the walk,
594
+ `finalizeStreamedChildren` fills the slot — inline `<!--[-->html<!--]-->` if the
595
+ flight already settled (byte-identical to the old inline await), or a streaming
596
+ `abide:await:CHILDPATH` boundary + html-only await if it is still pending. The
597
+ boundary id is the child's render-path (`$$renderPath(ordinal)`), congruent with the
598
+ client's `mountStreamedChild`. */
599
+ const flightName = `$cf${childFlightCounter++}`
600
+ childFlightDecls.push(
601
+ `const ${flightName} = $$flight(() => $$isolateCellBarrier(() => ${renderExpr}));`,
602
+ )
603
+ const slotVar = nextVar('$slot')
604
+ return (
605
+ anchor +
606
+ `${target}.push('');\n` +
607
+ `const ${slotVar} = ${target}.length - 1;\n` +
608
+ `$childSlots.push({ slot: ${slotVar}, out: ${target}, id: $$renderPath(${ordinal}), flight: ${flightName} });\n`
609
+ )
610
+ }
611
+ /* Non-hoistable child (off the top-level spine): await inline at its structural position,
612
+ sequential, as before — never streams. */
440
613
  const result = nextVar('$child')
441
614
  return (
442
615
  anchor +
443
616
  push(target, RANGE_OPEN) +
444
- /* The tag lowers like any reference (see generateBuild): a static import
445
- is left bare, a reactive/loop/await binding derefs — SSR registers such
446
- a binding as `plain`, so it reads the bare local holding the resolved
447
- component, keeping SSR and client congruent. */
448
- `const ${result} = await ${lowerExpression(node.name)}.render(${propsExpr}, $ctx);\n` +
617
+ `const ${result} = await ${renderExpr};\n` +
449
618
  `${target}.push(${result}.html);\n` +
450
619
  `for (const $a of ${result}.awaits) { $awaits.push($a); }\n` +
451
620
  `Object.assign($resume, ${result}.resume);\n` +
@@ -644,10 +813,13 @@ export function generateSSR(
644
813
  `withBindings` shadow models, so a reference to the binding reads the plain
645
814
  local rather than the (unresolved) component signal it shadows. */
646
815
  const resolved = nextVar('$av')
647
- let code = `const ${id} = $ctx.next++;\n`
816
+ /* ADR-0034: a hoisted block awaits the prefix flight const (already in-flight); a
817
+ non-hoisted block evaluates + awaits its promise inline as before. */
818
+ const hoistedPromise = flightNameByNode.get(node)
819
+ let code = `const ${id} = $$blockId($ctx);\n`
648
820
  code += `${target}.push("<!--abide:await:" + ${id} + "-->");\n`
649
821
  code += `try {\n`
650
- code += `const ${resolved} = await (${lowerExpression(node.promise)});\n`
822
+ code += `const ${resolved} = await (${hoistedPromise ?? lowerExpression(node.promise)});\n`
651
823
  code += `{\n`
652
824
  code += `const ${plan.resolvedAs} = ${resolved};\n`
653
825
  code += withBindings(withShadow, plan.resolvedBindings, ssrBindingKind, () =>
@@ -693,7 +865,7 @@ export function generateSSR(
693
865
  ): string {
694
866
  const plan = awaitPlan(node)
695
867
  const id = nextVar('$aid')
696
- let code = `const ${id} = $ctx.next++;\n`
868
+ let code = `const ${id} = $$blockId($ctx);\n`
697
869
  code += `${target}.push("<!--abide:await:" + ${id} + "-->");\n`
698
870
  code += branchContent(plan.pending, target)
699
871
  code += `${target}.push("<!--/abide:await:" + ${id} + "-->");\n`
@@ -713,26 +885,34 @@ export function generateSSR(
713
885
  const catchProp = plan.surfaceRejection
714
886
  ? ''
715
887
  : `catch: ${settled(plan.catchAs, plan.catchBindings, plan.catchChildren)} `
888
+ /* ADR-0034: a hoisted streaming block's thunk returns the prefix flight const (started
889
+ in the prefix, so it overlaps a preceding blocking await); non-hoisted evaluates lazily
890
+ at drain time as before. */
891
+ const hoistedPromise = flightNameByNode.get(node)
716
892
  code +=
717
893
  `$awaits.push({ id: ${id}, ` +
718
- `promise: () => (${lowerExpression(node.promise)}), ` +
894
+ `promise: () => (${hoistedPromise ?? lowerExpression(node.promise)}), ` +
719
895
  `then: ${settled(plan.resolvedAs, plan.resolvedBindings, plan.resolvedChildren)}, ` +
720
896
  `${catchProp}});\n`
721
897
  return code
722
898
  }
723
899
 
724
- /* A sync error boundary: push the guarded markup (++ finally) inside a real
725
- try/catch; on a throw, truncate the output back to the boundary start and push
726
- the catch markup (++ finally) instead — so even mid-stream a render throw
727
- becomes catch markup, not a broken response. No catch re-throws (propagates to
728
- an enclosing boundary / the 500 / the stream). Boundary comments let hydration
729
- discard the server content if the client adoption fails. */
900
+ /* A reactive error boundary: push the guarded markup (++ finally) inside a real
901
+ try/catch; on a throw, truncate the output back to the range start and push the
902
+ catch markup (++ finally) instead — so even mid-stream a render throw becomes catch
903
+ markup, not a broken response. No catch re-throws (propagates to an enclosing boundary
904
+ / the 500 / the stream). The `abide:try:N` boundary comments let hydration discard the
905
+ server content if the client adoption fails; the inner `[ … ]` range markers are the
906
+ live range the render-many client (`tryBlock`) tracks — emitted here so a FRESH client
907
+ mount (which brackets its branch) and the SSR markup stay congruent. */
730
908
  function generateTry(node: Extract<TemplateNode, { kind: 'try' }>, target: string): string {
731
909
  const plan = tryPlan(node)
732
910
  const id = nextVar('$tid')
733
911
  const mark = nextVar('$trim')
734
- let code = `const ${id} = $ctx.next++;\n`
912
+ let code = `const ${id} = $$blockId($ctx);\n`
735
913
  code += `${target}.push("<!--abide:try:" + ${id} + "-->");\n`
914
+ code += openRange(target)
915
+ /* Truncate back to just after the range-open marker on a catch, so `[` survives. */
736
916
  code += `const ${mark} = ${target}.length;\n`
737
917
  code += `try {\n`
738
918
  code += branchContent(plan.guarded, target)
@@ -747,9 +927,28 @@ export function generateSSR(
747
927
  code += `throw ${plan.catchAs};\n`
748
928
  }
749
929
  code += `}\n`
930
+ code += closeRange(target)
750
931
  code += `${target}.push("<!--/abide:try:" + ${id} + "-->");\n`
751
932
  return code
752
933
  }
753
934
 
754
- return generateInto(rootNodes, '$out')
935
+ /* The body walk emits `await $flightN` / `promise: () => $flightN` for hoisted nodes; the
936
+ matching prefix declarations (lowered at component scope — hoisted promises reference only
937
+ component-scope names, never a branch shadow, so this lowering matches the body's) are
938
+ returned for compileSSR to place after the lowered script and before the barrier. */
939
+ const body = generateInto(rootNodes, '$out')
940
+ let flightDecls = ''
941
+ for (const [node, name] of flightNameByNode) {
942
+ flightDecls += `const ${name} = $$flight(() => (${lowerExpression(node.promise)}));\n`
943
+ }
944
+ /* Child-render flights emitted during the body walk above (ADR-0037 Phase 2) — appended after
945
+ the await flights; both are independent prefix promise-starts, so order between them is free. */
946
+ for (const decl of childFlightDecls) {
947
+ flightDecls += `${decl}\n`
948
+ }
949
+ /* `hasStagedChildren` tells compileSSR to declare `$childSlots` and emit the post-walk
950
+ `$$finalizeStreamedChildren` await (ADR-0039); a component with no hoistable child stays
951
+ synchronous. Each hoistable child pushed one `childFlightDecls` entry, so its length is the
952
+ staged-child count. */
953
+ return { body, flightDecls, hasStagedChildren: childFlightDecls.length > 0 }
755
954
  }