@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,19 +1,18 @@
1
1
  import { decodeRefJson } from '../../shared/decodeRefJson.ts'
2
2
  import { effect } from '../effect.ts'
3
+ import { CURRENT_BOUNDARY } from '../runtime/CURRENT_BOUNDARY.ts'
3
4
  import { claimExpected } from '../runtime/claimExpected.ts'
4
5
  import { generationGuard } from '../runtime/generationGuard.ts'
5
6
  import { RANGE_CLOSE, RANGE_OPEN } from '../runtime/RANGE_MARKER.ts'
6
7
  import { RENDER } from '../runtime/RENDER.ts'
7
8
  import type { ResumeEntry } from '../runtime/RESUME.ts'
8
9
  import { RESUME } from '../runtime/RESUME.ts'
9
- import { scope } from '../runtime/scope.ts'
10
10
  import { scopeGroup } from '../runtime/scopeGroup.ts'
11
11
  import type { State } from '../runtime/types/State.ts'
12
12
  import { withoutHydration } from '../runtime/withoutHydration.ts'
13
13
  import { state } from '../state.ts'
14
- import { buildDetachedRange } from './buildDetachedRange.ts'
14
+ import { anchoredBranch } from './anchoredBranch.ts'
15
15
  import { discardBoundary } from './discardBoundary.ts'
16
- import { removeRange } from './removeRange.ts'
17
16
 
18
17
  /*
19
18
  Async binding — the runtime for `<template await>`. Renders the pending branch,
@@ -39,12 +38,13 @@ parked just before the close marker.
39
38
  // @documentation plumbing
40
39
  export function awaitBlock(
41
40
  parent: Node,
42
- id: number,
41
+ id: string,
43
42
  promiseThunk: () => unknown,
44
43
  renderPending: ((parent: Node) => void) | undefined,
45
44
  renderThen: (parent: Node, value: unknown) => void,
46
- /* Absent when the block has no catch branch — a rejection then surfaces (re-throws
47
- to the unhandled-rejection path) instead of rendering an empty branch. */
45
+ /* Absent when the block has no catch branch — a rejection then routes to the enclosing
46
+ {#try} boundary captured at build (if any), else surfaces (re-throws to the
47
+ unhandled-rejection path) instead of rendering an empty branch. */
48
48
  renderCatch: ((parent: Node, error: unknown) => void) | undefined,
49
49
  /* A static node located by the skeleton: the block's anchor inserts before it on
50
50
  create (block before a static suffix). Null appends (tail). insertBefore(x, null)
@@ -52,11 +52,16 @@ export function awaitBlock(
52
52
  before: Node | null = null,
53
53
  ): void {
54
54
  const hydration = RENDER.hydration
55
+ /* The enclosing {#try} boundary ambient at BUILD (like createEffectNode captures it). A
56
+ catch-less rejection settles LATER, after CURRENT_BOUNDARY has been restored, so we must
57
+ capture it into this closure now rather than read it at settle time. */
58
+ const capturedBoundary = CURRENT_BOUNDARY.current
55
59
  /* The live branch's scope, registered with the owner so it disposes on owner
56
60
  teardown — not only when a settle/re-run swaps branches via detach. */
57
61
  const group = scopeGroup()
58
- let active: { start: Comment; end: Comment; dispose: () => void } | undefined
59
- let anchor: Node | undefined
62
+ /* The single active branch mounted before an anchor owns `active`/`detach`/`place`/
63
+ adopt-strand guard (shared with `tryBlock`); this block keeps its bespoke settlement. */
64
+ const branch = anchoredBranch(parent, group)
60
65
  let first = true
61
66
  /* Bumped each run so a prior run's in-flight promise can't clobber a newer one, AND on
62
67
  owner teardown so an in-flight promise that settles AFTER the enclosing `{#if}`/
@@ -73,43 +78,6 @@ export function awaitBlock(
73
78
  place (then→then) or must build a fresh branch. */
74
79
  let activeKind: 'pending' | 'then' | 'catch' | undefined
75
80
 
76
- const detach = (): void => {
77
- if (active !== undefined) {
78
- active.dispose()
79
- /* `removeRange` evicts the markers AND everything between them via the end
80
- marker's LIVE parent — not the captured `parent`, which (when this await is a
81
- bare child of a control-flow branch) is the branch's build fragment, emptied
82
- into the document once the enclosing block placed it. */
83
- removeRange(active.start, active.end)
84
- active = undefined
85
- }
86
- }
87
-
88
- /* Replace the current content with a freshly-built branch, before the anchor. The branch
89
- builds into a detached `[`…`]`-bracketed fragment (so any content — components, text,
90
- nested blocks — appends freely), the same create primitive the keyed-list runtimes use,
91
- which lands as a marker-bounded range the next swap detaches with `removeRange`. */
92
- const place = (build: (parent: Node) => void): void => {
93
- /* Backstop for a settle whose anchor has been detached from the tree. The
94
- generationGuard is the PRIMARY defense — it drops a late settle after the owner
95
- tears down — but it only covers teardowns that dispose THIS block's scope. A
96
- gap (e.g. a nested hydration `adopt` that aborts to `rebuildCold`, leaving the
97
- inner block's range removed while its guard stays live) can still route a late
98
- settle here with `anchor` already pulled out of the DOM. Inserting before a
99
- node that is no longer a child of any parent throws a `NotFoundError` from
100
- `insertBefore` — surfacing as a process-fatal unhandled rejection under Bun. A
101
- detached anchor unambiguously means the block is gone, so drop the settle. */
102
- if (anchor !== undefined && anchor.parentNode === null) {
103
- return
104
- }
105
- detach()
106
- const namespaceParent = anchor?.parentNode ?? parent
107
- const { start, end, fragment, dispose } = buildDetachedRange(namespaceParent, build)
108
- const tracked = group.track(dispose)
109
- namespaceParent.insertBefore(fragment, anchor ?? null)
110
- active = { start, end, dispose: tracked }
111
- }
112
-
113
81
  /* Settle to a resolved value. then→then updates the cell in place — the branch and its
114
82
  inner each survive (no flash); any other prior kind builds a fresh then-branch around
115
83
  a new cell. renderThen receives the CELL (not the raw value), so the branch reads it
@@ -121,17 +89,23 @@ export function awaitBlock(
121
89
  }
122
90
  const cell = state(value)
123
91
  valueCell = cell
124
- place((host) => renderThen(host, cell))
92
+ branch.place((host) => renderThen(host, cell))
125
93
  activeKind = 'then'
126
94
  }
127
95
 
128
- /* Settle to a rejection: surface it with no catch branch, else swap to the catch branch. */
96
+ /* Settle to a rejection: with a local catch branch, swap to it. With no local catch, route
97
+ to the enclosing {#try} boundary captured at build if one was ambient; otherwise surface
98
+ (unhandled rejection), preserving the prior behaviour when there's no boundary. */
129
99
  const settleError = (error: unknown): void => {
130
100
  if (renderCatch === undefined) {
101
+ if (capturedBoundary !== undefined) {
102
+ capturedBoundary.handle(error)
103
+ return
104
+ }
131
105
  throw error
132
106
  }
133
107
  valueCell = undefined
134
- place((host) => renderCatch(host, error))
108
+ branch.place((host) => renderCatch(host, error))
135
109
  activeKind = 'catch'
136
110
  }
137
111
 
@@ -148,10 +122,10 @@ export function awaitBlock(
148
122
  shows the pending branch (or detaches) while the promise is in flight. */
149
123
  if (activeKind !== 'then') {
150
124
  if (renderPending !== undefined) {
151
- place((host) => renderPending(host))
125
+ branch.place((host) => renderPending(host))
152
126
  activeKind = 'pending'
153
127
  } else {
154
- detach()
128
+ branch.detach()
155
129
  activeKind = undefined
156
130
  }
157
131
  }
@@ -174,37 +148,14 @@ export function awaitBlock(
174
148
  marker for later swaps. The adopted content is everything the build claimed between
175
149
  the open and close markers; bracketing it makes the adopted branch a marker-bounded
176
150
  range identical to a freshly-`place`d one, so the FIRST swap detaches it with
177
- `removeRange` like every later swap (no node-array special case). */
151
+ `removeRange` like every later swap (no node-array special case). `adoptStrand` carries
152
+ the shared strand-dispose guard; the `afterBuild` closure below is await's bespoke
153
+ bracketing — it runs only after a clean build. */
178
154
  const adopt = (open: Node | null, build: (parent: Node) => void): void => {
179
155
  const cursor = hydration as NonNullable<typeof hydration>
180
156
  const firstAdopted = open?.nextSibling ?? null
181
157
  cursor.next.set(parent, firstAdopted)
182
- /* Adoption is guarded (see firstHydrate): a build that can't claim the server
183
- markup — a resume value that didn't round-trip, a nested-adopt claim desync —
184
- throws, and the caller recovers via `rebuildCold`. But the partial build may have
185
- already created a live sub-scope (an inner `await`'s effect/guard, a subscription)
186
- before it threw; letting the throw escape `scope()` would strand that scope's
187
- disposer (unreachable → never disposed), leaking the effect AND leaving its guard
188
- un-bumped so a late settle stays "live". So capture the build's error, ALWAYS take
189
- the returned disposer, and dispose it on ANY failure before rethrowing — `rebuildCold`
190
- then starts from a clean slate. */
191
- let dispose: (() => void) | undefined
192
- try {
193
- let buildFailed = false
194
- let buildError: unknown
195
- dispose = group.track(
196
- scope(() => {
197
- try {
198
- build(parent)
199
- } catch (error) {
200
- buildFailed = true
201
- buildError = error
202
- }
203
- }),
204
- )
205
- if (buildFailed) {
206
- throw buildError
207
- }
158
+ branch.adoptStrand(build, () => {
208
159
  /* A guaranteed control-flow marker — claimExpected throws on a desync (caught by
209
160
  firstHydrate's adopt try/catch → rebuildCold) instead of silently claiming null
210
161
  and over-clearing the parent. */
@@ -216,22 +167,17 @@ export function awaitBlock(
216
167
  parent.insertBefore(start, firstAdopted ?? close)
217
168
  const end = document.createComment(RANGE_CLOSE)
218
169
  parent.insertBefore(end, close)
219
- anchor = document.createTextNode('')
220
- parent.insertBefore(anchor, close)
221
- active = { start, end, dispose }
222
- } catch (error) {
223
- /* `dispose` (the group-tracked wrapper) is idempotent; running it here tears the
224
- partial branch scope down and drops it from the group so `rebuildCold` doesn't
225
- inherit a stranded scope, then the caller's `catch` falls back to a cold build. */
226
- dispose?.()
227
- throw error
228
- }
170
+ const anchorNode = document.createTextNode('')
171
+ parent.insertBefore(anchorNode, close)
172
+ branch.anchor = anchorNode
173
+ return { start, end }
174
+ })
229
175
  }
230
176
 
231
177
  /* Discard the SSR boundary and (re)build the block from the live promise, fresh
232
178
  (hydration off) — the recovery path when adoption can't use the server markup. */
233
179
  const rebuildCold = (open: Node | null): void => {
234
- detach()
180
+ branch.detach()
235
181
  /* Insert at the node AFTER the discarded boundary (its return) — NOT the captured
236
182
  `before`, which for a skeleton-anchored block is the open boundary itself and is
237
183
  removed here, so reusing it throws `NotFoundError` in a strict DOM. */
@@ -241,8 +187,9 @@ export function awaitBlock(
241
187
  `/abide:await:${id}`,
242
188
  hydration as NonNullable<typeof hydration>,
243
189
  )
244
- anchor = document.createTextNode('')
245
- parent.insertBefore(anchor, after)
190
+ const anchorNode = document.createTextNode('')
191
+ branch.anchor = anchorNode
192
+ parent.insertBefore(anchorNode, after)
246
193
  withoutHydration(() => render(promiseThunk()))
247
194
  }
248
195
 
@@ -324,8 +271,9 @@ export function awaitBlock(
324
271
  }
325
272
  /* Insert at the node after the discarded boundary (see `rebuildCold`). */
326
273
  const after = discardBoundary(parent, open, `/abide:await:${id}`, cursor)
327
- anchor = document.createTextNode('')
328
- parent.insertBefore(anchor, after)
274
+ const anchorNode = document.createTextNode('')
275
+ branch.anchor = anchorNode
276
+ parent.insertBefore(anchorNode, after)
329
277
  /* The boundary's server nodes are gone, so the pending branch builds FRESH — clear
330
278
  the claim cursor (see withoutHydration) so its `cloneStatic`/text don't try to
331
279
  claim discarded nodes and silently render nothing. */
@@ -342,8 +290,9 @@ export function awaitBlock(
342
290
  firstHydrate()
343
291
  return
344
292
  }
345
- anchor = document.createTextNode('')
346
- parent.insertBefore(anchor, before)
293
+ const anchorNode = document.createTextNode('')
294
+ branch.anchor = anchorNode
295
+ parent.insertBefore(anchorNode, before)
347
296
  }
348
297
  /* Read the promise every subsequent run so an invalidate re-runs the block. ONLY this
349
298
  read is tracked (the branch builds untracked via `scope`), so the block re-runs only
@@ -0,0 +1,22 @@
1
+ /*
2
+ The parent half of a component `bind:prop={target}`: annotates the prop's value
3
+ thunk with a `set` write-back channel, so the same `() => value` entry every prop
4
+ compiles to ALSO carries a setter. A read-only consumer calls the thunk (`$props[key]?.()`)
5
+ and gets the value exactly as for a plain prop — the `set` simply rides along, invisible
6
+ to code that doesn't look for it (`restProps`/`mergeProps`/`spreadProps` pass the thunk
7
+ through untouched). The child upgrades the prop to a writable cell (`bindableProp`) only
8
+ when it writes or forwards it, and reaches this setter through `.set`.
9
+
10
+ `read`/`write` are the caller's lowered bind accessors — an lvalue target reads as
11
+ itself and writes by assignment, a `{ get, set }` accessor reads via `get()` and writes
12
+ via `set(next)` — so a component bind accepts the same targets an element bind does.
13
+ */
14
+ // @documentation plumbing
15
+ export function bindProp<T>(
16
+ read: () => T,
17
+ write: (next: T) => void,
18
+ ): (() => T) & { set: (next: T) => void } {
19
+ const thunk = read as (() => T) & { set: (next: T) => void }
20
+ thunk.set = write
21
+ return thunk
22
+ }
@@ -0,0 +1,47 @@
1
+ import { linked } from '../linked.ts'
2
+ import type { State } from '../runtime/types/State.ts'
3
+ import type { UiProps } from '../runtime/types/UiProps.ts'
4
+
5
+ /*
6
+ The child half of a two-way prop: the writable cell a component gets for a prop it
7
+ WRITES or forwards to another `bind:` target. A prop the child only reads stays a
8
+ read-only derive (unchanged, cheap); this upgrade is emitted only for the props the
9
+ compiler sees written, so the common case pays nothing.
10
+
11
+ Whether the prop is actually two-way is decided at construction from what the parent
12
+ passed:
13
+ • Bound (`bind:prop` on the parent) — the value thunk carries a `set` (see `bindProp`),
14
+ so the cell is a pass-through accessor: reads pull the parent's value (tracking its
15
+ reactive source), writes go straight upstream. The parent's target is the single
16
+ source of truth, so no local copy is kept.
17
+ • Unbound (plain `prop={value}`, or the prop was never passed) — no setter exists, so
18
+ the cell degrades to a local `linked` cell seeded from the parent value: it reseeds
19
+ when the parent value changes and holds local writes in between. The component still
20
+ works standalone; its writes just don't flow anywhere.
21
+
22
+ `fallback` supplies the destructure's `= default` when the prop is absent/undefined,
23
+ matching the read-only derive's `?? default`.
24
+ */
25
+ // @documentation plumbing
26
+ export function bindableProp<T>(props: UiProps, key: string, fallback?: () => T): State<T> {
27
+ const entry = (props as Record<string, unknown>)[key]
28
+ const read = (): T => {
29
+ const value = typeof entry === 'function' ? (entry as () => T)() : (entry as T)
30
+ return (value === undefined ? fallback?.() : value) as T
31
+ }
32
+ const setter =
33
+ typeof entry === 'function' ? (entry as { set?: (next: T) => void }).set : undefined
34
+ if (setter !== undefined) {
35
+ /* Bound: a pure pass-through to the parent's target — no local store. */
36
+ return {
37
+ get value(): T {
38
+ return read()
39
+ },
40
+ set value(next: T) {
41
+ setter(next)
42
+ },
43
+ }
44
+ }
45
+ /* Unbound: a local reseeding cell — writes echo locally, reseed on parent change. */
46
+ return linked(read) as State<T>
47
+ }
@@ -0,0 +1,24 @@
1
+ import { isAsyncCell } from '../../shared/isAsyncCell.ts'
2
+ import type { AsyncComputed } from '../../shared/types/AsyncComputed.ts'
3
+
4
+ /*
5
+ Whether a control-flow subject (`{#if}`/`{#switch}`) is a still-loading async cell — no
6
+ value yet, no error (`AsyncComputed.pending()`). The compiler pairs it with the value read
7
+ for a bare async subject: while `cellPending(cell)` is true the block renders NO branch, so
8
+ "still loading" never collapses into the falsy branch (`{#if getProfile()}` does not flash
9
+ its `{:else}` before the promise settles). A settled falsy value (or a held value being
10
+ refreshed) is NOT pending, so it still routes to its branch. Non-async subjects (a plain
11
+ value, a sync `computed`) are never pending — the block reads them directly.
12
+
13
+ Reading `pending()` subscribes the block's effect to the cell's in-flight/has-value facets,
14
+ so the block re-renders when the value arrives. An errored cell is NOT pending: the paired
15
+ value read (`$$readCell`) then throws its `AsyncCellError`, routing the error to the nearest
16
+ `{#try}` exactly as a bare read does.
17
+ */
18
+ // @documentation plumbing
19
+ export function cellPending(cell: unknown): boolean {
20
+ if (isAsyncCell(cell)) {
21
+ return (cell as AsyncComputed<unknown>).pending()
22
+ }
23
+ return false
24
+ }
@@ -5,7 +5,8 @@ import { clearBetween } from './clearBetween.ts'
5
5
  The standard teardown for a marker-bounded range (component, layout/page boundary,
6
6
  slot): dispose its lexical scope (which stops the content's reactivity first, then its
7
7
  nested children and capabilities), and clear the nodes between the markers — leaving the
8
- markers in place so a hot swap rebuilds the range. Shared by every range mount
8
+ markers in place so a later rebuild (navigation, control-flow re-fill) refills the range.
9
+ Shared by every range mount
9
10
  (`fillRange`, `mountRange`, `fillBoundary`) so the one disposer contract lives in a
10
11
  single place.
11
12
  */
@@ -1,4 +1,5 @@
1
1
  import { effect } from '../effect.ts'
2
+ import { CURRENT_PATH } from '../runtime/CURRENT_PATH.ts'
2
3
  import { claimChild } from '../runtime/claimChild.ts'
3
4
  import { claimExpected } from '../runtime/claimExpected.ts'
4
5
  import { RENDER } from '../runtime/RENDER.ts'
@@ -6,6 +7,7 @@ import { scope } from '../runtime/scope.ts'
6
7
  import { scopeGroup } from '../runtime/scopeGroup.ts'
7
8
  import type { State } from '../runtime/types/State.ts'
8
9
  import { withoutHydration } from '../runtime/withoutHydration.ts'
10
+ import { withPathFrom } from '../runtime/withPathFrom.ts'
9
11
  import { state } from '../state.ts'
10
12
  import { buildDetachedRange } from './buildDetachedRange.ts'
11
13
  import { moveRange } from './moveRange.ts'
@@ -41,8 +43,17 @@ export function each<T>(
41
43
  read them; a direct caller reads `item.value` / `index.value`. */
42
44
  render: (parent: Node, item: State<T>, index: State<number>) => void,
43
45
  before: Node | null = null,
46
+ /* Whether the each has an explicit `by` key. A KEYED row takes its stable key as its
47
+ render-path segment (survives reorders/reloads); a KEYLESS row (no `by`, so `keyOf`
48
+ returns the raw item identity — unstable) takes its POSITION instead, which is at least
49
+ deterministic SSR↔client for a single render. Absent → keyless (a direct caller). */
50
+ keyed = false,
44
51
  ): void {
45
52
  const rows = new Map<string, EachRow>()
53
+ /* This each's render-path ancestry, captured at construction; every row builds under
54
+ `basePath/segment` so a component/cell in a row gets a stable id, re-established on a
55
+ post-mount reconcile (when the ambient path is gone). */
56
+ const basePath = CURRENT_PATH.current
46
57
  /* Monotonic reconcile-pass id. Each reconcile stamps every surviving row with the
47
58
  current pass, so prune is a `row.gen !== pass` int compare instead of building a
48
59
  `Set` of present keys every reconcile. */
@@ -66,11 +77,19 @@ export function each<T>(
66
77
  reconcile below) instead of rebuilding it. */
67
78
  const cell = state(item) as State<unknown>
68
79
  const indexCell = state(position)
80
+ /* Keyed rows key their path on the stable key; keyless on the position. */
81
+ const segment = keyed ? keyOf(item) : position
69
82
  const hydration = RENDER.hydration
70
83
  if (hydration !== undefined) {
71
84
  const start = claimExpected(hydration, parent, 'each row start marker')
72
85
  hydration.next.set(parent, start.nextSibling)
73
- const dispose = group.track(scope(() => render(parent, cell as State<T>, indexCell)))
86
+ const dispose = group.track(
87
+ scope(() =>
88
+ withPathFrom(basePath, segment, () =>
89
+ render(parent, cell as State<T>, indexCell),
90
+ ),
91
+ ),
92
+ )
74
93
  const end = claimExpected(hydration, parent, 'each row end marker')
75
94
  hydration.next.set(parent, end.nextSibling)
76
95
  return { start, end, dispose, cell, indexCell }
@@ -79,7 +98,7 @@ export function each<T>(
79
98
  foreign namespace (so svg/math row children are namespaced, not built as HTML),
80
99
  held in `pending` until placement inserts it. */
81
100
  const { start, end, fragment, dispose } = buildDetachedRange(parent, (host) =>
82
- render(host, cell as State<T>, indexCell),
101
+ withPathFrom(basePath, segment, () => render(host, cell as State<T>, indexCell)),
83
102
  )
84
103
  return { start, end, dispose: group.track(dispose), cell, indexCell, pending: fragment }
85
104
  }
@@ -107,7 +126,9 @@ export function each<T>(
107
126
  const hydration = RENDER.hydration
108
127
  if (hydration !== undefined) {
109
128
  let position = 0
110
- for (const item of items()) {
129
+ /* An undefined source renders an empty list, not a throw — a `{#for x in promise}`
130
+ whose lifted source peeks undefined while pending (ADR-0032 D3). */
131
+ for (const item of items() ?? []) {
111
132
  const key = keyOf(item)
112
133
  const row = buildRow(item, position) // claims the SSR row where it sits
113
134
  if (rows.has(key)) {
@@ -148,7 +169,8 @@ export function each<T>(
148
169
  freshly keyed row. withoutHydration restores the outer cursor after, so the
149
170
  enclosing hydrate pass is untouched (mirrors awaitBlock/tryBlock). */
150
171
  withoutHydration(() => {
151
- const list = Array.isArray(source) ? source : [...source]
172
+ /* Undefined source → empty list (see the hydration loop, ADR-0032 D3). */
173
+ const list = source == null ? [] : Array.isArray(source) ? source : [...source]
152
174
  const keys = list.map(keyOf)
153
175
  generation += 1
154
176
  const pass = generation
@@ -1,9 +1,12 @@
1
1
  import { effect } from '../effect.ts'
2
+ import { CURRENT_BOUNDARY } from '../runtime/CURRENT_BOUNDARY.ts'
3
+ import { CURRENT_PATH } from '../runtime/CURRENT_PATH.ts'
2
4
  import { claimChild } from '../runtime/claimChild.ts'
3
5
  import { generationGuard } from '../runtime/generationGuard.ts'
4
6
  import { RENDER } from '../runtime/RENDER.ts'
5
7
  import { scopeGroup } from '../runtime/scopeGroup.ts'
6
8
  import type { State } from '../runtime/types/State.ts'
9
+ import { withPathFrom } from '../runtime/withPathFrom.ts'
7
10
  import { state } from '../state.ts'
8
11
  import { buildDetachedRange } from './buildDetachedRange.ts'
9
12
  import { removeRange } from './removeRange.ts'
@@ -32,11 +35,23 @@ export function eachAsync<T>(
32
35
  `each`; the streaming runtime rebuilds the row on a re-yield rather than patching, and
33
36
  the position is the stream arrival ordinal (a stream only appends, never reorders). */
34
37
  render: (parent: Node, item: State<T>, index: State<number>) => void,
35
- /* Absent → an iterator rejection surfaces instead of rendering a catch branch. */
38
+ /* Absent → an iterator rejection routes to the enclosing {#try} boundary captured at build
39
+ (if any), else surfaces, instead of rendering a catch branch. */
36
40
  renderCatch: ((parent: Node, error: unknown) => void) | undefined,
37
41
  before: Node | null = null,
42
+ /* Explicit `by` key → the row keys its render-path segment on the stable key; else on the
43
+ stream arrival ordinal (a stream only appends, so the ordinal is stable). */
44
+ keyed = false,
38
45
  ): void {
39
46
  const rows = new Map<string, EachRow>()
47
+ /* This each's render-path ancestry, captured at build; each row builds under
48
+ `basePath/segment` so a component/cell in a streamed row gets a stable id even though the
49
+ row arrives after the ambient path is gone. */
50
+ const basePath = CURRENT_PATH.current
51
+ /* The enclosing {#try} boundary ambient at BUILD (see `awaitBlock`). A catch-less rejection
52
+ arrives LATER from the async drain, after CURRENT_BOUNDARY has been restored, so capture
53
+ it into this closure now rather than read it at settle time. */
54
+ const capturedBoundary = CURRENT_BOUNDARY.current
40
55
  /* Each row's (and the error branch's) scope, registered with the owner so they
41
56
  dispose on owner teardown; the block's own teardown only stops the stream. */
42
57
  const group = scopeGroup()
@@ -87,6 +102,17 @@ export function eachAsync<T>(
87
102
  iterator?.return?.(undefined)?.catch(() => undefined) // close the superseded run's iterator before re-streaming
88
103
  iterator = undefined
89
104
  clearError() // a fresh run drops a prior error branch
105
+ /* A reseed streams a FRESH source (e.g. `room(newId)`), so the prior run's rows are stale.
106
+ Drop them now rather than relying on the drain's completion prune below — a never-
107
+ completing source (a socket) never reaches that prune, so a key the new stream doesn't
108
+ re-yield would otherwise leak in the DOM and the `rows` map forever (and mix the old
109
+ source's rows in with the new). Keys the new stream does re-yield rebuild fresh (the
110
+ block rebuilds on every re-yield regardless), so nothing reusable is lost. */
111
+ for (const [, row] of rows) {
112
+ row.dispose()
113
+ removeRange(row.start, row.end)
114
+ }
115
+ rows.clear()
90
116
  const iterable = items() // read (subscribe) synchronously
91
117
  const present = new Set<string>()
92
118
  let arrivals = 0 // stream arrival ordinal → each row's index
@@ -110,9 +136,14 @@ export function eachAsync<T>(
110
136
  const stale = rows.get(key)
111
137
  const cell = state(result.value) as State<unknown>
112
138
  const indexCell = state(arrivals)
139
+ const segment = keyed ? key : arrivals
113
140
  arrivals += 1
114
141
  rows.set(key, {
115
- ...insertRange((host) => render(host, cell as State<T>, indexCell)),
142
+ ...insertRange((host) =>
143
+ withPathFrom(basePath, segment, () =>
144
+ render(host, cell as State<T>, indexCell),
145
+ ),
146
+ ),
116
147
  cell,
117
148
  indexCell,
118
149
  })
@@ -133,8 +164,13 @@ export function eachAsync<T>(
133
164
  if (!guard.live(generationAtStart)) {
134
165
  return
135
166
  }
136
- /* No catch branch → surface the rejection (mirrors `<template await>`). */
167
+ /* No catch branch → route to the enclosing {#try} boundary captured at build if one
168
+ was ambient; otherwise surface the rejection (mirrors `<template await>`). */
137
169
  if (renderCatch === undefined) {
170
+ if (capturedBoundary !== undefined) {
171
+ capturedBoundary.handle(error)
172
+ return
173
+ }
138
174
  throw error
139
175
  }
140
176
  /* Keep the streamed rows; render the catch branch after them, at the anchor. */
@@ -24,15 +24,14 @@ export function fillBoundary(
24
24
  close: Comment,
25
25
  build: (host: Node, props?: UiProps) => void,
26
26
  props: UiProps | undefined,
27
- label: string | undefined,
28
27
  ): { dispose: () => void } {
29
28
  const hydration = RENDER.hydration
30
29
  if (hydration === undefined) {
31
- return fillRange(open, close, build, props, label)
30
+ return fillRange(open, close, build, props)
32
31
  }
33
32
  /* Hydrate: adopt the server content between the markers in place. */
34
33
  const parent = open.parentNode as Node
35
34
  hydration.next.set(parent, open.nextSibling)
36
- const scoped = withScope(label, () => scope(() => build(parent, props)))
35
+ const scoped = withScope(() => scope(() => build(parent, props)))
37
36
  return { dispose: disposeRange(scoped, open, close) }
38
37
  }
@@ -14,8 +14,9 @@ selector-transparent (a true direct child of its parent, no `<abide-name>` wrapp
14
14
  Brackets a render pass (a nested child continues the parent's block-id counter) and
15
15
  establishes the child's lexical scope in `awaiting` mode so it adopts the model doc
16
16
  its first `doc()` creates. The disposer stops the content's reactivity, disposes the
17
- lexical scope, and clears the range — leaving the markers, so a hot swap rebuilds in
18
- place. Shared by `mountRange` (create branch) and `hotReplace` (re-fill on edit).
17
+ lexical scope, and clears the range — leaving the markers, so a later rebuild
18
+ (navigation, control-flow re-fill) fills in place. Shared by `mountRange` (create
19
+ branch) and `fillBoundary` (a page/layout outlet boundary).
19
20
  */
20
21
  // @documentation plumbing
21
22
  export function fillRange(
@@ -23,8 +24,7 @@ export function fillRange(
23
24
  end: Comment,
24
25
  build: (host: Node, props?: UiProps) => void,
25
26
  props: UiProps | undefined,
26
- label: string | undefined,
27
27
  ): { start: Comment; end: Comment; dispose: () => void } {
28
- const scoped = withScope(label, () => fillBefore(end, (fragment) => build(fragment, props)))
28
+ const scoped = withScope(() => fillBefore(end, (fragment) => build(fragment, props)))
29
29
  return { start, end, dispose: disposeRange(scoped, start, end) }
30
30
  }
@@ -1,8 +1,6 @@
1
- import { hydratingSlot } from '../../shared/hydratingSlot.ts'
2
- import { wakeHydrationPeeks } from '../../shared/wakeHydrationPeeks.ts'
1
+ import { hydrationWindow } from '../../shared/hydrationWindow.ts'
3
2
  import { RENDER } from '../runtime/RENDER.ts'
4
3
  import { scope } from '../runtime/scope.ts'
5
- import { scopeLabel } from './scopeLabel.ts'
6
4
  import { withScope } from './withScope.ts'
7
5
 
8
6
  /*
@@ -27,21 +25,17 @@ export function hydrate(
27
25
  props?: unknown,
28
26
  ): () => void {
29
27
  const previous = RENDER.hydration
30
- const previousHydrating = hydratingSlot.active
31
28
  RENDER.hydration = { next: new Map() }
32
- hydratingSlot.active = true
29
+ hydrationWindow.enter()
33
30
  try {
34
31
  /* Same shared mount core as `mount` (see `withScope`) — a hydrated component owns a
35
32
  scope too, adopting the model its build adopts — run with the claim cursor active. */
36
- const { lexical } = withScope(scopeLabel(host), () => scope(() => build(host, props)))
33
+ const { lexical } = withScope(() => scope(() => build(host, props)))
37
34
  return () => lexical.dispose()
38
35
  } finally {
39
36
  RENDER.hydration = previous
40
- hydratingSlot.active = previousHydrating
41
- /* Wake the peeks this pass withheld only when unwinding the OUTERMOST pass, so a
42
- nested child hydrate doesn't fire it early (save/restore, not a bare reset). */
43
- if (!previousHydrating) {
44
- wakeHydrationPeeks()
45
- }
37
+ /* Outermost exit clears the window and wakes the peeks this pass withheld; a nested
38
+ child hydrate raises/lowers the depth without firing it early. */
39
+ hydrationWindow.exit()
46
40
  }
47
41
  }
@@ -1,6 +1,6 @@
1
1
  /* A comment node carrying exactly `data`. Empty-child check distinguishes the
2
2
  `<!--data-->` marker from an element that happens to expose a `data` property.
3
- Shared by the marker-range scanners in appendText/applyResolved. */
3
+ Shared by the marker-range scanners in appendText. */
4
4
  export function isComment(node: Node, data: string): boolean {
5
5
  return (node as { data?: string }).data === data && node.childNodes.length === 0
6
6
  }
@@ -0,0 +1,29 @@
1
+ import { commentData } from './commentData.ts'
2
+ import { markerDepthDelta } from './markerDepthDelta.ts'
3
+
4
+ /*
5
+ The `]` close marker that balances the `[` range opened by `open`, found by a DEPTH-counting
6
+ scan over the following siblings. A branch's content can hold nested `[`…`]` (if/switch/each)
7
+ and `abide:`…`/abide:` (await/try/snippet) ranges — `markerDepthDelta` scores each open `+1`
8
+ and each close `-1`, so an INNER close is skipped and only the one that returns to depth 0 is
9
+ this range's own. Unlike `discardBoundary`'s named-boundary scan (a unique id-suffixed close
10
+ string), the `[`/`]` alphabet is non-unique, so the depth count is what tells nested closes
11
+ apart. Throws on a desync (no balancing close) rather than over-scan to end-of-parent.
12
+ */
13
+ export function matchingRangeClose(open: Node): Node {
14
+ let depth = 1
15
+ let node = open.nextSibling
16
+ while (node !== null) {
17
+ const data = commentData(node)
18
+ if (data !== undefined) {
19
+ depth += markerDepthDelta(data)
20
+ if (depth === 0) {
21
+ return node
22
+ }
23
+ }
24
+ node = node.nextSibling
25
+ }
26
+ throw new Error(
27
+ '[abide] hydration desync: a control-flow range open marker "[" has no balancing close "]" — the server DOM is truncated or a block diverged.',
28
+ )
29
+ }