@abide/abide 0.48.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 (360) hide show
  1. package/AGENTS.md +85 -59
  2. package/CHANGELOG.md +216 -0
  3. package/README.md +21 -10
  4. package/package.json +15 -4
  5. package/src/abideLsp.ts +5 -6
  6. package/src/abideResolverPlugin.ts +197 -169
  7. package/src/build.ts +79 -56
  8. package/src/buildCli.ts +3 -5
  9. package/src/buildDisconnected.ts +2 -5
  10. package/src/bundleApp.ts +2 -3
  11. package/src/checkAbide.ts +12 -2
  12. package/src/compile.ts +2 -4
  13. package/src/devEntry.ts +113 -28
  14. package/src/discoveryEntry.ts +3 -2
  15. package/src/lib/bundle/disconnected.abide +69 -73
  16. package/src/lib/bundle/infoPlist.ts +13 -7
  17. package/src/lib/bundle/installDownloads.ts +13 -3
  18. package/src/lib/bundle/installMacMenu.ts +13 -3
  19. package/src/lib/bundle/spawnEmbeddedServer.ts +13 -4
  20. package/src/lib/bundle/waitForServer.ts +6 -1
  21. package/src/lib/cli/parseArgvForRpc.ts +36 -9
  22. package/src/lib/cli/tokenizeArgvFlags.ts +4 -2
  23. package/src/lib/mcp/createMcpResourceServer.ts +13 -2
  24. package/src/lib/mcp/mcpTools.ts +15 -6
  25. package/src/lib/mcp/toolResultFromResponse.ts +40 -12
  26. package/src/lib/server/error.ts +6 -6
  27. package/src/lib/server/json.ts +17 -3
  28. package/src/lib/server/rpc/defineRpc.ts +58 -24
  29. package/src/lib/server/rpc/parseArgs.ts +113 -14
  30. package/src/lib/server/rpc/resolveRpcJsonSchema.ts +28 -0
  31. package/src/lib/server/rpc/runWithRpcTimeout.ts +12 -1
  32. package/src/lib/server/rpc/types/RpcHelper.ts +75 -105
  33. package/src/lib/server/rpc/types/RpcRegistryEntry.ts +24 -9
  34. package/src/lib/server/rpc/validationError.ts +1 -1
  35. package/src/lib/server/runtime/DEV_RELOAD_CLIENT_SCRIPT.ts +0 -15
  36. package/src/lib/server/runtime/buildCacheSnapshot.ts +10 -9
  37. package/src/lib/server/runtime/buildInspectorSurface.ts +6 -4
  38. package/src/lib/server/runtime/buildOpenApiSpec.ts +55 -11
  39. package/src/lib/server/runtime/buildPreloadManifest.ts +12 -10
  40. package/src/lib/server/runtime/createAppAssetServer.ts +18 -15
  41. package/src/lib/server/runtime/createAppRouteResolver.ts +145 -0
  42. package/src/lib/server/runtime/createPlumbingRouter.ts +212 -0
  43. package/src/lib/server/runtime/createRouteDispatcher.ts +4 -10
  44. package/src/lib/server/runtime/createServer.ts +120 -324
  45. package/src/lib/server/runtime/createUiPageRenderer.ts +137 -26
  46. package/src/lib/server/runtime/devClientFingerprint.ts +19 -34
  47. package/src/lib/server/runtime/finalizeResponse.ts +11 -1
  48. package/src/lib/server/runtime/installAmbientScopeStore.ts +30 -9
  49. package/src/lib/server/runtime/logExposedSurfaces.ts +8 -7
  50. package/src/lib/server/runtime/pathStore.ts +15 -0
  51. package/src/lib/server/runtime/registryManifests.ts +1 -1
  52. package/src/lib/server/runtime/renderCellBarrierStore.ts +15 -0
  53. package/src/lib/server/runtime/runWithRequestScope.ts +3 -0
  54. package/src/lib/server/runtime/serializeCacheSnapshot.ts +6 -4
  55. package/src/lib/server/runtime/snapshotEntryFromCache.ts +24 -16
  56. package/src/lib/server/runtime/streamCacheResolutions.ts +14 -7
  57. package/src/lib/server/runtime/streamFromIterator.ts +30 -2
  58. package/src/lib/server/runtime/textResponse.ts +23 -0
  59. package/src/lib/server/runtime/types/DevReloadStamp.ts +6 -10
  60. package/src/lib/server/runtime/types/InspectorCacheEntry.ts +1 -1
  61. package/src/lib/server/runtime/types/RequestStore.ts +27 -0
  62. package/src/lib/server/runtime/warnUnguardedMcp.ts +15 -5
  63. package/src/lib/server/sockets/createSocketDispatcher.ts +9 -11
  64. package/src/lib/server/sockets/defineSocket.ts +5 -4
  65. package/src/lib/server/sse.ts +5 -1
  66. package/src/lib/shared/ASYNC_CELL.ts +5 -0
  67. package/src/lib/shared/DEV_REBUILD_PATH.ts +6 -0
  68. package/src/lib/shared/HTTP_METHODS.ts +6 -0
  69. package/src/lib/shared/HttpError.ts +1 -5
  70. package/src/lib/shared/MCP_PATH.ts +6 -0
  71. package/src/lib/shared/PROXIED_SERVER_SUBDIRS.ts +15 -0
  72. package/src/lib/shared/REF_JSON_TAGS.ts +13 -1
  73. package/src/lib/shared/RPC_SHIM_GLOBALS.ts +9 -0
  74. package/src/lib/shared/activeCacheStore.ts +1 -0
  75. package/src/lib/shared/activePage.ts +1 -0
  76. package/src/lib/shared/buildArtifact.ts +20 -0
  77. package/src/lib/shared/buildRpcRequest.ts +6 -2
  78. package/src/lib/shared/buildSocketOverChannel.ts +4 -3
  79. package/src/lib/shared/bundleGraphFromMetafile.ts +68 -0
  80. package/src/lib/shared/cache.ts +156 -131
  81. package/src/lib/shared/canonicalJson.ts +24 -1
  82. package/src/lib/shared/changeAffectsClient.ts +12 -7
  83. package/src/lib/shared/createCacheStore.ts +31 -8
  84. package/src/lib/shared/createChannelLog.ts +24 -0
  85. package/src/lib/shared/createLifecycleChannel.ts +7 -1
  86. package/src/lib/shared/createReachable.ts +47 -78
  87. package/src/lib/shared/createRemoteFunction.ts +52 -30
  88. package/src/lib/shared/createRpcServerProgram.ts +820 -0
  89. package/src/lib/shared/decodeRefJson.ts +4 -4
  90. package/src/lib/shared/decodeResponse.ts +2 -2
  91. package/src/lib/shared/decodeWireBody.ts +35 -0
  92. package/src/lib/shared/detectRpcMethod.ts +8 -1
  93. package/src/lib/shared/done.ts +8 -2
  94. package/src/lib/shared/encodeRefJson.ts +4 -4
  95. package/src/lib/shared/encodeWireBody.ts +18 -0
  96. package/src/lib/shared/exitOnBuildFailure.ts +4 -3
  97. package/src/lib/{server/rpc → shared}/fieldErrorsFromIssues.ts +5 -1
  98. package/src/lib/shared/generateDeclarations.ts +72 -0
  99. package/src/lib/shared/hasSeedableRequest.ts +25 -0
  100. package/src/lib/shared/hydrationWindow.ts +53 -0
  101. package/src/lib/shared/isAsyncCell.ts +11 -0
  102. package/src/lib/shared/isAsyncIterable.ts +8 -0
  103. package/src/lib/shared/isSubscribable.ts +3 -3
  104. package/src/lib/shared/isThenable.ts +9 -0
  105. package/src/lib/shared/jsonSchemaForType.ts +258 -0
  106. package/src/lib/shared/lenientDecode.ts +15 -0
  107. package/src/lib/shared/loadProjectTsConfig.ts +28 -0
  108. package/src/lib/shared/markFrameworkSourcesIgnored.ts +1 -1
  109. package/src/lib/shared/matchRoute.ts +4 -14
  110. package/src/lib/shared/parseEnv.ts +17 -6
  111. package/src/lib/shared/peek.ts +14 -0
  112. package/src/lib/shared/pending.ts +9 -6
  113. package/src/lib/shared/pendingAsyncCellsSlot.ts +13 -0
  114. package/src/lib/shared/prepareRpcModule.ts +91 -93
  115. package/src/lib/shared/prepareSocketModule.ts +3 -2
  116. package/src/lib/shared/probeRegistries.ts +5 -18
  117. package/src/lib/shared/reachable.ts +7 -10
  118. package/src/lib/shared/refresh.ts +12 -2
  119. package/src/lib/shared/refreshing.ts +8 -2
  120. package/src/lib/shared/resolvedCellsSlot.ts +13 -0
  121. package/src/lib/shared/reviveWireField.ts +49 -0
  122. package/src/lib/shared/reviveWireOutput.ts +36 -0
  123. package/src/lib/shared/rpcServerForRoot.ts +25 -0
  124. package/src/lib/shared/scanPages.ts +25 -0
  125. package/src/lib/shared/serializeEnv.ts +18 -6
  126. package/src/lib/shared/snapshotShippable.ts +8 -7
  127. package/src/lib/shared/snippet.ts +11 -6
  128. package/src/lib/shared/streamedCellsSlot.ts +14 -0
  129. package/src/lib/shared/subscribableFromResponse.ts +4 -4
  130. package/src/lib/shared/subscribableProbes.ts +1 -1
  131. package/src/lib/shared/tailProbeSlot.ts +1 -1
  132. package/src/lib/shared/types/AsyncComputed.ts +20 -0
  133. package/src/lib/shared/types/AsyncState.ts +13 -0
  134. package/src/lib/shared/types/CacheEntry.ts +7 -7
  135. package/src/lib/shared/types/CacheOptions.ts +23 -37
  136. package/src/lib/shared/types/CachePolicy.ts +25 -0
  137. package/src/lib/shared/types/CacheSnapshotEntry.ts +6 -5
  138. package/src/lib/shared/types/ErrorJsonSchemas.ts +8 -0
  139. package/src/lib/shared/types/HttpMethod.ts +3 -1
  140. package/src/lib/shared/types/InputCoercion.ts +17 -0
  141. package/src/lib/shared/types/{Subscribable.ts → NamedAsyncIterable.ts} +1 -1
  142. package/src/lib/shared/types/OutputWirePlan.ts +17 -0
  143. package/src/lib/shared/types/PagesScan.ts +7 -0
  144. package/src/lib/shared/types/PendingAsyncCells.ts +10 -0
  145. package/src/lib/shared/types/RawRemoteFunction.ts +6 -0
  146. package/src/lib/shared/types/RemoteCallable.ts +8 -7
  147. package/src/lib/shared/types/RemoteFunction.ts +16 -15
  148. package/src/lib/shared/types/ResolvedCells.ts +11 -0
  149. package/src/lib/shared/types/ReturnBody.ts +15 -0
  150. package/src/lib/shared/types/RpcBuildStamps.ts +23 -0
  151. package/src/lib/shared/types/RpcErrorGuard.ts +3 -8
  152. package/src/lib/shared/types/Socket.ts +4 -4
  153. package/src/lib/shared/types/SsrPayload.ts +5 -1
  154. package/src/lib/shared/types/StreamPolicy.ts +11 -0
  155. package/src/lib/shared/types/StreamedCells.ts +19 -0
  156. package/src/lib/shared/types/StreamedResolution.ts +24 -6
  157. package/src/lib/shared/types/TailHooks.ts +1 -1
  158. package/src/lib/shared/types/WireKind.ts +11 -0
  159. package/src/lib/shared/url.ts +5 -0
  160. package/src/lib/shared/validationHttpError.ts +33 -0
  161. package/src/lib/shared/warmSeedKey.ts +16 -0
  162. package/src/lib/shared/wireJsonReplacer.ts +30 -0
  163. package/src/lib/shared/writeRpcDts.ts +11 -1
  164. package/src/lib/shared/writeTestSocketsDts.ts +1 -1
  165. package/src/lib/test/createTestApp.ts +19 -1
  166. package/src/lib/ui/README.md +1 -1
  167. package/src/lib/ui/activePendingCells.ts +14 -0
  168. package/src/lib/ui/compile/BLOCK_KEYWORDS.ts +21 -0
  169. package/src/lib/ui/compile/SSR_ESCAPE.ts +4 -1
  170. package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +35 -7
  171. package/src/lib/ui/compile/abideUiPlugin.ts +19 -2
  172. package/src/lib/ui/compile/analyzeComponent.ts +89 -6
  173. package/src/lib/ui/compile/assignmentTargetNames.ts +54 -0
  174. package/src/lib/ui/compile/asyncInterpolationFields.ts +152 -0
  175. package/src/lib/ui/compile/asyncValuePositionError.ts +21 -0
  176. package/src/lib/ui/compile/asyncValuePositionInterpolations.ts +64 -0
  177. package/src/lib/ui/compile/attrLiftPosition.ts +18 -0
  178. package/src/lib/ui/compile/bindListenEvent.ts +5 -6
  179. package/src/lib/ui/compile/cachedSourceFile.ts +40 -0
  180. package/src/lib/ui/compile/catchBinding.ts +7 -5
  181. package/src/lib/ui/compile/classifyInterpolationType.ts +46 -0
  182. package/src/lib/ui/compile/collectAbideDiagnostics.ts +96 -0
  183. package/src/lib/ui/compile/compileComponent.ts +19 -3
  184. package/src/lib/ui/compile/compileModule.ts +27 -58
  185. package/src/lib/ui/compile/compileSSR.ts +52 -13
  186. package/src/lib/ui/compile/compileShadow.ts +265 -42
  187. package/src/lib/ui/compile/composeProps.ts +15 -4
  188. package/src/lib/ui/compile/createShadowLanguageService.ts +116 -68
  189. package/src/lib/ui/compile/createShadowProgram.ts +35 -6
  190. package/src/lib/ui/compile/declaredNames.ts +36 -0
  191. package/src/lib/ui/compile/desugarSignals.ts +419 -37
  192. package/src/lib/ui/compile/expressionIsPrefixEvaluable.ts +19 -0
  193. package/src/lib/ui/compile/generateBuild.ts +74 -17
  194. package/src/lib/ui/compile/generateSSR.ts +286 -75
  195. package/src/lib/ui/compile/hoistableAwaits.ts +193 -0
  196. package/src/lib/ui/compile/hoistableChildRenders.ts +112 -0
  197. package/src/lib/ui/compile/interpolatedTemplateLiteral.ts +3 -1
  198. package/src/lib/ui/compile/interpolationClassifierForRoot.ts +37 -0
  199. package/src/lib/ui/compile/isSpuriousAsyncReadDiagnostic.ts +174 -0
  200. package/src/lib/ui/compile/isWhitespaceText.ts +10 -2
  201. package/src/lib/ui/compile/liftAsyncSubExpressions.ts +166 -0
  202. package/src/lib/ui/compile/lowerAsyncInterpolations.ts +92 -0
  203. package/src/lib/ui/compile/lowerContext.ts +10 -0
  204. package/src/lib/ui/compile/lowerDocAccess.ts +51 -10
  205. package/src/lib/ui/compile/lowerScript.ts +40 -4
  206. package/src/lib/ui/compile/nodeAtShadowOffset.ts +28 -0
  207. package/src/lib/ui/compile/parseTemplate.ts +18 -1090
  208. package/src/lib/ui/compile/parseTemplateRecovering.ts +1385 -0
  209. package/src/lib/ui/compile/referencedIdentifiers.ts +35 -0
  210. package/src/lib/ui/compile/renameSignalRefs.ts +26 -40
  211. package/src/lib/ui/compile/resolveReactiveExport.ts +4 -7
  212. package/src/lib/ui/compile/scopeCss.ts +27 -4
  213. package/src/lib/ui/compile/seedTypeClassifierForRoot.ts +65 -0
  214. package/src/lib/ui/compile/shadowInterpolationClassifier.ts +47 -0
  215. package/src/lib/ui/compile/sourceFileOptionsSignature.ts +20 -0
  216. package/src/lib/ui/compile/structuralHeadTokens.ts +107 -0
  217. package/src/lib/ui/compile/templateSemanticTokens.ts +21 -0
  218. package/src/lib/ui/compile/templateStartOffset.ts +15 -0
  219. package/src/lib/ui/compile/tryPlan.ts +4 -3
  220. package/src/lib/ui/compile/types/AnalyzedComponent.ts +4 -0
  221. package/src/lib/ui/compile/types/AsyncInterpolationField.ts +19 -0
  222. package/src/lib/ui/compile/types/InterpolationClassifier.ts +12 -0
  223. package/src/lib/ui/compile/types/InterpolationKind.ts +7 -0
  224. package/src/lib/ui/compile/types/ParseDiagnostic.ts +8 -0
  225. package/src/lib/ui/compile/types/SeedTypeClassifier.ts +15 -0
  226. package/src/lib/ui/compile/types/TemplateNode.ts +36 -4
  227. package/src/lib/ui/compile/types/ValuePositionInterpolation.ts +13 -0
  228. package/src/lib/ui/compile/writtenTemplateNames.ts +84 -0
  229. package/src/lib/ui/computed.ts +28 -1
  230. package/src/lib/ui/createScope.ts +35 -68
  231. package/src/lib/ui/dom/anchoredBranch.ts +142 -0
  232. package/src/lib/ui/dom/appendText.ts +8 -3
  233. package/src/lib/ui/dom/awaitBlock.ts +62 -75
  234. package/src/lib/ui/dom/bindProp.ts +22 -0
  235. package/src/lib/ui/dom/bindableProp.ts +47 -0
  236. package/src/lib/ui/dom/cellPending.ts +24 -0
  237. package/src/lib/ui/dom/discardBoundary.ts +24 -6
  238. package/src/lib/ui/dom/disposeRange.ts +2 -1
  239. package/src/lib/ui/dom/each.ts +41 -12
  240. package/src/lib/ui/dom/eachAsync.ts +39 -3
  241. package/src/lib/ui/dom/fillBefore.ts +6 -7
  242. package/src/lib/ui/dom/fillBoundary.ts +2 -3
  243. package/src/lib/ui/dom/fillRange.ts +4 -4
  244. package/src/lib/ui/dom/hydrate.ts +6 -12
  245. package/src/lib/ui/dom/isComment.ts +1 -1
  246. package/src/lib/ui/dom/matchingRangeClose.ts +29 -0
  247. package/src/lib/ui/dom/mergeProps.ts +1 -1
  248. package/src/lib/ui/dom/mount.ts +1 -2
  249. package/src/lib/ui/dom/mountChild.ts +9 -40
  250. package/src/lib/ui/dom/mountRange.ts +2 -3
  251. package/src/lib/ui/dom/mountSlot.ts +1 -1
  252. package/src/lib/ui/dom/mountStreamedChild.ts +84 -0
  253. package/src/lib/ui/dom/mountSwappableRange.ts +46 -4
  254. package/src/lib/ui/dom/mutateDocContainer.ts +65 -0
  255. package/src/lib/ui/dom/on.ts +2 -2
  256. package/src/lib/ui/dom/readCell.ts +40 -0
  257. package/src/lib/ui/dom/restProps.ts +2 -2
  258. package/src/lib/ui/dom/spreadProps.ts +3 -4
  259. package/src/lib/ui/dom/switchBlock.ts +30 -7
  260. package/src/lib/ui/dom/tryBlock.ts +233 -70
  261. package/src/lib/ui/dom/types/SwitchCase.ts +5 -1
  262. package/src/lib/ui/dom/when.ts +12 -2
  263. package/src/lib/ui/dom/withScope.ts +9 -2
  264. package/src/lib/ui/finalizeStreamedChildren.ts +89 -0
  265. package/src/lib/ui/flight.ts +56 -0
  266. package/src/lib/ui/html.ts +12 -1
  267. package/src/lib/ui/isolateCellBarrier.ts +17 -0
  268. package/src/lib/ui/linked.ts +48 -2
  269. package/src/lib/ui/props.ts +17 -0
  270. package/src/lib/ui/remoteProxy.ts +84 -159
  271. package/src/lib/ui/renderChain.ts +53 -13
  272. package/src/lib/ui/renderToStream.ts +22 -18
  273. package/src/lib/ui/resumeSeedScript.ts +1 -1
  274. package/src/lib/ui/router.ts +86 -53
  275. package/src/lib/ui/runtime/AsyncCellError.ts +20 -0
  276. package/src/lib/ui/runtime/CELL_SEED.ts +14 -0
  277. package/src/lib/ui/runtime/CHILD_PRESENT.ts +2 -2
  278. package/src/lib/ui/runtime/CURRENT_BOUNDARY.ts +11 -0
  279. package/src/lib/ui/runtime/CURRENT_PATH.ts +29 -0
  280. package/src/lib/ui/runtime/RENDER.ts +10 -7
  281. package/src/lib/ui/runtime/RESUME.ts +4 -4
  282. package/src/lib/ui/runtime/STREAMED_CELLS.ts +57 -0
  283. package/src/lib/ui/runtime/ambientPathBacking.ts +32 -0
  284. package/src/lib/ui/runtime/applyPatchToTree.ts +17 -4
  285. package/src/lib/ui/runtime/blockId.ts +31 -0
  286. package/src/lib/ui/runtime/boundaryFor.ts +11 -0
  287. package/src/lib/ui/runtime/cellBarrierBacking.ts +30 -0
  288. package/src/lib/ui/runtime/createAsyncCell.ts +300 -0
  289. package/src/lib/ui/runtime/createDoc.ts +31 -54
  290. package/src/lib/ui/runtime/createEffectNode.ts +9 -0
  291. package/src/lib/ui/runtime/enterRenderPass.ts +5 -4
  292. package/src/lib/ui/runtime/flushEffects.ts +33 -1
  293. package/src/lib/ui/runtime/isAsyncFunction.ts +14 -0
  294. package/src/lib/ui/runtime/nextBlockId.ts +8 -7
  295. package/src/lib/ui/runtime/renderPath.ts +16 -0
  296. package/src/lib/ui/runtime/scope.ts +11 -0
  297. package/src/lib/ui/runtime/toTeardown.ts +10 -3
  298. package/src/lib/ui/runtime/types/Boundary.ts +9 -0
  299. package/src/lib/ui/runtime/types/RenderContext.ts +9 -7
  300. package/src/lib/ui/runtime/types/SsrRender.ts +9 -2
  301. package/src/lib/ui/runtime/types/UiComponent.ts +1 -5
  302. package/src/lib/ui/runtime/types/UiProps.ts +6 -5
  303. package/src/lib/ui/runtime/withOptionalPath.ts +8 -0
  304. package/src/lib/ui/runtime/withPath.ts +16 -0
  305. package/src/lib/ui/runtime/withPathFrom.ts +20 -0
  306. package/src/lib/ui/runtime/withoutHydration.ts +22 -0
  307. package/src/lib/ui/seedStreamedResolution.ts +31 -6
  308. package/src/lib/ui/settleAsyncCells.ts +24 -0
  309. package/src/lib/ui/socketProxy.ts +1 -1
  310. package/src/lib/ui/startClient.ts +16 -31
  311. package/src/lib/ui/state.ts +9 -3
  312. package/src/lib/ui/trackedComputed.ts +68 -0
  313. package/src/lib/ui/types/Scope.ts +8 -24
  314. package/src/lib/ui/watch.ts +3 -3
  315. package/src/serverEntry.ts +14 -0
  316. package/template/src/server/rpc/getHello.ts +15 -13
  317. package/template/test/app.test.ts +1 -1
  318. package/src/lib/server/runtime/devHotModuleResponse.ts +0 -41
  319. package/src/lib/shared/DEV_HOT_PREFIX.ts +0 -7
  320. package/src/lib/shared/UNREACHABLE_STATUSES.ts +0 -13
  321. package/src/lib/shared/hasReplayableRequest.ts +0 -17
  322. package/src/lib/shared/hydratingSlot.ts +0 -12
  323. package/src/lib/shared/outboxProbeSlot.ts +0 -20
  324. package/src/lib/shared/types/Outbox.ts +0 -9
  325. package/src/lib/shared/types/OutboxEntry.ts +0 -27
  326. package/src/lib/shared/types/SmartReadOptions.ts +0 -36
  327. package/src/lib/shared/wakeHydrationPeeks.ts +0 -20
  328. package/src/lib/ui/COMPONENT_WRAPPER_PREFIX.ts +0 -5
  329. package/src/lib/ui/compile/REACTIVE_CALLEES.ts +0 -11
  330. package/src/lib/ui/compile/assertRuntimeHelpersBound.ts +0 -80
  331. package/src/lib/ui/compile/markupTokens.ts +0 -313
  332. package/src/lib/ui/compile/structuralBlockTokens.ts +0 -100
  333. package/src/lib/ui/dom/applyResolved.ts +0 -87
  334. package/src/lib/ui/dom/scopeLabel.ts +0 -21
  335. package/src/lib/ui/dom/text.ts +0 -20
  336. package/src/lib/ui/history.ts +0 -101
  337. package/src/lib/ui/installHotBridge.ts +0 -93
  338. package/src/lib/ui/installInspectorBridge.ts +0 -140
  339. package/src/lib/ui/outbox.ts +0 -35
  340. package/src/lib/ui/persist.ts +0 -115
  341. package/src/lib/ui/rpcOutbox/createOutboxQueue.ts +0 -281
  342. package/src/lib/ui/rpcOutbox/outboxRegistry.ts +0 -69
  343. package/src/lib/ui/runtime/PATCH_BUS.ts +0 -28
  344. package/src/lib/ui/runtime/captureModelDoc.ts +0 -28
  345. package/src/lib/ui/runtime/hotInstances.ts +0 -10
  346. package/src/lib/ui/runtime/hotReloadEnabled.ts +0 -8
  347. package/src/lib/ui/runtime/hotReplace.ts +0 -38
  348. package/src/lib/ui/runtime/liveScopes.ts +0 -15
  349. package/src/lib/ui/runtime/localStoragePersistence.ts +0 -47
  350. package/src/lib/ui/runtime/registerHotInstance.ts +0 -23
  351. package/src/lib/ui/runtime/seedModelDoc.ts +0 -26
  352. package/src/lib/ui/runtime/types/HotInstance.ts +0 -22
  353. package/src/lib/ui/runtime/types/PatchEvent.ts +0 -16
  354. package/src/lib/ui/seedResolved.ts +0 -28
  355. package/src/lib/ui/sync.ts +0 -48
  356. package/src/lib/ui/types/History.ts +0 -14
  357. package/src/lib/ui/types/PersistHandle.ts +0 -11
  358. package/src/lib/ui/types/PersistenceStore.ts +0 -12
  359. package/src/lib/ui/types/ResolvedFrame.ts +0 -15
  360. package/src/lib/ui/types/SyncTransport.ts +0 -13
@@ -1,102 +1,265 @@
1
- import { claimChild } from '../runtime/claimChild.ts'
1
+ import { effect } from '../effect.ts'
2
+ import { AsyncCellError } from '../runtime/AsyncCellError.ts'
3
+ import { CURRENT_BOUNDARY } from '../runtime/CURRENT_BOUNDARY.ts'
4
+ import { claimExpected } from '../runtime/claimExpected.ts'
2
5
  import { OWNER } from '../runtime/OWNER.ts'
3
6
  import { RENDER } from '../runtime/RENDER.ts'
4
7
  import { scopeGroup } from '../runtime/scopeGroup.ts'
8
+ import type { Boundary } from '../runtime/types/Boundary.ts'
9
+ import type { State } from '../runtime/types/State.ts'
10
+ import { withoutHydration } from '../runtime/withoutHydration.ts'
11
+ import { state } from '../state.ts'
12
+ import { anchoredBranch } from './anchoredBranch.ts'
5
13
  import { discardBoundary } from './discardBoundary.ts'
6
- import { enterNamespace } from './enterNamespace.ts'
7
14
 
8
15
  /*
9
- Synchronous error boundary — the runtime for `<template try>`. Builds the guarded
10
- subtree (`renderTry`); if building it throws including a throw from an initial
11
- reactive read, since effects run during build it tears down the partial scope and
12
- builds `renderCatch(error)` instead. Each branch builds its content (any content)
13
- into the parent. No `renderCatch` (no `<template catch>`) re-throws, so the error
14
- propagates to the nearest enclosing boundary. The block renders once and never
15
- re-renders, so its content needs no range markers — an enclosing block's range
16
- removes it on teardown.
16
+ Reactive error boundary — the runtime for `<template try>` (ADR-0019 D3). Unlike the old
17
+ synchronous render-once version it is render-MANY and reactive both directions: it catches
18
+ a build throw AND an initial reactive-read throw (as before) AND the headline capability
19
+ a throw from a LATER re-run, where an async error lives (an async cell that rejects after
20
+ mount, read via a throwing peek `AsyncCellError`).
17
21
 
18
- On create the guarded content is built into a fragment first, so a throw mid-build
19
- discards the partial nodes (they never entered the document) before the catch
20
- builds. On hydrate it claims the SSR boundary
21
- (`<!--abide:try:N-->…<!--/abide:try:N-->`): the happy path adopts the guarded nodes
22
- in place; a throw discards the boundary's server nodes and builds the catch fresh.
22
+ Each branch's content lives in a `[`…`]`-bracketed RANGE (the model `when`/`await`/`each`
23
+ share) parked before an anchor, so a swap detaches one unit and builds the other. The
24
+ guarded branch builds under an ambient `CURRENT_BOUNDARY` so every effect created inside
25
+ associates with this boundary (`boundaryFor`); when such an effect throws on a re-run the
26
+ scheduler (`flushEffects.drain`) calls `boundary.handle(error)` which swaps to the catch
27
+ branch. No `renderCatch` (no `<template catch>`) means no boundary is installed, so a later
28
+ throw propagates to the nearest ENCLOSING `{#try}` unchanged, and an initial throw rethrows.
29
+
30
+ Keep-the-watch: the terminal alternative would dispose the guarded scope on throw, killing
31
+ the only subscription to the failing cell — the boundary would go deaf to recovery. So the
32
+ throw carries the originating cell (`AsyncCellError.cell`); on catching an async-cell error
33
+ the boundary subscribes to that cell's lifecycle and, when it recovers (error→value via a
34
+ `refresh()` or a dep change), rebuilds the guarded content fresh — one rebuild, then
35
+ fine-grained. On catch→catch (a fresh error while still failing) the catch branch rebuilds
36
+ with the new error (the codegen catch binding is a plain local, so the error passes by
37
+ value; in-place `err` update is a v2 refinement).
38
+
39
+ Hydration claims the SSR boundary (`<!--abide:try:N-->…<!--/abide:try:N-->`): the happy
40
+ path adopts the guarded nodes in place and brackets them into a range (so the first swap
41
+ detaches them like any later one); a throw discards the server nodes and builds the catch
42
+ fresh.
23
43
  */
24
44
  // @documentation plumbing
25
45
  export function tryBlock(
26
46
  parent: Node,
27
- id: number,
47
+ id: string,
28
48
  renderTry: (parent: Node) => void,
29
- renderCatch?: (parent: Node, error: unknown) => void,
49
+ renderCatch?: (parent: Node, error: State<unknown>) => void,
30
50
  before: Node | null = null,
31
51
  ): void {
32
- /* The guarded subtree's scope, registered with the owner so it disposes on owner
33
- teardown. The block renders once, so there is at most one tracked subtree (the
34
- try branch, or the catch branch if try threw). */
52
+ const hydration = RENDER.hydration
53
+ /* The live branch's scope, registered with the owner so it disposes on owner teardown —
54
+ not only when a swap detaches it. */
35
55
  const group = scopeGroup()
36
- /* Run a void build under a fresh ownership scope. On success, hand its disposers to
37
- the group so the subtree tears down with the owner (they were previously dropped —
38
- the leak). On throw, tear down the partial build now and rethrow so the caller can
39
- fall back to the catch branch. */
40
- const guard = (build: () => void): void => {
41
- const previous = OWNER.current
42
- const disposers: Array<() => void> = []
43
- OWNER.current = disposers
44
- const disposeAll = (): void => {
45
- for (let index = disposers.length - 1; index >= 0; index -= 1) {
46
- disposers[index]?.()
47
- }
56
+ /* Only a block WITH a catch branch installs a boundary: with no catch it cannot handle a
57
+ throw, so its guarded effects must inherit the ENCLOSING boundary (a nested throw then
58
+ propagates outward) and an initial throw rethrows. */
59
+ const hasCatch = renderCatch !== undefined
60
+ /* The single active branch mounted before an anchor — owns `active`/`detach`/`place`/
61
+ adopt-strand guard (shared with `awaitBlock`); this block keeps its bespoke boundary
62
+ recovery. */
63
+ const branch = anchoredBranch(parent, group)
64
+ /* The keep-the-watch subscription on the throwing cell, live while the catch branch is
65
+ shown (async-cell errors only); disposed on recover / swap / teardown. */
66
+ let watchDispose: (() => void) | undefined
67
+ /* Which branch is mounted, and the reactive error cell the catch branch reads. A
68
+ catch→catch (a fresh error while still failing) WRITES this cell — the `err` binding
69
+ re-runs in place, no rebuild — mirroring how `awaitBlock` updates its then-value cell. */
70
+ let activeKind: 'try' | 'catch' | undefined
71
+ let errorCell: State<unknown> | undefined
72
+
73
+ const boundary: Boundary = { handle: (error) => showCatch(error) }
74
+
75
+ const disposeWatch = (): void => {
76
+ if (watchDispose !== undefined) {
77
+ watchDispose()
78
+ watchDispose = undefined
48
79
  }
80
+ }
81
+
82
+ /* Install this boundary as the ambient one around the guarded branch's build (passed as
83
+ `place`'s `wrapBuild`) so its effects associate for later-throw routing; the catch branch
84
+ builds WITHOUT it, so a throw in catch content propagates to the enclosing boundary rather
85
+ than back to this one. */
86
+ const withBoundary = (run: () => void): void => {
87
+ const previousBoundary = CURRENT_BOUNDARY.current
88
+ CURRENT_BOUNDARY.current = boundary
49
89
  try {
50
- build()
51
- OWNER.current = previous
52
- group.track(disposeAll)
53
- } catch (error) {
54
- OWNER.current = previous
55
- disposeAll()
56
- throw error
90
+ run()
91
+ } finally {
92
+ CURRENT_BOUNDARY.current = previousBoundary
57
93
  }
58
94
  }
59
95
 
60
- const hydration = RENDER.hydration
61
- if (hydration !== undefined) {
62
- const open = claimChild(hydration, parent)
63
- hydration.next.set(parent, open?.nextSibling ?? null) // advance past the open marker
96
+ /* Build (or rebuild) the guarded branch. A SYNCHRONOUS throw during the build — an
97
+ initial reactive-read throw or a plain build throw — falls to the catch branch (or
98
+ rethrows with no catch, preserving the old boundary's semantics). Later-run throws
99
+ arrive via `boundary.handle` instead. */
100
+ const showTry = (): void => {
101
+ disposeWatch()
64
102
  try {
65
- guard(() => renderTry(parent)) // claims the guarded nodes in place
66
- const close = claimChild(hydration, parent) // claim the close marker
67
- hydration.next.set(parent, close?.nextSibling ?? null)
103
+ branch.place((host) => renderTry(host), hasCatch ? withBoundary : undefined)
104
+ activeKind = 'try'
105
+ errorCell = undefined
68
106
  } catch (error) {
69
- /* The server markup didn't adopt — drop the whole boundary and build the
70
- catch fresh in its place. */
71
- const after = discardBoundary(parent, open, `/abide:try:${id}`, hydration)
72
- if (renderCatch === undefined) {
107
+ if (!hasCatch) {
73
108
  throw error
74
109
  }
75
- const previous = RENDER.hydration
76
- RENDER.hydration = undefined
77
- try {
78
- const fragment = document.createDocumentFragment()
79
- enterNamespace(parent, () => guard(() => renderCatch(fragment, error)))
80
- parent.insertBefore(fragment, after)
81
- } finally {
82
- RENDER.hydration = previous
83
- }
110
+ showCatch(error)
111
+ }
112
+ }
113
+
114
+ /* Swap to the catch branch. A catch→catch (a fresh error while the catch branch is
115
+ already mounted) updates the reactive `err` cell IN PLACE — the binding re-runs, no
116
+ rebuild, focus/scroll inside the catch branch survive. Otherwise (try→catch, or the
117
+ initial throw) it builds the catch branch fresh around a new error cell. Either way it
118
+ keeps a watch on the throwing async cell so a recovery re-arms the guarded branch. */
119
+ const showCatch = (error: unknown): void => {
120
+ if (activeKind === 'catch' && errorCell !== undefined) {
121
+ errorCell.value = error
122
+ rewatch(error)
123
+ return
124
+ }
125
+ const cell = state<unknown>(error)
126
+ errorCell = cell
127
+ branch.place((host) => (renderCatch as NonNullable<typeof renderCatch>)(host, cell))
128
+ activeKind = 'catch'
129
+ rewatch(error)
130
+ }
131
+
132
+ /* (Re)subscribe the keep-the-watch to whatever cell threw this time. Only a reactive
133
+ async-cell error carries a source to watch; a plain render bug is terminal in v1 (a bug
134
+ is not a data state that self-heals). */
135
+ const rewatch = (error: unknown): void => {
136
+ disposeWatch()
137
+ if (error instanceof AsyncCellError) {
138
+ watchCell(error.cell)
139
+ }
140
+ }
141
+
142
+ /* Subscribe to the throwing cell's lifecycle. On recovery (its error clears or a value
143
+ arrives — error→value via `refresh()` or a dep change) rebuild the guarded branch
144
+ fresh; on a fresh error while still failing, rebuild catch with the new error. Created
145
+ OUTSIDE this boundary and the owner (it must not route its own reads here and must not
146
+ double-register), tracked in the group so it disposes with the block. */
147
+ const watchCell = (cell: AsyncCellError['cell']): void => {
148
+ const previousBoundary = CURRENT_BOUNDARY.current
149
+ const previousOwner = OWNER.current
150
+ CURRENT_BOUNDARY.current = undefined
151
+ OWNER.current = undefined
152
+ let firstRun = true
153
+ let dispose: () => void
154
+ try {
155
+ dispose = effect(() => {
156
+ const cellError = cell.error()
157
+ const value = cell.peek()
158
+ /* The first run only subscribes — the cell is still in its error state that
159
+ drove us here, so acting on it would immediately re-arm. */
160
+ if (firstRun) {
161
+ firstRun = false
162
+ return
163
+ }
164
+ /* Recovered: rebuild guarded fresh. Still failing with a fresh error: rebuild
165
+ catch with it. Either path disposes THIS watch first (safe: an effect fully
166
+ unlinks itself, so its own end-of-run tracking trims nothing). */
167
+ if (cellError === undefined || value !== undefined) {
168
+ showTry()
169
+ } else {
170
+ showCatch(new AsyncCellError(cell, cellError))
171
+ }
172
+ })
173
+ } finally {
174
+ CURRENT_BOUNDARY.current = previousBoundary
175
+ OWNER.current = previousOwner
84
176
  }
177
+ watchDispose = group.track(dispose)
178
+ }
179
+
180
+ if (hydration !== undefined) {
181
+ firstHydrate()
85
182
  return
86
183
  }
184
+ const anchorNode = document.createTextNode('')
185
+ branch.anchor = anchorNode
186
+ parent.insertBefore(anchorNode, before)
187
+ showTry()
188
+
189
+ /* The first run when hydrating: claim the boundary's open marker, adopt the guarded
190
+ server nodes in place, then bracket them into a range so the first swap detaches them
191
+ like every later one. A guarded throw (the server rendered catch, or the client build
192
+ throws too) discards the boundary and builds the catch fresh. */
193
+ function firstHydrate(): void {
194
+ const cursor = hydration as NonNullable<typeof hydration>
195
+ const open = claimExpected(cursor, parent, `abide:try:${id} open marker`)
196
+ try {
197
+ adopt(open)
198
+ } catch (error) {
199
+ rebuildCold(open, error)
200
+ }
201
+ }
202
+
203
+ /* Adopt the guarded branch in place: claim the server's `[` range-open marker, build the
204
+ guarded content (which claims its nodes) under the boundary, then claim the `]`
205
+ range-close and the boundary close, parking an anchor so a later swap detaches this
206
+ range like any other. A build throw disposes the partial scope and rethrows so
207
+ `firstHydrate` rebuilds cold. */
208
+ function adopt(open: Node): void {
209
+ const cursor = hydration as NonNullable<typeof hydration>
210
+ cursor.next.set(parent, open.nextSibling ?? null)
211
+ /* The server emits the `[ … ]` range inside the boundary — claim `[` as the live
212
+ range start, then advance the cursor to the first content node. */
213
+ const start = claimExpected(cursor, parent, `abide:try:${id} range-open marker`) as Comment
214
+ cursor.next.set(parent, start.nextSibling ?? null)
215
+ /* Install the boundary around the whole adopt (build + marker claims); the shared
216
+ strand-dispose guard in `adoptStrand` disposes the partial scope and rethrows on any
217
+ failure, and this `finally` restores the boundary before the rethrow reaches
218
+ `firstHydrate`. */
219
+ const previousBoundary = CURRENT_BOUNDARY.current
220
+ if (hasCatch) {
221
+ CURRENT_BOUNDARY.current = boundary
222
+ }
223
+ try {
224
+ branch.adoptStrand(
225
+ (host) => renderTry(host),
226
+ () => {
227
+ const end = claimExpected(
228
+ cursor,
229
+ parent,
230
+ `abide:try:${id} range-close marker`,
231
+ ) as Comment
232
+ cursor.next.set(parent, end.nextSibling ?? null)
233
+ const close = claimExpected(cursor, parent, `/abide:try:${id} close marker`)
234
+ cursor.next.set(parent, close.nextSibling ?? null)
235
+ const anchorNode = document.createTextNode('')
236
+ parent.insertBefore(anchorNode, close)
237
+ branch.anchor = anchorNode
238
+ return { start, end }
239
+ },
240
+ )
241
+ activeKind = 'try'
242
+ } finally {
243
+ CURRENT_BOUNDARY.current = previousBoundary
244
+ }
245
+ }
87
246
 
88
- /* Create: build into a fragment so a throw mid-build discards the partial nodes
89
- (they never entered the document) before the catch builds. */
90
- try {
91
- const fragment = document.createDocumentFragment()
92
- enterNamespace(parent, () => guard(() => renderTry(fragment)))
93
- parent.insertBefore(fragment, before)
94
- } catch (error) {
95
- if (renderCatch === undefined) {
247
+ /* Discard the SSR boundary and build the catch branch fresh in its place (hydration off).
248
+ No catch → rethrow (the throw surfaces past the boundary, as the sync version did). */
249
+ function rebuildCold(open: Node, error: unknown): void {
250
+ branch.detach()
251
+ const after = discardBoundary(
252
+ parent,
253
+ open,
254
+ `/abide:try:${id}`,
255
+ hydration as NonNullable<typeof hydration>,
256
+ )
257
+ if (!hasCatch) {
96
258
  throw error
97
259
  }
98
- const fragment = document.createDocumentFragment()
99
- enterNamespace(parent, () => guard(() => renderCatch(fragment, error)))
100
- parent.insertBefore(fragment, before)
260
+ const anchorNode = document.createTextNode('')
261
+ branch.anchor = anchorNode
262
+ parent.insertBefore(anchorNode, after)
263
+ withoutHydration(() => showCatch(error))
101
264
  }
102
265
  }
@@ -1,7 +1,11 @@
1
1
  /* One branch of a `switch`: `match` returns the value this case selects on
2
2
  (undefined = the default branch); `render` builds the branch's content into the
3
- parent (the block tracks it as a range between markers). */
3
+ parent (the block tracks it as a range between markers). `pending` (only on an
4
+ async `{#if}`/`{:elseif}` cond-chain branch) reports whether this branch's own
5
+ async condition is still loading — a true reading holds the whole chain here, so a
6
+ later branch never renders on a not-yet-known earlier condition. */
4
7
  export type SwitchCase = {
5
8
  match: (() => unknown) | undefined
6
9
  render: (parent: Node) => void
10
+ pending?: () => boolean
7
11
  }
@@ -8,6 +8,12 @@ just element roots. A 2-case swappable range tracks `condition()` and swaps the
8
8
  range's content on a truthy↔falsy flip (`render` truthy, `renderElse` falsy); an
9
9
  unchanged condition is a no-op. See `mountSwappableRange` for the shared
10
10
  hydrate/swap/teardown semantics.
11
+
12
+ `isPending` (compiler-supplied only for a bare async subject) adds a third state: while
13
+ it returns true the block renders NEITHER branch, so a still-loading promise never flashes
14
+ its `{:else}` before settling — "pending" is not conflated with a settled falsy value. It is
15
+ consulted first, so `condition()` (the throwing peek) runs only once the cell is no longer
16
+ pending; an error then throws to the nearest `{#try}`.
11
17
  */
12
18
  // @documentation plumbing
13
19
  export function when(
@@ -16,11 +22,15 @@ export function when(
16
22
  render: (parent: Node) => void,
17
23
  renderElse?: (parent: Node) => void,
18
24
  before: Node | null = null,
25
+ isPending?: () => boolean,
19
26
  ): void {
20
27
  mountSwappableRange(
21
28
  parent,
22
- () => (condition() ? 'then' : 'else'),
23
- (branch) => (branch === 'then' ? render : renderElse),
29
+ () => (isPending?.() === true ? 'pending' : condition() ? 'then' : 'else'),
30
+ (branch) => (branch === 'then' ? render : branch === 'else' ? renderElse : undefined),
24
31
  before,
32
+ /* An async subject (compiler supplies `isPending`) can render a different branch on the
33
+ server than the pending client, so hydration must not adopt the SSR range in place. */
34
+ isPending !== undefined,
25
35
  )
26
36
  }
@@ -15,13 +15,20 @@ build, so the restore is exact). `build` returns its reactivity stopper (from `s
15
15
  the caller wraps its own DOM teardown (clear host vs clear range) around one
16
16
  `lexical.dispose()`, no longer composing a separate `stop()` at every site.
17
17
  */
18
- export function withScope(label: string | undefined, build: () => () => void): { lexical: Scope } {
18
+ export function withScope(build: () => () => void): { lexical: Scope } {
19
19
  const parentScope = CURRENT_SCOPE.current
20
- const lexical = createScope({}, parentScope, true, label)
20
+ const lexical = createScope({}, parentScope, true)
21
21
  enterRenderPass()
22
22
  CURRENT_SCOPE.current = lexical
23
23
  try {
24
24
  lexical.own(build())
25
+ } catch (error) {
26
+ /* `build` threw before returning its stopper — e.g. a hydration desync in one of
27
+ several sibling blocks. `own` never ran, so nothing downstream will dispose this
28
+ lexical scope; tear it down here (its child scopes/effects created before the
29
+ throw) before rethrowing, rather than leaking a live, unreachable scope. */
30
+ lexical.dispose()
31
+ throw error
25
32
  } finally {
26
33
  exitRenderPass()
27
34
  CURRENT_SCOPE.current = parentScope
@@ -0,0 +1,89 @@
1
+ import type { FlightPromise } from './flight.ts'
2
+ import { RANGE_CLOSE, RANGE_OPEN } from './runtime/RANGE_MARKER.ts'
3
+ import type { ResumeEntry } from './runtime/RESUME.ts'
4
+ import type { SsrAwait, SsrRender } from './runtime/types/SsrRender.ts'
5
+
6
+ /* Comment-wrapped range markers — imported from the shared constant (not a local literal) so the
7
+ inline branch below can never drift from `generateSSR`'s own range emit. */
8
+ const OPEN = `<!--${RANGE_OPEN}-->`
9
+ const CLOSE = `<!--${RANGE_CLOSE}-->`
10
+
11
+ /* One hoistable child render staged by generateSSR: the reserved index in its output array, the
12
+ array itself, the child's render-path (the streamed boundary id), and its in-flight render. */
13
+ export type StagedChild = {
14
+ slot: number
15
+ out: string[]
16
+ id: string
17
+ flight: FlightPromise
18
+ }
19
+
20
+ /*
21
+ The ADR-0039 WHEN-TO-STREAM decision, run once after a component's body walk (compileSSR emits
22
+ `await $$finalizeStreamedChildren($childSlots, $awaits, $resume)` before the render returns). Each
23
+ hoistable child was started as an isolated `$$flight` in the prefix and its output position RESERVED
24
+ (an empty `$out` slot); here we fill that slot per child:
25
+
26
+ - A flight that has already SETTLED (a synchronous / warm-cache child, or a fast read) inlines its
27
+ html into the reserved slot as `<!--[-->…<!--]-->` — BYTE-IDENTICAL to the pre-ADR-0039 inline
28
+ `await Child.render()` path, so an all-fast page's wire and hydration are unchanged. Its awaits /
29
+ resume merge exactly as the inline path did.
30
+ - A flight settled REJECTED rethrows here — same as a rejecting inline await (500 before flush).
31
+ - A still-PENDING flight (genuine I/O) STREAMS: the slot gets an empty `abide:await:CHILDPATH`
32
+ boundary, and an html-only SsrAwait is pushed so `renderToStream` flushes the shell now and streams
33
+ the child's fragment when it settles (its own nested awaits/resume compose through the same drain).
34
+
35
+ Timing: one microtask drain settles every synchronous child; a single shared macrotask is paid ONLY
36
+ if some child is still pending after that (a page whose children are all fast never reaches it), so
37
+ the inline fast path costs one microtask and a genuinely-slow child — which was going to block the
38
+ shell anyway — trades ~one tick for a progressively-flushed shell.
39
+ */
40
+ // @documentation plumbing
41
+ export async function finalizeStreamedChildren(
42
+ slots: StagedChild[],
43
+ awaits: SsrAwait[],
44
+ resume: Record<string, ResumeEntry>,
45
+ ): Promise<void> {
46
+ if (slots.length === 0) {
47
+ return
48
+ }
49
+ await Promise.resolve()
50
+ if (slots.some((staged) => !staged.flight.settled)) {
51
+ await new Promise<void>((resolve) => setTimeout(resolve, 0))
52
+ }
53
+ for (const staged of slots) {
54
+ if (staged.flight.settled && staged.flight.error === undefined) {
55
+ const rendered = staged.flight.value as SsrRender
56
+ staged.out[staged.slot] = OPEN + rendered.html + CLOSE
57
+ mergeChildRender(rendered, awaits, resume)
58
+ } else if (staged.flight.settled) {
59
+ throw staged.flight.error
60
+ } else {
61
+ staged.out[staged.slot] =
62
+ `<!--abide:await:${staged.id}--><!--/abide:await:${staged.id}-->`
63
+ awaits.push({
64
+ id: staged.id,
65
+ htmlOnly: true,
66
+ promise: () => staged.flight,
67
+ // biome-ignore lint/suspicious/noThenProperty: `then` is the SsrAwait renderer callback the stream drain invokes explicitly with the resolved value — this object is stored in `awaits[]`, never awaited as a thenable
68
+ then: async (rendered) => {
69
+ mergeChildRender(rendered as SsrRender, awaits, resume)
70
+ return (rendered as SsrRender).html
71
+ },
72
+ })
73
+ }
74
+ }
75
+ }
76
+
77
+ /* Fold a child render's contribution (its nested awaits, its resume map) up into the parent's — the
78
+ single definition both the inlined-settled arm and the still-pending stream arm merge through, so
79
+ the two can never drift on what a child contributes upward. */
80
+ function mergeChildRender(
81
+ rendered: SsrRender,
82
+ awaits: SsrAwait[],
83
+ resume: Record<string, ResumeEntry>,
84
+ ): void {
85
+ for (const nested of rendered.awaits) {
86
+ awaits.push(nested)
87
+ }
88
+ Object.assign(resume, rendered.resume)
89
+ }
@@ -0,0 +1,56 @@
1
+ /*
2
+ Server-only flight-starter behind the SSR codegen's `$$flight` alias (ADR-0034). The
3
+ SSR back-end hoists a *hoistable* await's promise into the synchronous render prefix as
4
+ `const $flightN = $$flight(() => (<promise expr>))`, so the flight is already in-flight
5
+ when the body walk reaches its blocking `await $flightN` / streaming `promise: () =>
6
+ $flightN` — independent flights overlap instead of serializing (prefix → barrier → body
7
+ → drain). Server-only: the client build never emits `$$flight`, so it tree-shakes out of
8
+ the browser bundle (like `$$settleAsyncCells`).
9
+
10
+ Two behaviours the raw promise expression can't provide on its own:
11
+
12
+ - A SYNCHRONOUS throw in the loader (before it returns a promise) is normalised to a
13
+ rejected promise, so a hoisted flight's throw still lands in the block's `{:catch}` /
14
+ 500 path exactly as an inline `await (expr)` would — the expression no longer evaluates
15
+ inside the block's try/catch, so the thunk wrapper restores that reach.
16
+ - The settledness `.then(onValue, onError)` below is itself a rejection handler attached
17
+ synchronously to `promise`, so a flight that rejects in the window before its real
18
+ consumer (the inline await / the `renderToStream` drain) attaches a handler is never a
19
+ Bun-fatal unhandled rejection — the `onError` branch never rethrows, so the block's
20
+ surface-rejection → 500 / catch-branch semantics are unchanged. Mirrors the inline
21
+ `.then(onValue, onError)` guard in `createAsyncCell`.
22
+ */
23
+ // @documentation plumbing
24
+ export function flight(thunk: () => unknown): FlightPromise {
25
+ let promise: Promise<unknown>
26
+ try {
27
+ promise = Promise.resolve(thunk())
28
+ } catch (error) {
29
+ promise = Promise.reject(error)
30
+ }
31
+ /* Synchronous settledness signal (ADR-0039): the post-walk `finalizeStreamedChildren` reads
32
+ `.settled` to decide inline-vs-stream — a child flight that already resolved by finalize time
33
+ inlines its html into the shell (byte-identical to today), a still-pending one streams. Its
34
+ `onError` branch doubles as the rejection keeper (see the header note): it marks `promise`
35
+ handled, so a pre-consumer rejection is never a Bun-fatal unhandled rejection. */
36
+ const flightPromise = promise as FlightPromise
37
+ flightPromise.settled = false
38
+ promise.then(
39
+ (value) => {
40
+ flightPromise.value = value
41
+ flightPromise.settled = true
42
+ },
43
+ (error) => {
44
+ flightPromise.error = error
45
+ flightPromise.settled = true
46
+ },
47
+ )
48
+ return flightPromise
49
+ }
50
+
51
+ /* A flight promise carrying a synchronous settledness snapshot for `finalizeStreamedChildren`. */
52
+ export type FlightPromise = Promise<unknown> & {
53
+ settled: boolean
54
+ value?: unknown
55
+ error?: unknown
56
+ }
@@ -16,7 +16,18 @@ written inside a template, so it lives in `ui/` (the reader, `rawHtmlString`, is
16
16
  isomorphic plumbing and stays in `shared/`).
17
17
  */
18
18
  // @documentation templating
19
- export function html(strings: TemplateStringsArray | string, ...values: unknown[]): RawHtml {
19
+ export function html(
20
+ strings: TemplateStringsArray | string | null | undefined,
21
+ ...values: unknown[]
22
+ ): RawHtml {
23
+ /* A plain call with a nullish argument renders nothing. A bare async read (ADR-0032)
24
+ hands `html()` `undefined` while pending — `{html(highlight(code)?.html)}` — so the
25
+ plain-call path degrades to empty raw instead of throwing on `strings[0]`, matching
26
+ how a bare `{value}` stringifies `undefined` to `""`. Mirrors the null-tolerance
27
+ ADR-0032 added to `done()`/`peek()` for the same inline-async ergonomics. */
28
+ if (strings === null || strings === undefined) {
29
+ return { [RAW_HTML]: '' }
30
+ }
20
31
  if (typeof strings === 'string') {
21
32
  return { [RAW_HTML]: strings }
22
33
  }
@@ -0,0 +1,17 @@
1
+ import { cellBarrierBacking } from './runtime/cellBarrierBacking.ts'
2
+
3
+ /*
4
+ Runs a hoisted child render (`$$isolateCellBarrier`) under its OWN async-cell barrier list, so its
5
+ cells register and its `$$settleAsyncCells` drain are isolated from every concurrent sibling and from
6
+ the page (ADR-0037 Phase 2). Wraps the render start emitted by the SSR flight hoist:
7
+ `$$flight(() => $$isolateCellBarrier(() => $$withPath(ordinal, () => Child.render(props, $ctx))))`.
8
+ The server backing runs it inside an AsyncLocalStorage so the fresh list is inherited across the
9
+ child render's own awaits (its top-level script await, its barrier); the default (client / no server
10
+ install) backing is an inert passthrough — the client mounts synchronously and has no SSR barrier, so
11
+ there is nothing to isolate. Only the pending (barrier) list is isolated; resolved/streamed cell
12
+ values still aggregate on the per-request store for the warm-seed snapshot.
13
+ */
14
+ // @documentation plumbing
15
+ export function isolateCellBarrier<T>(render: () => T): T {
16
+ return cellBarrierBacking.active.run({ promises: [] }, render)
17
+ }