@abide/abide 0.49.0 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (325) hide show
  1. package/AGENTS.md +70 -52
  2. package/CHANGELOG.md +177 -0
  3. package/README.md +12 -7
  4. package/package.json +14 -5
  5. package/src/abideLsp.ts +5 -6
  6. package/src/abideResolverPlugin.ts +197 -169
  7. package/src/build.ts +79 -56
  8. package/src/buildDisconnected.ts +0 -2
  9. package/src/checkAbide.ts +12 -2
  10. package/src/devEntry.ts +113 -28
  11. package/src/discoveryEntry.ts +3 -2
  12. package/src/lib/bundle/disconnected.abide +69 -73
  13. package/src/lib/bundle/infoPlist.ts +13 -7
  14. package/src/lib/bundle/spawnEmbeddedServer.ts +13 -4
  15. package/src/lib/bundle/waitForServer.ts +6 -1
  16. package/src/lib/mcp/mcpTools.ts +15 -6
  17. package/src/lib/server/error.ts +6 -6
  18. package/src/lib/server/json.ts +17 -3
  19. package/src/lib/server/rpc/defineRpc.ts +58 -24
  20. package/src/lib/server/rpc/parseArgs.ts +98 -13
  21. package/src/lib/server/rpc/resolveRpcJsonSchema.ts +28 -0
  22. package/src/lib/server/rpc/types/RpcHelper.ts +75 -105
  23. package/src/lib/server/rpc/types/RpcRegistryEntry.ts +24 -9
  24. package/src/lib/server/rpc/validationError.ts +1 -1
  25. package/src/lib/server/runtime/DEV_RELOAD_CLIENT_SCRIPT.ts +0 -15
  26. package/src/lib/server/runtime/buildCacheSnapshot.ts +10 -9
  27. package/src/lib/server/runtime/buildInspectorSurface.ts +6 -4
  28. package/src/lib/server/runtime/buildOpenApiSpec.ts +55 -11
  29. package/src/lib/server/runtime/buildPreloadManifest.ts +12 -10
  30. package/src/lib/server/runtime/createAppAssetServer.ts +18 -15
  31. package/src/lib/server/runtime/createAppRouteResolver.ts +145 -0
  32. package/src/lib/server/runtime/createPlumbingRouter.ts +212 -0
  33. package/src/lib/server/runtime/createRouteDispatcher.ts +4 -10
  34. package/src/lib/server/runtime/createServer.ts +120 -324
  35. package/src/lib/server/runtime/createUiPageRenderer.ts +137 -26
  36. package/src/lib/server/runtime/devClientFingerprint.ts +19 -34
  37. package/src/lib/server/runtime/installAmbientScopeStore.ts +30 -9
  38. package/src/lib/server/runtime/logExposedSurfaces.ts +8 -7
  39. package/src/lib/server/runtime/pathStore.ts +15 -0
  40. package/src/lib/server/runtime/registryManifests.ts +1 -1
  41. package/src/lib/server/runtime/renderCellBarrierStore.ts +15 -0
  42. package/src/lib/server/runtime/runWithRequestScope.ts +3 -0
  43. package/src/lib/server/runtime/serializeCacheSnapshot.ts +6 -4
  44. package/src/lib/server/runtime/snapshotEntryFromCache.ts +14 -14
  45. package/src/lib/server/runtime/streamCacheResolutions.ts +14 -7
  46. package/src/lib/server/runtime/streamFromIterator.ts +30 -2
  47. package/src/lib/server/runtime/textResponse.ts +23 -0
  48. package/src/lib/server/runtime/types/DevReloadStamp.ts +6 -10
  49. package/src/lib/server/runtime/types/InspectorCacheEntry.ts +1 -1
  50. package/src/lib/server/runtime/types/RequestStore.ts +27 -0
  51. package/src/lib/server/runtime/warnUnguardedMcp.ts +15 -5
  52. package/src/lib/server/sockets/createSocketDispatcher.ts +9 -11
  53. package/src/lib/server/sockets/defineSocket.ts +5 -4
  54. package/src/lib/server/sse.ts +5 -1
  55. package/src/lib/shared/ASYNC_CELL.ts +5 -0
  56. package/src/lib/shared/DEV_REBUILD_PATH.ts +6 -0
  57. package/src/lib/shared/HTTP_METHODS.ts +6 -0
  58. package/src/lib/shared/HttpError.ts +1 -5
  59. package/src/lib/shared/MCP_PATH.ts +6 -0
  60. package/src/lib/shared/PROXIED_SERVER_SUBDIRS.ts +15 -0
  61. package/src/lib/shared/REF_JSON_TAGS.ts +13 -1
  62. package/src/lib/shared/RPC_SHIM_GLOBALS.ts +9 -0
  63. package/src/lib/shared/activeCacheStore.ts +1 -0
  64. package/src/lib/shared/activePage.ts +1 -0
  65. package/src/lib/shared/buildArtifact.ts +4 -1
  66. package/src/lib/shared/buildSocketOverChannel.ts +4 -3
  67. package/src/lib/shared/bundleGraphFromMetafile.ts +68 -0
  68. package/src/lib/shared/cache.ts +156 -131
  69. package/src/lib/shared/changeAffectsClient.ts +12 -7
  70. package/src/lib/shared/createCacheStore.ts +31 -8
  71. package/src/lib/shared/createLifecycleChannel.ts +7 -1
  72. package/src/lib/shared/createReachable.ts +47 -78
  73. package/src/lib/shared/createRemoteFunction.ts +52 -30
  74. package/src/lib/shared/createRpcServerProgram.ts +820 -0
  75. package/src/lib/shared/decodeRefJson.ts +4 -4
  76. package/src/lib/shared/decodeResponse.ts +2 -2
  77. package/src/lib/shared/decodeWireBody.ts +35 -0
  78. package/src/lib/shared/detectRpcMethod.ts +8 -1
  79. package/src/lib/shared/done.ts +8 -2
  80. package/src/lib/shared/encodeRefJson.ts +4 -4
  81. package/src/lib/shared/encodeWireBody.ts +18 -0
  82. package/src/lib/{server/rpc → shared}/fieldErrorsFromIssues.ts +5 -1
  83. package/src/lib/shared/generateDeclarations.ts +72 -0
  84. package/src/lib/shared/hasSeedableRequest.ts +25 -0
  85. package/src/lib/shared/hydrationWindow.ts +53 -0
  86. package/src/lib/shared/isAsyncCell.ts +11 -0
  87. package/src/lib/shared/isAsyncIterable.ts +8 -0
  88. package/src/lib/shared/isSubscribable.ts +3 -3
  89. package/src/lib/shared/isThenable.ts +9 -0
  90. package/src/lib/shared/jsonSchemaForType.ts +258 -0
  91. package/src/lib/shared/lenientDecode.ts +15 -0
  92. package/src/lib/shared/loadProjectTsConfig.ts +28 -0
  93. package/src/lib/shared/markFrameworkSourcesIgnored.ts +1 -1
  94. package/src/lib/shared/matchRoute.ts +4 -14
  95. package/src/lib/shared/peek.ts +14 -0
  96. package/src/lib/shared/pending.ts +9 -6
  97. package/src/lib/shared/pendingAsyncCellsSlot.ts +13 -0
  98. package/src/lib/shared/prepareRpcModule.ts +91 -93
  99. package/src/lib/shared/prepareSocketModule.ts +3 -2
  100. package/src/lib/shared/probeRegistries.ts +5 -18
  101. package/src/lib/shared/reachable.ts +7 -10
  102. package/src/lib/shared/refresh.ts +12 -2
  103. package/src/lib/shared/refreshing.ts +8 -2
  104. package/src/lib/shared/resolvedCellsSlot.ts +13 -0
  105. package/src/lib/shared/reviveWireField.ts +49 -0
  106. package/src/lib/shared/reviveWireOutput.ts +36 -0
  107. package/src/lib/shared/rpcServerForRoot.ts +25 -0
  108. package/src/lib/shared/scanPages.ts +25 -0
  109. package/src/lib/shared/snapshotShippable.ts +8 -7
  110. package/src/lib/shared/streamedCellsSlot.ts +14 -0
  111. package/src/lib/shared/subscribableFromResponse.ts +4 -4
  112. package/src/lib/shared/subscribableProbes.ts +1 -1
  113. package/src/lib/shared/tailProbeSlot.ts +1 -1
  114. package/src/lib/shared/types/AsyncComputed.ts +20 -0
  115. package/src/lib/shared/types/AsyncState.ts +13 -0
  116. package/src/lib/shared/types/CacheEntry.ts +7 -7
  117. package/src/lib/shared/types/CacheOptions.ts +23 -37
  118. package/src/lib/shared/types/CachePolicy.ts +25 -0
  119. package/src/lib/shared/types/CacheSnapshotEntry.ts +6 -5
  120. package/src/lib/shared/types/ErrorJsonSchemas.ts +8 -0
  121. package/src/lib/shared/types/HttpMethod.ts +3 -1
  122. package/src/lib/shared/types/InputCoercion.ts +17 -0
  123. package/src/lib/shared/types/{Subscribable.ts → NamedAsyncIterable.ts} +1 -1
  124. package/src/lib/shared/types/OutputWirePlan.ts +17 -0
  125. package/src/lib/shared/types/PagesScan.ts +7 -0
  126. package/src/lib/shared/types/PendingAsyncCells.ts +10 -0
  127. package/src/lib/shared/types/RawRemoteFunction.ts +6 -0
  128. package/src/lib/shared/types/RemoteCallable.ts +8 -7
  129. package/src/lib/shared/types/RemoteFunction.ts +16 -15
  130. package/src/lib/shared/types/ResolvedCells.ts +11 -0
  131. package/src/lib/shared/types/ReturnBody.ts +15 -0
  132. package/src/lib/shared/types/RpcBuildStamps.ts +23 -0
  133. package/src/lib/shared/types/RpcErrorGuard.ts +3 -8
  134. package/src/lib/shared/types/Socket.ts +4 -4
  135. package/src/lib/shared/types/SsrPayload.ts +5 -1
  136. package/src/lib/shared/types/StreamPolicy.ts +11 -0
  137. package/src/lib/shared/types/StreamedCells.ts +19 -0
  138. package/src/lib/shared/types/StreamedResolution.ts +24 -6
  139. package/src/lib/shared/types/TailHooks.ts +1 -1
  140. package/src/lib/shared/types/WireKind.ts +11 -0
  141. package/src/lib/shared/validationHttpError.ts +33 -0
  142. package/src/lib/shared/warmSeedKey.ts +16 -0
  143. package/src/lib/shared/wireJsonReplacer.ts +30 -0
  144. package/src/lib/shared/writeRpcDts.ts +11 -1
  145. package/src/lib/shared/writeTestSocketsDts.ts +1 -1
  146. package/src/lib/test/createTestApp.ts +19 -1
  147. package/src/lib/ui/README.md +1 -1
  148. package/src/lib/ui/activePendingCells.ts +14 -0
  149. package/src/lib/ui/compile/BLOCK_KEYWORDS.ts +21 -0
  150. package/src/lib/ui/compile/SSR_ESCAPE.ts +4 -1
  151. package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +35 -8
  152. package/src/lib/ui/compile/abideUiPlugin.ts +19 -2
  153. package/src/lib/ui/compile/analyzeComponent.ts +89 -6
  154. package/src/lib/ui/compile/assignmentTargetNames.ts +54 -0
  155. package/src/lib/ui/compile/asyncInterpolationFields.ts +152 -0
  156. package/src/lib/ui/compile/asyncValuePositionError.ts +21 -0
  157. package/src/lib/ui/compile/asyncValuePositionInterpolations.ts +64 -0
  158. package/src/lib/ui/compile/attrLiftPosition.ts +18 -0
  159. package/src/lib/ui/compile/cachedSourceFile.ts +40 -0
  160. package/src/lib/ui/compile/catchBinding.ts +7 -5
  161. package/src/lib/ui/compile/classifyInterpolationType.ts +46 -0
  162. package/src/lib/ui/compile/collectAbideDiagnostics.ts +96 -0
  163. package/src/lib/ui/compile/compileComponent.ts +19 -3
  164. package/src/lib/ui/compile/compileModule.ts +27 -58
  165. package/src/lib/ui/compile/compileSSR.ts +51 -12
  166. package/src/lib/ui/compile/compileShadow.ts +145 -18
  167. package/src/lib/ui/compile/composeProps.ts +12 -2
  168. package/src/lib/ui/compile/createShadowLanguageService.ts +116 -68
  169. package/src/lib/ui/compile/createShadowProgram.ts +35 -6
  170. package/src/lib/ui/compile/declaredNames.ts +36 -0
  171. package/src/lib/ui/compile/desugarSignals.ts +418 -36
  172. package/src/lib/ui/compile/expressionIsPrefixEvaluable.ts +19 -0
  173. package/src/lib/ui/compile/generateBuild.ts +52 -9
  174. package/src/lib/ui/compile/generateSSR.ts +242 -43
  175. package/src/lib/ui/compile/hoistableAwaits.ts +193 -0
  176. package/src/lib/ui/compile/hoistableChildRenders.ts +112 -0
  177. package/src/lib/ui/compile/interpolatedTemplateLiteral.ts +3 -1
  178. package/src/lib/ui/compile/interpolationClassifierForRoot.ts +37 -0
  179. package/src/lib/ui/compile/isSpuriousAsyncReadDiagnostic.ts +174 -0
  180. package/src/lib/ui/compile/liftAsyncSubExpressions.ts +166 -0
  181. package/src/lib/ui/compile/lowerAsyncInterpolations.ts +92 -0
  182. package/src/lib/ui/compile/lowerContext.ts +10 -0
  183. package/src/lib/ui/compile/lowerDocAccess.ts +26 -23
  184. package/src/lib/ui/compile/lowerScript.ts +40 -4
  185. package/src/lib/ui/compile/nodeAtShadowOffset.ts +28 -0
  186. package/src/lib/ui/compile/parseTemplate.ts +18 -1092
  187. package/src/lib/ui/compile/parseTemplateRecovering.ts +1385 -0
  188. package/src/lib/ui/compile/referencedIdentifiers.ts +35 -0
  189. package/src/lib/ui/compile/renameSignalRefs.ts +21 -22
  190. package/src/lib/ui/compile/seedTypeClassifierForRoot.ts +65 -0
  191. package/src/lib/ui/compile/shadowInterpolationClassifier.ts +47 -0
  192. package/src/lib/ui/compile/sourceFileOptionsSignature.ts +20 -0
  193. package/src/lib/ui/compile/structuralHeadTokens.ts +107 -0
  194. package/src/lib/ui/compile/templateSemanticTokens.ts +21 -0
  195. package/src/lib/ui/compile/templateStartOffset.ts +15 -0
  196. package/src/lib/ui/compile/tryPlan.ts +4 -3
  197. package/src/lib/ui/compile/types/AnalyzedComponent.ts +4 -0
  198. package/src/lib/ui/compile/types/AsyncInterpolationField.ts +19 -0
  199. package/src/lib/ui/compile/types/InterpolationClassifier.ts +12 -0
  200. package/src/lib/ui/compile/types/InterpolationKind.ts +7 -0
  201. package/src/lib/ui/compile/types/ParseDiagnostic.ts +8 -0
  202. package/src/lib/ui/compile/types/SeedTypeClassifier.ts +15 -0
  203. package/src/lib/ui/compile/types/TemplateNode.ts +36 -4
  204. package/src/lib/ui/compile/types/ValuePositionInterpolation.ts +13 -0
  205. package/src/lib/ui/compile/writtenTemplateNames.ts +84 -0
  206. package/src/lib/ui/computed.ts +28 -1
  207. package/src/lib/ui/createScope.ts +35 -68
  208. package/src/lib/ui/dom/anchoredBranch.ts +142 -0
  209. package/src/lib/ui/dom/appendText.ts +8 -3
  210. package/src/lib/ui/dom/awaitBlock.ts +43 -94
  211. package/src/lib/ui/dom/bindProp.ts +22 -0
  212. package/src/lib/ui/dom/bindableProp.ts +47 -0
  213. package/src/lib/ui/dom/cellPending.ts +24 -0
  214. package/src/lib/ui/dom/disposeRange.ts +2 -1
  215. package/src/lib/ui/dom/each.ts +26 -4
  216. package/src/lib/ui/dom/eachAsync.ts +39 -3
  217. package/src/lib/ui/dom/fillBoundary.ts +2 -3
  218. package/src/lib/ui/dom/fillRange.ts +4 -4
  219. package/src/lib/ui/dom/hydrate.ts +6 -12
  220. package/src/lib/ui/dom/isComment.ts +1 -1
  221. package/src/lib/ui/dom/matchingRangeClose.ts +29 -0
  222. package/src/lib/ui/dom/mount.ts +1 -2
  223. package/src/lib/ui/dom/mountChild.ts +9 -40
  224. package/src/lib/ui/dom/mountRange.ts +2 -3
  225. package/src/lib/ui/dom/mountStreamedChild.ts +84 -0
  226. package/src/lib/ui/dom/mountSwappableRange.ts +46 -4
  227. package/src/lib/ui/dom/mutateDocContainer.ts +65 -0
  228. package/src/lib/ui/dom/on.ts +2 -2
  229. package/src/lib/ui/dom/readCell.ts +40 -0
  230. package/src/lib/ui/dom/switchBlock.ts +30 -7
  231. package/src/lib/ui/dom/tryBlock.ts +230 -66
  232. package/src/lib/ui/dom/types/SwitchCase.ts +5 -1
  233. package/src/lib/ui/dom/when.ts +12 -2
  234. package/src/lib/ui/dom/withScope.ts +2 -2
  235. package/src/lib/ui/finalizeStreamedChildren.ts +89 -0
  236. package/src/lib/ui/flight.ts +56 -0
  237. package/src/lib/ui/html.ts +12 -1
  238. package/src/lib/ui/isolateCellBarrier.ts +17 -0
  239. package/src/lib/ui/linked.ts +48 -2
  240. package/src/lib/ui/remoteProxy.ts +84 -159
  241. package/src/lib/ui/renderChain.ts +49 -13
  242. package/src/lib/ui/renderToStream.ts +22 -18
  243. package/src/lib/ui/resumeSeedScript.ts +1 -1
  244. package/src/lib/ui/router.ts +63 -36
  245. package/src/lib/ui/runtime/AsyncCellError.ts +20 -0
  246. package/src/lib/ui/runtime/CELL_SEED.ts +14 -0
  247. package/src/lib/ui/runtime/CURRENT_BOUNDARY.ts +11 -0
  248. package/src/lib/ui/runtime/CURRENT_PATH.ts +29 -0
  249. package/src/lib/ui/runtime/RENDER.ts +10 -7
  250. package/src/lib/ui/runtime/RESUME.ts +4 -4
  251. package/src/lib/ui/runtime/STREAMED_CELLS.ts +57 -0
  252. package/src/lib/ui/runtime/ambientPathBacking.ts +32 -0
  253. package/src/lib/ui/runtime/applyPatchToTree.ts +1 -1
  254. package/src/lib/ui/runtime/blockId.ts +31 -0
  255. package/src/lib/ui/runtime/boundaryFor.ts +11 -0
  256. package/src/lib/ui/runtime/cellBarrierBacking.ts +30 -0
  257. package/src/lib/ui/runtime/createAsyncCell.ts +300 -0
  258. package/src/lib/ui/runtime/createDoc.ts +3 -42
  259. package/src/lib/ui/runtime/createEffectNode.ts +9 -0
  260. package/src/lib/ui/runtime/enterRenderPass.ts +5 -4
  261. package/src/lib/ui/runtime/flushEffects.ts +16 -6
  262. package/src/lib/ui/runtime/isAsyncFunction.ts +14 -0
  263. package/src/lib/ui/runtime/nextBlockId.ts +8 -7
  264. package/src/lib/ui/runtime/renderPath.ts +16 -0
  265. package/src/lib/ui/runtime/types/Boundary.ts +9 -0
  266. package/src/lib/ui/runtime/types/RenderContext.ts +9 -7
  267. package/src/lib/ui/runtime/types/SsrRender.ts +9 -2
  268. package/src/lib/ui/runtime/types/UiComponent.ts +1 -5
  269. package/src/lib/ui/runtime/withOptionalPath.ts +8 -0
  270. package/src/lib/ui/runtime/withPath.ts +16 -0
  271. package/src/lib/ui/runtime/withPathFrom.ts +20 -0
  272. package/src/lib/ui/seedStreamedResolution.ts +31 -6
  273. package/src/lib/ui/settleAsyncCells.ts +24 -0
  274. package/src/lib/ui/socketProxy.ts +1 -1
  275. package/src/lib/ui/startClient.ts +16 -31
  276. package/src/lib/ui/trackedComputed.ts +68 -0
  277. package/src/lib/ui/types/Scope.ts +8 -24
  278. package/src/lib/ui/watch.ts +3 -3
  279. package/src/serverEntry.ts +14 -0
  280. package/template/src/server/rpc/getHello.ts +15 -13
  281. package/template/test/app.test.ts +1 -1
  282. package/src/lib/server/runtime/devHotModuleResponse.ts +0 -41
  283. package/src/lib/shared/DEV_HOT_PREFIX.ts +0 -7
  284. package/src/lib/shared/UNREACHABLE_STATUSES.ts +0 -13
  285. package/src/lib/shared/hasReplayableRequest.ts +0 -17
  286. package/src/lib/shared/hydratingSlot.ts +0 -12
  287. package/src/lib/shared/outboxProbeSlot.ts +0 -20
  288. package/src/lib/shared/types/Outbox.ts +0 -9
  289. package/src/lib/shared/types/OutboxEntry.ts +0 -27
  290. package/src/lib/shared/types/SmartReadOptions.ts +0 -36
  291. package/src/lib/shared/wakeHydrationPeeks.ts +0 -20
  292. package/src/lib/ui/COMPONENT_WRAPPER_PREFIX.ts +0 -5
  293. package/src/lib/ui/compile/REACTIVE_CALLEES.ts +0 -11
  294. package/src/lib/ui/compile/assertRuntimeHelpersBound.ts +0 -80
  295. package/src/lib/ui/compile/markupTokens.ts +0 -313
  296. package/src/lib/ui/compile/structuralBlockTokens.ts +0 -100
  297. package/src/lib/ui/dom/applyResolved.ts +0 -87
  298. package/src/lib/ui/dom/mutateDocArray.ts +0 -38
  299. package/src/lib/ui/dom/scopeLabel.ts +0 -21
  300. package/src/lib/ui/dom/text.ts +0 -20
  301. package/src/lib/ui/history.ts +0 -108
  302. package/src/lib/ui/installHotBridge.ts +0 -95
  303. package/src/lib/ui/installInspectorBridge.ts +0 -140
  304. package/src/lib/ui/outbox.ts +0 -35
  305. package/src/lib/ui/persist.ts +0 -115
  306. package/src/lib/ui/rpcOutbox/createOutboxQueue.ts +0 -281
  307. package/src/lib/ui/rpcOutbox/outboxRegistry.ts +0 -69
  308. package/src/lib/ui/runtime/PATCH_BUS.ts +0 -28
  309. package/src/lib/ui/runtime/captureModelDoc.ts +0 -43
  310. package/src/lib/ui/runtime/hotInstances.ts +0 -10
  311. package/src/lib/ui/runtime/hotReloadEnabled.ts +0 -8
  312. package/src/lib/ui/runtime/hotReplace.ts +0 -38
  313. package/src/lib/ui/runtime/liveScopes.ts +0 -15
  314. package/src/lib/ui/runtime/localStoragePersistence.ts +0 -47
  315. package/src/lib/ui/runtime/registerHotInstance.ts +0 -23
  316. package/src/lib/ui/runtime/seedModelDoc.ts +0 -26
  317. package/src/lib/ui/runtime/types/HotInstance.ts +0 -22
  318. package/src/lib/ui/runtime/types/PatchEvent.ts +0 -16
  319. package/src/lib/ui/seedResolved.ts +0 -28
  320. package/src/lib/ui/sync.ts +0 -48
  321. package/src/lib/ui/types/History.ts +0 -14
  322. package/src/lib/ui/types/PersistHandle.ts +0 -11
  323. package/src/lib/ui/types/PersistenceStore.ts +0 -12
  324. package/src/lib/ui/types/ResolvedFrame.ts +0 -15
  325. package/src/lib/ui/types/SyncTransport.ts +0 -13
@@ -1,101 +1,265 @@
1
+ import { effect } from '../effect.ts'
2
+ import { AsyncCellError } from '../runtime/AsyncCellError.ts'
3
+ import { CURRENT_BOUNDARY } from '../runtime/CURRENT_BOUNDARY.ts'
1
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'
5
10
  import { withoutHydration } from '../runtime/withoutHydration.ts'
11
+ import { state } from '../state.ts'
12
+ import { anchoredBranch } from './anchoredBranch.ts'
6
13
  import { discardBoundary } from './discardBoundary.ts'
7
- import { enterNamespace } from './enterNamespace.ts'
8
14
 
9
15
  /*
10
- Synchronous error boundary — the runtime for `<template try>`. Builds the guarded
11
- subtree (`renderTry`); if building it throws including a throw from an initial
12
- reactive read, since effects run during build it tears down the partial scope and
13
- builds `renderCatch(error)` instead. Each branch builds its content (any content)
14
- into the parent. No `renderCatch` (no `<template catch>`) re-throws, so the error
15
- propagates to the nearest enclosing boundary. The block renders once and never
16
- re-renders, so its content needs no range markers — an enclosing block's range
17
- 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`).
18
21
 
19
- On create the guarded content is built into a fragment first, so a throw mid-build
20
- discards the partial nodes (they never entered the document) before the catch
21
- builds. On hydrate it claims the SSR boundary
22
- (`<!--abide:try:N-->…<!--/abide:try:N-->`): the happy path adopts the guarded nodes
23
- 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.
24
43
  */
25
44
  // @documentation plumbing
26
45
  export function tryBlock(
27
46
  parent: Node,
28
- id: number,
47
+ id: string,
29
48
  renderTry: (parent: Node) => void,
30
- renderCatch?: (parent: Node, error: unknown) => void,
49
+ renderCatch?: (parent: Node, error: State<unknown>) => void,
31
50
  before: Node | null = null,
32
51
  ): void {
33
- /* The guarded subtree's scope, registered with the owner so it disposes on owner
34
- teardown. The block renders once, so there is at most one tracked subtree (the
35
- 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. */
36
55
  const group = scopeGroup()
37
- /* Run a void build under a fresh ownership scope. On success, hand its disposers to
38
- the group so the subtree tears down with the owner (they were previously dropped —
39
- the leak). On throw, tear down the partial build now and rethrow so the caller can
40
- fall back to the catch branch. */
41
- const guard = (build: () => void): void => {
42
- const previous = OWNER.current
43
- const disposers: Array<() => void> = []
44
- OWNER.current = disposers
45
- const disposeAll = (): void => {
46
- for (let index = disposers.length - 1; index >= 0; index -= 1) {
47
- disposers[index]?.()
48
- }
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
49
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
50
89
  try {
51
- build()
52
- OWNER.current = previous
53
- group.track(disposeAll)
54
- } catch (error) {
55
- OWNER.current = previous
56
- disposeAll()
57
- throw error
90
+ run()
91
+ } finally {
92
+ CURRENT_BOUNDARY.current = previousBoundary
58
93
  }
59
94
  }
60
95
 
61
- const hydration = RENDER.hydration
62
- if (hydration !== undefined) {
63
- // Guaranteed control-flow markers claimExpected throws a legible desync (the close
64
- // is caught below → rebuild the catch fresh) instead of claiming null and over-clearing.
65
- const open = claimExpected(hydration, parent, `abide:try:${id} open marker`)
66
- 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()
67
102
  try {
68
- guard(() => renderTry(parent)) // claims the guarded nodes in place
69
- const close = claimExpected(hydration, parent, `/abide:try:${id} close marker`)
70
- hydration.next.set(parent, close.nextSibling ?? null)
103
+ branch.place((host) => renderTry(host), hasCatch ? withBoundary : undefined)
104
+ activeKind = 'try'
105
+ errorCell = undefined
71
106
  } catch (error) {
72
- /* The server markup didn't adopt — drop the whole boundary and build the
73
- catch fresh in its place. */
74
- const after = discardBoundary(parent, open, `/abide:try:${id}`, hydration)
75
- if (renderCatch === undefined) {
107
+ if (!hasCatch) {
76
108
  throw error
77
109
  }
78
- withoutHydration(() => {
79
- const fragment = document.createDocumentFragment()
80
- enterNamespace(parent, () => guard(() => renderCatch(fragment, error)))
81
- parent.insertBefore(fragment, after)
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
+ }
82
172
  })
173
+ } finally {
174
+ CURRENT_BOUNDARY.current = previousBoundary
175
+ OWNER.current = previousOwner
83
176
  }
177
+ watchDispose = group.track(dispose)
178
+ }
179
+
180
+ if (hydration !== undefined) {
181
+ firstHydrate()
84
182
  return
85
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
+ }
86
246
 
87
- /* Create: build into a fragment so a throw mid-build discards the partial nodes
88
- (they never entered the document) before the catch builds. */
89
- try {
90
- const fragment = document.createDocumentFragment()
91
- enterNamespace(parent, () => guard(() => renderTry(fragment)))
92
- parent.insertBefore(fragment, before)
93
- } catch (error) {
94
- 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) {
95
258
  throw error
96
259
  }
97
- const fragment = document.createDocumentFragment()
98
- enterNamespace(parent, () => guard(() => renderCatch(fragment, error)))
99
- parent.insertBefore(fragment, before)
260
+ const anchorNode = document.createTextNode('')
261
+ branch.anchor = anchorNode
262
+ parent.insertBefore(anchorNode, after)
263
+ withoutHydration(() => showCatch(error))
100
264
  }
101
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,9 +15,9 @@ 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 {
@@ -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
+ }
@@ -1,4 +1,9 @@
1
+ import { isAsyncIterable } from '../shared/isAsyncIterable.ts'
2
+ import type { AsyncState } from '../shared/types/AsyncState.ts'
3
+ import type { NamedAsyncIterable } from '../shared/types/NamedAsyncIterable.ts'
4
+ import { createAsyncCell } from './runtime/createAsyncCell.ts'
1
5
  import { createEffectNode } from './runtime/createEffectNode.ts'
6
+ import { isAsyncFunction } from './runtime/isAsyncFunction.ts'
2
7
  import type { State } from './runtime/types/State.ts'
3
8
  import { state } from './state.ts'
4
9
 
@@ -16,8 +21,49 @@ entering the store — explicit `.value =` writes and reseeds alike — so the s
16
21
  never holds an un-coerced value (`return previous` rejects via the `Object.is`
17
22
  no-op). The seed is captured by reference: callers clone in the thunk
18
23
  (`linked(() => structuredClone(x))`) when they want isolation.
24
+
25
+ When the seed tracks an async source it becomes a writable async cell (`AsyncState<T>`,
26
+ ADR-0019 D1): an `async () => await …` thunk unwraps its promise, a `NamedAsyncIterable`
27
+ seed auto-tracks its frames. Same probe surface as `AsyncComputed` plus `set()`, which
28
+ latches until the next reseed — an arriving frame never clobbers an in-progress edit.
19
29
  */
20
- export function linked<T>(seed: () => T, transform?: (next: T, previous: T) => T): State<T> {
30
+ export function linked<T>(
31
+ seed: () => NamedAsyncIterable<T>,
32
+ transform?: (next: T, previous: T) => T,
33
+ ): AsyncState<T>
34
+ export function linked<T>(
35
+ seed: () => Promise<T>,
36
+ transform?: (next: T, previous: T) => T,
37
+ ): AsyncState<T>
38
+ export function linked<T>(seed: () => T, transform?: (next: T, previous: T) => T): State<T>
39
+ export function linked<T>(
40
+ seed: () => T | Promise<T> | NamedAsyncIterable<T>,
41
+ transform?: (next: T, previous: T) => T,
42
+ ): State<T> | AsyncState<T> {
43
+ const coerce = transform as ((next: unknown, previous: unknown) => unknown) | undefined
44
+ /* `await` marker: an async-function seed unwraps its promise into a writable async cell. */
45
+ if (isAsyncFunction(seed)) {
46
+ return createAsyncCell(seed as () => unknown, {
47
+ writable: true,
48
+ transform: coerce,
49
+ }) as AsyncState<T>
50
+ }
51
+ /* Peek the seed once to detect a self-identifying stream source (auto-track); `linked`
52
+ already runs its seed eagerly, so an early probe matches its semantics. A throw or a
53
+ non-stream value falls through to the plain synchronous cell. */
54
+ let probe: unknown
55
+ let threw = false
56
+ try {
57
+ probe = seed()
58
+ } catch {
59
+ threw = true
60
+ }
61
+ if (!threw && isAsyncIterable(probe)) {
62
+ return createAsyncCell(seed as () => unknown, {
63
+ writable: true,
64
+ transform: coerce,
65
+ }) as AsyncState<T>
66
+ }
21
67
  /* The cell is a plain `state` — same store, same write path, so `transform` gates
22
68
  reseeds and explicit writes identically. */
23
69
  const cell = state<T>(undefined as T, transform)
@@ -27,7 +73,7 @@ export function linked<T>(seed: () => T, transform?: (next: T, previous: T) => T
27
73
  what `seed` reads can retrigger it. `createEffectNode` registers the disposer
28
74
  with the enclosing scope. */
29
75
  createEffectNode(() => {
30
- cell.value = seed()
76
+ cell.value = seed() as T
31
77
  })
32
78
  return cell
33
79
  }