@abide/abide 0.48.0 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (360) hide show
  1. package/AGENTS.md +85 -59
  2. package/CHANGELOG.md +216 -0
  3. package/README.md +21 -10
  4. package/package.json +15 -4
  5. package/src/abideLsp.ts +5 -6
  6. package/src/abideResolverPlugin.ts +197 -169
  7. package/src/build.ts +79 -56
  8. package/src/buildCli.ts +3 -5
  9. package/src/buildDisconnected.ts +2 -5
  10. package/src/bundleApp.ts +2 -3
  11. package/src/checkAbide.ts +12 -2
  12. package/src/compile.ts +2 -4
  13. package/src/devEntry.ts +113 -28
  14. package/src/discoveryEntry.ts +3 -2
  15. package/src/lib/bundle/disconnected.abide +69 -73
  16. package/src/lib/bundle/infoPlist.ts +13 -7
  17. package/src/lib/bundle/installDownloads.ts +13 -3
  18. package/src/lib/bundle/installMacMenu.ts +13 -3
  19. package/src/lib/bundle/spawnEmbeddedServer.ts +13 -4
  20. package/src/lib/bundle/waitForServer.ts +6 -1
  21. package/src/lib/cli/parseArgvForRpc.ts +36 -9
  22. package/src/lib/cli/tokenizeArgvFlags.ts +4 -2
  23. package/src/lib/mcp/createMcpResourceServer.ts +13 -2
  24. package/src/lib/mcp/mcpTools.ts +15 -6
  25. package/src/lib/mcp/toolResultFromResponse.ts +40 -12
  26. package/src/lib/server/error.ts +6 -6
  27. package/src/lib/server/json.ts +17 -3
  28. package/src/lib/server/rpc/defineRpc.ts +58 -24
  29. package/src/lib/server/rpc/parseArgs.ts +113 -14
  30. package/src/lib/server/rpc/resolveRpcJsonSchema.ts +28 -0
  31. package/src/lib/server/rpc/runWithRpcTimeout.ts +12 -1
  32. package/src/lib/server/rpc/types/RpcHelper.ts +75 -105
  33. package/src/lib/server/rpc/types/RpcRegistryEntry.ts +24 -9
  34. package/src/lib/server/rpc/validationError.ts +1 -1
  35. package/src/lib/server/runtime/DEV_RELOAD_CLIENT_SCRIPT.ts +0 -15
  36. package/src/lib/server/runtime/buildCacheSnapshot.ts +10 -9
  37. package/src/lib/server/runtime/buildInspectorSurface.ts +6 -4
  38. package/src/lib/server/runtime/buildOpenApiSpec.ts +55 -11
  39. package/src/lib/server/runtime/buildPreloadManifest.ts +12 -10
  40. package/src/lib/server/runtime/createAppAssetServer.ts +18 -15
  41. package/src/lib/server/runtime/createAppRouteResolver.ts +145 -0
  42. package/src/lib/server/runtime/createPlumbingRouter.ts +212 -0
  43. package/src/lib/server/runtime/createRouteDispatcher.ts +4 -10
  44. package/src/lib/server/runtime/createServer.ts +120 -324
  45. package/src/lib/server/runtime/createUiPageRenderer.ts +137 -26
  46. package/src/lib/server/runtime/devClientFingerprint.ts +19 -34
  47. package/src/lib/server/runtime/finalizeResponse.ts +11 -1
  48. package/src/lib/server/runtime/installAmbientScopeStore.ts +30 -9
  49. package/src/lib/server/runtime/logExposedSurfaces.ts +8 -7
  50. package/src/lib/server/runtime/pathStore.ts +15 -0
  51. package/src/lib/server/runtime/registryManifests.ts +1 -1
  52. package/src/lib/server/runtime/renderCellBarrierStore.ts +15 -0
  53. package/src/lib/server/runtime/runWithRequestScope.ts +3 -0
  54. package/src/lib/server/runtime/serializeCacheSnapshot.ts +6 -4
  55. package/src/lib/server/runtime/snapshotEntryFromCache.ts +24 -16
  56. package/src/lib/server/runtime/streamCacheResolutions.ts +14 -7
  57. package/src/lib/server/runtime/streamFromIterator.ts +30 -2
  58. package/src/lib/server/runtime/textResponse.ts +23 -0
  59. package/src/lib/server/runtime/types/DevReloadStamp.ts +6 -10
  60. package/src/lib/server/runtime/types/InspectorCacheEntry.ts +1 -1
  61. package/src/lib/server/runtime/types/RequestStore.ts +27 -0
  62. package/src/lib/server/runtime/warnUnguardedMcp.ts +15 -5
  63. package/src/lib/server/sockets/createSocketDispatcher.ts +9 -11
  64. package/src/lib/server/sockets/defineSocket.ts +5 -4
  65. package/src/lib/server/sse.ts +5 -1
  66. package/src/lib/shared/ASYNC_CELL.ts +5 -0
  67. package/src/lib/shared/DEV_REBUILD_PATH.ts +6 -0
  68. package/src/lib/shared/HTTP_METHODS.ts +6 -0
  69. package/src/lib/shared/HttpError.ts +1 -5
  70. package/src/lib/shared/MCP_PATH.ts +6 -0
  71. package/src/lib/shared/PROXIED_SERVER_SUBDIRS.ts +15 -0
  72. package/src/lib/shared/REF_JSON_TAGS.ts +13 -1
  73. package/src/lib/shared/RPC_SHIM_GLOBALS.ts +9 -0
  74. package/src/lib/shared/activeCacheStore.ts +1 -0
  75. package/src/lib/shared/activePage.ts +1 -0
  76. package/src/lib/shared/buildArtifact.ts +20 -0
  77. package/src/lib/shared/buildRpcRequest.ts +6 -2
  78. package/src/lib/shared/buildSocketOverChannel.ts +4 -3
  79. package/src/lib/shared/bundleGraphFromMetafile.ts +68 -0
  80. package/src/lib/shared/cache.ts +156 -131
  81. package/src/lib/shared/canonicalJson.ts +24 -1
  82. package/src/lib/shared/changeAffectsClient.ts +12 -7
  83. package/src/lib/shared/createCacheStore.ts +31 -8
  84. package/src/lib/shared/createChannelLog.ts +24 -0
  85. package/src/lib/shared/createLifecycleChannel.ts +7 -1
  86. package/src/lib/shared/createReachable.ts +47 -78
  87. package/src/lib/shared/createRemoteFunction.ts +52 -30
  88. package/src/lib/shared/createRpcServerProgram.ts +820 -0
  89. package/src/lib/shared/decodeRefJson.ts +4 -4
  90. package/src/lib/shared/decodeResponse.ts +2 -2
  91. package/src/lib/shared/decodeWireBody.ts +35 -0
  92. package/src/lib/shared/detectRpcMethod.ts +8 -1
  93. package/src/lib/shared/done.ts +8 -2
  94. package/src/lib/shared/encodeRefJson.ts +4 -4
  95. package/src/lib/shared/encodeWireBody.ts +18 -0
  96. package/src/lib/shared/exitOnBuildFailure.ts +4 -3
  97. package/src/lib/{server/rpc → shared}/fieldErrorsFromIssues.ts +5 -1
  98. package/src/lib/shared/generateDeclarations.ts +72 -0
  99. package/src/lib/shared/hasSeedableRequest.ts +25 -0
  100. package/src/lib/shared/hydrationWindow.ts +53 -0
  101. package/src/lib/shared/isAsyncCell.ts +11 -0
  102. package/src/lib/shared/isAsyncIterable.ts +8 -0
  103. package/src/lib/shared/isSubscribable.ts +3 -3
  104. package/src/lib/shared/isThenable.ts +9 -0
  105. package/src/lib/shared/jsonSchemaForType.ts +258 -0
  106. package/src/lib/shared/lenientDecode.ts +15 -0
  107. package/src/lib/shared/loadProjectTsConfig.ts +28 -0
  108. package/src/lib/shared/markFrameworkSourcesIgnored.ts +1 -1
  109. package/src/lib/shared/matchRoute.ts +4 -14
  110. package/src/lib/shared/parseEnv.ts +17 -6
  111. package/src/lib/shared/peek.ts +14 -0
  112. package/src/lib/shared/pending.ts +9 -6
  113. package/src/lib/shared/pendingAsyncCellsSlot.ts +13 -0
  114. package/src/lib/shared/prepareRpcModule.ts +91 -93
  115. package/src/lib/shared/prepareSocketModule.ts +3 -2
  116. package/src/lib/shared/probeRegistries.ts +5 -18
  117. package/src/lib/shared/reachable.ts +7 -10
  118. package/src/lib/shared/refresh.ts +12 -2
  119. package/src/lib/shared/refreshing.ts +8 -2
  120. package/src/lib/shared/resolvedCellsSlot.ts +13 -0
  121. package/src/lib/shared/reviveWireField.ts +49 -0
  122. package/src/lib/shared/reviveWireOutput.ts +36 -0
  123. package/src/lib/shared/rpcServerForRoot.ts +25 -0
  124. package/src/lib/shared/scanPages.ts +25 -0
  125. package/src/lib/shared/serializeEnv.ts +18 -6
  126. package/src/lib/shared/snapshotShippable.ts +8 -7
  127. package/src/lib/shared/snippet.ts +11 -6
  128. package/src/lib/shared/streamedCellsSlot.ts +14 -0
  129. package/src/lib/shared/subscribableFromResponse.ts +4 -4
  130. package/src/lib/shared/subscribableProbes.ts +1 -1
  131. package/src/lib/shared/tailProbeSlot.ts +1 -1
  132. package/src/lib/shared/types/AsyncComputed.ts +20 -0
  133. package/src/lib/shared/types/AsyncState.ts +13 -0
  134. package/src/lib/shared/types/CacheEntry.ts +7 -7
  135. package/src/lib/shared/types/CacheOptions.ts +23 -37
  136. package/src/lib/shared/types/CachePolicy.ts +25 -0
  137. package/src/lib/shared/types/CacheSnapshotEntry.ts +6 -5
  138. package/src/lib/shared/types/ErrorJsonSchemas.ts +8 -0
  139. package/src/lib/shared/types/HttpMethod.ts +3 -1
  140. package/src/lib/shared/types/InputCoercion.ts +17 -0
  141. package/src/lib/shared/types/{Subscribable.ts → NamedAsyncIterable.ts} +1 -1
  142. package/src/lib/shared/types/OutputWirePlan.ts +17 -0
  143. package/src/lib/shared/types/PagesScan.ts +7 -0
  144. package/src/lib/shared/types/PendingAsyncCells.ts +10 -0
  145. package/src/lib/shared/types/RawRemoteFunction.ts +6 -0
  146. package/src/lib/shared/types/RemoteCallable.ts +8 -7
  147. package/src/lib/shared/types/RemoteFunction.ts +16 -15
  148. package/src/lib/shared/types/ResolvedCells.ts +11 -0
  149. package/src/lib/shared/types/ReturnBody.ts +15 -0
  150. package/src/lib/shared/types/RpcBuildStamps.ts +23 -0
  151. package/src/lib/shared/types/RpcErrorGuard.ts +3 -8
  152. package/src/lib/shared/types/Socket.ts +4 -4
  153. package/src/lib/shared/types/SsrPayload.ts +5 -1
  154. package/src/lib/shared/types/StreamPolicy.ts +11 -0
  155. package/src/lib/shared/types/StreamedCells.ts +19 -0
  156. package/src/lib/shared/types/StreamedResolution.ts +24 -6
  157. package/src/lib/shared/types/TailHooks.ts +1 -1
  158. package/src/lib/shared/types/WireKind.ts +11 -0
  159. package/src/lib/shared/url.ts +5 -0
  160. package/src/lib/shared/validationHttpError.ts +33 -0
  161. package/src/lib/shared/warmSeedKey.ts +16 -0
  162. package/src/lib/shared/wireJsonReplacer.ts +30 -0
  163. package/src/lib/shared/writeRpcDts.ts +11 -1
  164. package/src/lib/shared/writeTestSocketsDts.ts +1 -1
  165. package/src/lib/test/createTestApp.ts +19 -1
  166. package/src/lib/ui/README.md +1 -1
  167. package/src/lib/ui/activePendingCells.ts +14 -0
  168. package/src/lib/ui/compile/BLOCK_KEYWORDS.ts +21 -0
  169. package/src/lib/ui/compile/SSR_ESCAPE.ts +4 -1
  170. package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +35 -7
  171. package/src/lib/ui/compile/abideUiPlugin.ts +19 -2
  172. package/src/lib/ui/compile/analyzeComponent.ts +89 -6
  173. package/src/lib/ui/compile/assignmentTargetNames.ts +54 -0
  174. package/src/lib/ui/compile/asyncInterpolationFields.ts +152 -0
  175. package/src/lib/ui/compile/asyncValuePositionError.ts +21 -0
  176. package/src/lib/ui/compile/asyncValuePositionInterpolations.ts +64 -0
  177. package/src/lib/ui/compile/attrLiftPosition.ts +18 -0
  178. package/src/lib/ui/compile/bindListenEvent.ts +5 -6
  179. package/src/lib/ui/compile/cachedSourceFile.ts +40 -0
  180. package/src/lib/ui/compile/catchBinding.ts +7 -5
  181. package/src/lib/ui/compile/classifyInterpolationType.ts +46 -0
  182. package/src/lib/ui/compile/collectAbideDiagnostics.ts +96 -0
  183. package/src/lib/ui/compile/compileComponent.ts +19 -3
  184. package/src/lib/ui/compile/compileModule.ts +27 -58
  185. package/src/lib/ui/compile/compileSSR.ts +52 -13
  186. package/src/lib/ui/compile/compileShadow.ts +265 -42
  187. package/src/lib/ui/compile/composeProps.ts +15 -4
  188. package/src/lib/ui/compile/createShadowLanguageService.ts +116 -68
  189. package/src/lib/ui/compile/createShadowProgram.ts +35 -6
  190. package/src/lib/ui/compile/declaredNames.ts +36 -0
  191. package/src/lib/ui/compile/desugarSignals.ts +419 -37
  192. package/src/lib/ui/compile/expressionIsPrefixEvaluable.ts +19 -0
  193. package/src/lib/ui/compile/generateBuild.ts +74 -17
  194. package/src/lib/ui/compile/generateSSR.ts +286 -75
  195. package/src/lib/ui/compile/hoistableAwaits.ts +193 -0
  196. package/src/lib/ui/compile/hoistableChildRenders.ts +112 -0
  197. package/src/lib/ui/compile/interpolatedTemplateLiteral.ts +3 -1
  198. package/src/lib/ui/compile/interpolationClassifierForRoot.ts +37 -0
  199. package/src/lib/ui/compile/isSpuriousAsyncReadDiagnostic.ts +174 -0
  200. package/src/lib/ui/compile/isWhitespaceText.ts +10 -2
  201. package/src/lib/ui/compile/liftAsyncSubExpressions.ts +166 -0
  202. package/src/lib/ui/compile/lowerAsyncInterpolations.ts +92 -0
  203. package/src/lib/ui/compile/lowerContext.ts +10 -0
  204. package/src/lib/ui/compile/lowerDocAccess.ts +51 -10
  205. package/src/lib/ui/compile/lowerScript.ts +40 -4
  206. package/src/lib/ui/compile/nodeAtShadowOffset.ts +28 -0
  207. package/src/lib/ui/compile/parseTemplate.ts +18 -1090
  208. package/src/lib/ui/compile/parseTemplateRecovering.ts +1385 -0
  209. package/src/lib/ui/compile/referencedIdentifiers.ts +35 -0
  210. package/src/lib/ui/compile/renameSignalRefs.ts +26 -40
  211. package/src/lib/ui/compile/resolveReactiveExport.ts +4 -7
  212. package/src/lib/ui/compile/scopeCss.ts +27 -4
  213. package/src/lib/ui/compile/seedTypeClassifierForRoot.ts +65 -0
  214. package/src/lib/ui/compile/shadowInterpolationClassifier.ts +47 -0
  215. package/src/lib/ui/compile/sourceFileOptionsSignature.ts +20 -0
  216. package/src/lib/ui/compile/structuralHeadTokens.ts +107 -0
  217. package/src/lib/ui/compile/templateSemanticTokens.ts +21 -0
  218. package/src/lib/ui/compile/templateStartOffset.ts +15 -0
  219. package/src/lib/ui/compile/tryPlan.ts +4 -3
  220. package/src/lib/ui/compile/types/AnalyzedComponent.ts +4 -0
  221. package/src/lib/ui/compile/types/AsyncInterpolationField.ts +19 -0
  222. package/src/lib/ui/compile/types/InterpolationClassifier.ts +12 -0
  223. package/src/lib/ui/compile/types/InterpolationKind.ts +7 -0
  224. package/src/lib/ui/compile/types/ParseDiagnostic.ts +8 -0
  225. package/src/lib/ui/compile/types/SeedTypeClassifier.ts +15 -0
  226. package/src/lib/ui/compile/types/TemplateNode.ts +36 -4
  227. package/src/lib/ui/compile/types/ValuePositionInterpolation.ts +13 -0
  228. package/src/lib/ui/compile/writtenTemplateNames.ts +84 -0
  229. package/src/lib/ui/computed.ts +28 -1
  230. package/src/lib/ui/createScope.ts +35 -68
  231. package/src/lib/ui/dom/anchoredBranch.ts +142 -0
  232. package/src/lib/ui/dom/appendText.ts +8 -3
  233. package/src/lib/ui/dom/awaitBlock.ts +62 -75
  234. package/src/lib/ui/dom/bindProp.ts +22 -0
  235. package/src/lib/ui/dom/bindableProp.ts +47 -0
  236. package/src/lib/ui/dom/cellPending.ts +24 -0
  237. package/src/lib/ui/dom/discardBoundary.ts +24 -6
  238. package/src/lib/ui/dom/disposeRange.ts +2 -1
  239. package/src/lib/ui/dom/each.ts +41 -12
  240. package/src/lib/ui/dom/eachAsync.ts +39 -3
  241. package/src/lib/ui/dom/fillBefore.ts +6 -7
  242. package/src/lib/ui/dom/fillBoundary.ts +2 -3
  243. package/src/lib/ui/dom/fillRange.ts +4 -4
  244. package/src/lib/ui/dom/hydrate.ts +6 -12
  245. package/src/lib/ui/dom/isComment.ts +1 -1
  246. package/src/lib/ui/dom/matchingRangeClose.ts +29 -0
  247. package/src/lib/ui/dom/mergeProps.ts +1 -1
  248. package/src/lib/ui/dom/mount.ts +1 -2
  249. package/src/lib/ui/dom/mountChild.ts +9 -40
  250. package/src/lib/ui/dom/mountRange.ts +2 -3
  251. package/src/lib/ui/dom/mountSlot.ts +1 -1
  252. package/src/lib/ui/dom/mountStreamedChild.ts +84 -0
  253. package/src/lib/ui/dom/mountSwappableRange.ts +46 -4
  254. package/src/lib/ui/dom/mutateDocContainer.ts +65 -0
  255. package/src/lib/ui/dom/on.ts +2 -2
  256. package/src/lib/ui/dom/readCell.ts +40 -0
  257. package/src/lib/ui/dom/restProps.ts +2 -2
  258. package/src/lib/ui/dom/spreadProps.ts +3 -4
  259. package/src/lib/ui/dom/switchBlock.ts +30 -7
  260. package/src/lib/ui/dom/tryBlock.ts +233 -70
  261. package/src/lib/ui/dom/types/SwitchCase.ts +5 -1
  262. package/src/lib/ui/dom/when.ts +12 -2
  263. package/src/lib/ui/dom/withScope.ts +9 -2
  264. package/src/lib/ui/finalizeStreamedChildren.ts +89 -0
  265. package/src/lib/ui/flight.ts +56 -0
  266. package/src/lib/ui/html.ts +12 -1
  267. package/src/lib/ui/isolateCellBarrier.ts +17 -0
  268. package/src/lib/ui/linked.ts +48 -2
  269. package/src/lib/ui/props.ts +17 -0
  270. package/src/lib/ui/remoteProxy.ts +84 -159
  271. package/src/lib/ui/renderChain.ts +53 -13
  272. package/src/lib/ui/renderToStream.ts +22 -18
  273. package/src/lib/ui/resumeSeedScript.ts +1 -1
  274. package/src/lib/ui/router.ts +86 -53
  275. package/src/lib/ui/runtime/AsyncCellError.ts +20 -0
  276. package/src/lib/ui/runtime/CELL_SEED.ts +14 -0
  277. package/src/lib/ui/runtime/CHILD_PRESENT.ts +2 -2
  278. package/src/lib/ui/runtime/CURRENT_BOUNDARY.ts +11 -0
  279. package/src/lib/ui/runtime/CURRENT_PATH.ts +29 -0
  280. package/src/lib/ui/runtime/RENDER.ts +10 -7
  281. package/src/lib/ui/runtime/RESUME.ts +4 -4
  282. package/src/lib/ui/runtime/STREAMED_CELLS.ts +57 -0
  283. package/src/lib/ui/runtime/ambientPathBacking.ts +32 -0
  284. package/src/lib/ui/runtime/applyPatchToTree.ts +17 -4
  285. package/src/lib/ui/runtime/blockId.ts +31 -0
  286. package/src/lib/ui/runtime/boundaryFor.ts +11 -0
  287. package/src/lib/ui/runtime/cellBarrierBacking.ts +30 -0
  288. package/src/lib/ui/runtime/createAsyncCell.ts +300 -0
  289. package/src/lib/ui/runtime/createDoc.ts +31 -54
  290. package/src/lib/ui/runtime/createEffectNode.ts +9 -0
  291. package/src/lib/ui/runtime/enterRenderPass.ts +5 -4
  292. package/src/lib/ui/runtime/flushEffects.ts +33 -1
  293. package/src/lib/ui/runtime/isAsyncFunction.ts +14 -0
  294. package/src/lib/ui/runtime/nextBlockId.ts +8 -7
  295. package/src/lib/ui/runtime/renderPath.ts +16 -0
  296. package/src/lib/ui/runtime/scope.ts +11 -0
  297. package/src/lib/ui/runtime/toTeardown.ts +10 -3
  298. package/src/lib/ui/runtime/types/Boundary.ts +9 -0
  299. package/src/lib/ui/runtime/types/RenderContext.ts +9 -7
  300. package/src/lib/ui/runtime/types/SsrRender.ts +9 -2
  301. package/src/lib/ui/runtime/types/UiComponent.ts +1 -5
  302. package/src/lib/ui/runtime/types/UiProps.ts +6 -5
  303. package/src/lib/ui/runtime/withOptionalPath.ts +8 -0
  304. package/src/lib/ui/runtime/withPath.ts +16 -0
  305. package/src/lib/ui/runtime/withPathFrom.ts +20 -0
  306. package/src/lib/ui/runtime/withoutHydration.ts +22 -0
  307. package/src/lib/ui/seedStreamedResolution.ts +31 -6
  308. package/src/lib/ui/settleAsyncCells.ts +24 -0
  309. package/src/lib/ui/socketProxy.ts +1 -1
  310. package/src/lib/ui/startClient.ts +16 -31
  311. package/src/lib/ui/state.ts +9 -3
  312. package/src/lib/ui/trackedComputed.ts +68 -0
  313. package/src/lib/ui/types/Scope.ts +8 -24
  314. package/src/lib/ui/watch.ts +3 -3
  315. package/src/serverEntry.ts +14 -0
  316. package/template/src/server/rpc/getHello.ts +15 -13
  317. package/template/test/app.test.ts +1 -1
  318. package/src/lib/server/runtime/devHotModuleResponse.ts +0 -41
  319. package/src/lib/shared/DEV_HOT_PREFIX.ts +0 -7
  320. package/src/lib/shared/UNREACHABLE_STATUSES.ts +0 -13
  321. package/src/lib/shared/hasReplayableRequest.ts +0 -17
  322. package/src/lib/shared/hydratingSlot.ts +0 -12
  323. package/src/lib/shared/outboxProbeSlot.ts +0 -20
  324. package/src/lib/shared/types/Outbox.ts +0 -9
  325. package/src/lib/shared/types/OutboxEntry.ts +0 -27
  326. package/src/lib/shared/types/SmartReadOptions.ts +0 -36
  327. package/src/lib/shared/wakeHydrationPeeks.ts +0 -20
  328. package/src/lib/ui/COMPONENT_WRAPPER_PREFIX.ts +0 -5
  329. package/src/lib/ui/compile/REACTIVE_CALLEES.ts +0 -11
  330. package/src/lib/ui/compile/assertRuntimeHelpersBound.ts +0 -80
  331. package/src/lib/ui/compile/markupTokens.ts +0 -313
  332. package/src/lib/ui/compile/structuralBlockTokens.ts +0 -100
  333. package/src/lib/ui/dom/applyResolved.ts +0 -87
  334. package/src/lib/ui/dom/scopeLabel.ts +0 -21
  335. package/src/lib/ui/dom/text.ts +0 -20
  336. package/src/lib/ui/history.ts +0 -101
  337. package/src/lib/ui/installHotBridge.ts +0 -93
  338. package/src/lib/ui/installInspectorBridge.ts +0 -140
  339. package/src/lib/ui/outbox.ts +0 -35
  340. package/src/lib/ui/persist.ts +0 -115
  341. package/src/lib/ui/rpcOutbox/createOutboxQueue.ts +0 -281
  342. package/src/lib/ui/rpcOutbox/outboxRegistry.ts +0 -69
  343. package/src/lib/ui/runtime/PATCH_BUS.ts +0 -28
  344. package/src/lib/ui/runtime/captureModelDoc.ts +0 -28
  345. package/src/lib/ui/runtime/hotInstances.ts +0 -10
  346. package/src/lib/ui/runtime/hotReloadEnabled.ts +0 -8
  347. package/src/lib/ui/runtime/hotReplace.ts +0 -38
  348. package/src/lib/ui/runtime/liveScopes.ts +0 -15
  349. package/src/lib/ui/runtime/localStoragePersistence.ts +0 -47
  350. package/src/lib/ui/runtime/registerHotInstance.ts +0 -23
  351. package/src/lib/ui/runtime/seedModelDoc.ts +0 -26
  352. package/src/lib/ui/runtime/types/HotInstance.ts +0 -22
  353. package/src/lib/ui/runtime/types/PatchEvent.ts +0 -16
  354. package/src/lib/ui/seedResolved.ts +0 -28
  355. package/src/lib/ui/sync.ts +0 -48
  356. package/src/lib/ui/types/History.ts +0 -14
  357. package/src/lib/ui/types/PersistHandle.ts +0 -11
  358. package/src/lib/ui/types/PersistenceStore.ts +0 -12
  359. package/src/lib/ui/types/ResolvedFrame.ts +0 -15
  360. package/src/lib/ui/types/SyncTransport.ts +0 -13
@@ -1,18 +1,18 @@
1
1
  import { decodeRefJson } from '../../shared/decodeRefJson.ts'
2
2
  import { effect } from '../effect.ts'
3
- import { claimChild } from '../runtime/claimChild.ts'
3
+ import { CURRENT_BOUNDARY } from '../runtime/CURRENT_BOUNDARY.ts'
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
+ import { withoutHydration } from '../runtime/withoutHydration.ts'
12
13
  import { state } from '../state.ts'
13
- import { buildDetachedRange } from './buildDetachedRange.ts'
14
+ import { anchoredBranch } from './anchoredBranch.ts'
14
15
  import { discardBoundary } from './discardBoundary.ts'
15
- import { removeRange } from './removeRange.ts'
16
16
 
17
17
  /*
18
18
  Async binding — the runtime for `<template await>`. Renders the pending branch,
@@ -38,12 +38,13 @@ parked just before the close marker.
38
38
  // @documentation plumbing
39
39
  export function awaitBlock(
40
40
  parent: Node,
41
- id: number,
41
+ id: string,
42
42
  promiseThunk: () => unknown,
43
43
  renderPending: ((parent: Node) => void) | undefined,
44
44
  renderThen: (parent: Node, value: unknown) => void,
45
- /* Absent when the block has no catch branch — a rejection then surfaces (re-throws
46
- 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. */
47
48
  renderCatch: ((parent: Node, error: unknown) => void) | undefined,
48
49
  /* A static node located by the skeleton: the block's anchor inserts before it on
49
50
  create (block before a static suffix). Null appends (tail). insertBefore(x, null)
@@ -51,11 +52,16 @@ export function awaitBlock(
51
52
  before: Node | null = null,
52
53
  ): void {
53
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
54
59
  /* The live branch's scope, registered with the owner so it disposes on owner
55
60
  teardown — not only when a settle/re-run swaps branches via detach. */
56
61
  const group = scopeGroup()
57
- let active: { start: Comment; end: Comment; dispose: () => void } | undefined
58
- 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)
59
65
  let first = true
60
66
  /* Bumped each run so a prior run's in-flight promise can't clobber a newer one, AND on
61
67
  owner teardown so an in-flight promise that settles AFTER the enclosing `{#if}`/
@@ -72,31 +78,6 @@ export function awaitBlock(
72
78
  place (then→then) or must build a fresh branch. */
73
79
  let activeKind: 'pending' | 'then' | 'catch' | undefined
74
80
 
75
- const detach = (): void => {
76
- if (active !== undefined) {
77
- active.dispose()
78
- /* `removeRange` evicts the markers AND everything between them via the end
79
- marker's LIVE parent — not the captured `parent`, which (when this await is a
80
- bare child of a control-flow branch) is the branch's build fragment, emptied
81
- into the document once the enclosing block placed it. */
82
- removeRange(active.start, active.end)
83
- active = undefined
84
- }
85
- }
86
-
87
- /* Replace the current content with a freshly-built branch, before the anchor. The branch
88
- builds into a detached `[`…`]`-bracketed fragment (so any content — components, text,
89
- nested blocks — appends freely), the same create primitive the keyed-list runtimes use,
90
- which lands as a marker-bounded range the next swap detaches with `removeRange`. */
91
- const place = (build: (parent: Node) => void): void => {
92
- detach()
93
- const namespaceParent = anchor?.parentNode ?? parent
94
- const { start, end, fragment, dispose } = buildDetachedRange(namespaceParent, build)
95
- const tracked = group.track(dispose)
96
- namespaceParent.insertBefore(fragment, anchor ?? null)
97
- active = { start, end, dispose: tracked }
98
- }
99
-
100
81
  /* Settle to a resolved value. then→then updates the cell in place — the branch and its
101
82
  inner each survive (no flash); any other prior kind builds a fresh then-branch around
102
83
  a new cell. renderThen receives the CELL (not the raw value), so the branch reads it
@@ -108,17 +89,23 @@ export function awaitBlock(
108
89
  }
109
90
  const cell = state(value)
110
91
  valueCell = cell
111
- place((host) => renderThen(host, cell))
92
+ branch.place((host) => renderThen(host, cell))
112
93
  activeKind = 'then'
113
94
  }
114
95
 
115
- /* 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. */
116
99
  const settleError = (error: unknown): void => {
117
100
  if (renderCatch === undefined) {
101
+ if (capturedBoundary !== undefined) {
102
+ capturedBoundary.handle(error)
103
+ return
104
+ }
118
105
  throw error
119
106
  }
120
107
  valueCell = undefined
121
- place((host) => renderCatch(host, error))
108
+ branch.place((host) => renderCatch(host, error))
122
109
  activeKind = 'catch'
123
110
  }
124
111
 
@@ -135,10 +122,10 @@ export function awaitBlock(
135
122
  shows the pending branch (or detaches) while the promise is in flight. */
136
123
  if (activeKind !== 'then') {
137
124
  if (renderPending !== undefined) {
138
- place((host) => renderPending(host))
125
+ branch.place((host) => renderPending(host))
139
126
  activeKind = 'pending'
140
127
  } else {
141
- detach()
128
+ branch.detach()
142
129
  activeKind = undefined
143
130
  }
144
131
  }
@@ -161,29 +148,36 @@ export function awaitBlock(
161
148
  marker for later swaps. The adopted content is everything the build claimed between
162
149
  the open and close markers; bracketing it makes the adopted branch a marker-bounded
163
150
  range identical to a freshly-`place`d one, so the FIRST swap detaches it with
164
- `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. */
165
154
  const adopt = (open: Node | null, build: (parent: Node) => void): void => {
166
155
  const cursor = hydration as NonNullable<typeof hydration>
167
156
  const firstAdopted = open?.nextSibling ?? null
168
157
  cursor.next.set(parent, firstAdopted)
169
- const dispose = group.track(scope(() => build(parent)))
170
- const close = claimChild(cursor, parent)
171
- cursor.next.set(parent, close?.nextSibling ?? null)
172
- /* Bracket the adopted nodes: `[` before the first claimed node (or before `close`
173
- for an empty branch), `]` then the anchor just before `close`. */
174
- const start = document.createComment(RANGE_OPEN)
175
- parent.insertBefore(start, firstAdopted ?? close)
176
- const end = document.createComment(RANGE_CLOSE)
177
- parent.insertBefore(end, close)
178
- anchor = document.createTextNode('')
179
- parent.insertBefore(anchor, close)
180
- active = { start, end, dispose }
158
+ branch.adoptStrand(build, () => {
159
+ /* A guaranteed control-flow marker — claimExpected throws on a desync (caught by
160
+ firstHydrate's adopt try/catch → rebuildCold) instead of silently claiming null
161
+ and over-clearing the parent. */
162
+ const close = claimExpected(cursor, parent, `/abide:await:${id} close marker`)
163
+ cursor.next.set(parent, close.nextSibling ?? null)
164
+ /* Bracket the adopted nodes: `[` before the first claimed node (or before `close`
165
+ for an empty branch), `]` then the anchor just before `close`. */
166
+ const start = document.createComment(RANGE_OPEN)
167
+ parent.insertBefore(start, firstAdopted ?? close)
168
+ const end = document.createComment(RANGE_CLOSE)
169
+ parent.insertBefore(end, close)
170
+ const anchorNode = document.createTextNode('')
171
+ parent.insertBefore(anchorNode, close)
172
+ branch.anchor = anchorNode
173
+ return { start, end }
174
+ })
181
175
  }
182
176
 
183
177
  /* Discard the SSR boundary and (re)build the block from the live promise, fresh
184
178
  (hydration off) — the recovery path when adoption can't use the server markup. */
185
179
  const rebuildCold = (open: Node | null): void => {
186
- detach()
180
+ branch.detach()
187
181
  /* Insert at the node AFTER the discarded boundary (its return) — NOT the captured
188
182
  `before`, which for a skeleton-anchored block is the open boundary itself and is
189
183
  removed here, so reusing it throws `NotFoundError` in a strict DOM. */
@@ -193,15 +187,10 @@ export function awaitBlock(
193
187
  `/abide:await:${id}`,
194
188
  hydration as NonNullable<typeof hydration>,
195
189
  )
196
- anchor = document.createTextNode('')
197
- parent.insertBefore(anchor, after)
198
- const previous = RENDER.hydration
199
- RENDER.hydration = undefined
200
- try {
201
- render(promiseThunk())
202
- } finally {
203
- RENDER.hydration = previous
204
- }
190
+ const anchorNode = document.createTextNode('')
191
+ branch.anchor = anchorNode
192
+ parent.insertBefore(anchorNode, after)
193
+ withoutHydration(() => render(promiseThunk()))
205
194
  }
206
195
 
207
196
  /* The first run when hydrating: adopt by precedence (resume / warm-sync), else
@@ -211,7 +200,9 @@ export function awaitBlock(
211
200
  warm cache (or re-fetches) instead of crashing hydration. */
212
201
  const firstHydrate = (): void => {
213
202
  const cursor = hydration as NonNullable<typeof hydration>
214
- const open = claimChild(cursor, parent)
203
+ /* The await block's open marker is compiler-guaranteed — claimExpected throws a
204
+ legible desync here rather than propagating a null that over-clears the parent. */
205
+ const open = claimExpected(cursor, parent, `abide:await:${id} open marker`)
215
206
  /* RESUME holds the ref-json-encoded entry STRING; decode here, where the codec
216
207
  lives. A decode failure (malformed/absent payload) reads as "no resume" — fall
217
208
  through to the live promise rather than crash hydration. */
@@ -280,18 +271,13 @@ export function awaitBlock(
280
271
  }
281
272
  /* Insert at the node after the discarded boundary (see `rebuildCold`). */
282
273
  const after = discardBoundary(parent, open, `/abide:await:${id}`, cursor)
283
- anchor = document.createTextNode('')
284
- parent.insertBefore(anchor, after)
274
+ const anchorNode = document.createTextNode('')
275
+ branch.anchor = anchorNode
276
+ parent.insertBefore(anchorNode, after)
285
277
  /* The boundary's server nodes are gone, so the pending branch builds FRESH — clear
286
- the claim cursor (mirrors `rebuildCold`) so its `cloneStatic`/text don't try to
278
+ the claim cursor (see withoutHydration) so its `cloneStatic`/text don't try to
287
279
  claim discarded nodes and silently render nothing. */
288
- const previous = RENDER.hydration
289
- RENDER.hydration = undefined
290
- try {
291
- render(result)
292
- } finally {
293
- RENDER.hydration = previous
294
- }
280
+ withoutHydration(() => render(result))
295
281
  }
296
282
 
297
283
  effect(() => {
@@ -304,8 +290,9 @@ export function awaitBlock(
304
290
  firstHydrate()
305
291
  return
306
292
  }
307
- anchor = document.createTextNode('')
308
- parent.insertBefore(anchor, before)
293
+ const anchorNode = document.createTextNode('')
294
+ branch.anchor = anchorNode
295
+ parent.insertBefore(anchorNode, before)
309
296
  }
310
297
  /* Read the promise every subsequent run so an invalidate re-runs the block. ONLY this
311
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
+ }
@@ -10,14 +10,32 @@ export function discardBoundary(
10
10
  closeData: string,
11
11
  hydration: NonNullable<(typeof RENDER)['hydration']>,
12
12
  ): Node | null {
13
- let node = open
14
- let after: Node | null = null
13
+ /* Nothing to discard (no open marker) — park the cursor and return, as before. */
14
+ if (open === null) {
15
+ hydration.next.set(parent, null)
16
+ return null
17
+ }
18
+ /* Locate the close marker WITHOUT mutating first. Removing as we walk would, on a
19
+ marker/id desync (the await/try block-id counter drifting between server and
20
+ client), delete every remaining sibling before discovering there is no match —
21
+ silently wiping unrelated later content. Throw AT the divergence like `outlet`
22
+ instead, rather than over-clear to end-of-parent. */
23
+ let close: Node | null = open
24
+ while (close !== null && (close as { data?: string }).data !== closeData) {
25
+ close = close.nextSibling
26
+ }
27
+ if (close === null) {
28
+ throw new Error(
29
+ `[abide] hydration desync: boundary open marker has no matching close "${closeData}" — the server DOM is truncated or the block id drifted.`,
30
+ )
31
+ }
32
+ const after = close.nextSibling
33
+ /* Remove open..close inclusive now that the range is known-bounded. */
34
+ let node: Node | null = open
15
35
  while (node !== null) {
16
- const next = node.nextSibling
17
- const isClose = (node as { data?: string }).data === closeData
36
+ const next: Node | null = node.nextSibling
18
37
  parent.removeChild(node)
19
- if (isClose) {
20
- after = next
38
+ if (node === close) {
21
39
  break
22
40
  }
23
41
  node = next
@@ -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,10 +1,13 @@
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'
5
6
  import { scope } from '../runtime/scope.ts'
6
7
  import { scopeGroup } from '../runtime/scopeGroup.ts'
7
8
  import type { State } from '../runtime/types/State.ts'
9
+ import { withoutHydration } from '../runtime/withoutHydration.ts'
10
+ import { withPathFrom } from '../runtime/withPathFrom.ts'
8
11
  import { state } from '../state.ts'
9
12
  import { buildDetachedRange } from './buildDetachedRange.ts'
10
13
  import { moveRange } from './moveRange.ts'
@@ -40,8 +43,17 @@ export function each<T>(
40
43
  read them; a direct caller reads `item.value` / `index.value`. */
41
44
  render: (parent: Node, item: State<T>, index: State<number>) => void,
42
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,
43
51
  ): void {
44
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
45
57
  /* Monotonic reconcile-pass id. Each reconcile stamps every surviving row with the
46
58
  current pass, so prune is a `row.gen !== pass` int compare instead of building a
47
59
  `Set` of present keys every reconcile. */
@@ -65,11 +77,19 @@ export function each<T>(
65
77
  reconcile below) instead of rebuilding it. */
66
78
  const cell = state(item) as State<unknown>
67
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
68
82
  const hydration = RENDER.hydration
69
83
  if (hydration !== undefined) {
70
84
  const start = claimExpected(hydration, parent, 'each row start marker')
71
85
  hydration.next.set(parent, start.nextSibling)
72
- 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
+ )
73
93
  const end = claimExpected(hydration, parent, 'each row end marker')
74
94
  hydration.next.set(parent, end.nextSibling)
75
95
  return { start, end, dispose, cell, indexCell }
@@ -78,7 +98,7 @@ export function each<T>(
78
98
  foreign namespace (so svg/math row children are namespaced, not built as HTML),
79
99
  held in `pending` until placement inserts it. */
80
100
  const { start, end, fragment, dispose } = buildDetachedRange(parent, (host) =>
81
- render(host, cell as State<T>, indexCell),
101
+ withPathFrom(basePath, segment, () => render(host, cell as State<T>, indexCell)),
82
102
  )
83
103
  return { start, end, dispose: group.track(dispose), cell, indexCell, pending: fragment }
84
104
  }
@@ -106,7 +126,9 @@ export function each<T>(
106
126
  const hydration = RENDER.hydration
107
127
  if (hydration !== undefined) {
108
128
  let position = 0
109
- 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() ?? []) {
110
132
  const key = keyOf(item)
111
133
  const row = buildRow(item, position) // claims the SSR row where it sits
112
134
  if (rows.has(key)) {
@@ -144,12 +166,11 @@ export function each<T>(
144
166
  write that reconciles *mid-hydrate* (RENDER.hydration still active — e.g. a
145
167
  page setting shared state during the hydrate pass) would otherwise make
146
168
  buildRow and its inner row render claim SSR nodes that don't exist for a
147
- freshly keyed row. The same `next` Map is restored, so the outer hydration
148
- cursor is untouched (mirrors awaitBlock/tryBlock). */
149
- const previousHydration = RENDER.hydration
150
- RENDER.hydration = undefined
151
- try {
152
- const list = Array.isArray(source) ? source : [...source]
169
+ freshly keyed row. withoutHydration restores the outer cursor after, so the
170
+ enclosing hydrate pass is untouched (mirrors awaitBlock/tryBlock). */
171
+ withoutHydration(() => {
172
+ /* Undefined source → empty list (see the hydration loop, ADR-0032 D3). */
173
+ const list = source == null ? [] : Array.isArray(source) ? source : [...source]
153
174
  const keys = list.map(keyOf)
154
175
  generation += 1
155
176
  const pass = generation
@@ -183,6 +204,16 @@ export function each<T>(
183
204
  for (let index = list.length - 1; index >= 0; index -= 1) {
184
205
  let row = resolved[index]
185
206
  if (row === undefined) {
207
+ /* Duplicate key within this list: a row for this key was already built
208
+ earlier in this pass. Building another and rows.set-ing it would
209
+ overwrite the map entry for the first, orphaning it — the prune loop
210
+ only reaches rows still in the map, so it would leak on screen forever.
211
+ Collapse to one row (like the hydration path above), skipping this
212
+ index rather than inserting an unreachable duplicate. */
213
+ const built = rows.get(keys[index] as string)
214
+ if (built !== undefined && built.gen === pass) {
215
+ continue
216
+ }
186
217
  row = buildRow(list[index] as T, index)
187
218
  row.gen = pass
188
219
  rows.set(keys[index] as string, row)
@@ -197,8 +228,6 @@ export function each<T>(
197
228
  placeBefore(row, cursor)
198
229
  cursor = row.start
199
230
  }
200
- } finally {
201
- RENDER.hydration = previousHydration
202
- }
231
+ })
203
232
  })
204
233
  }
@@ -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. */
@@ -1,5 +1,5 @@
1
- import { RENDER } from '../runtime/RENDER.ts'
2
1
  import { scope } from '../runtime/scope.ts'
2
+ import { withoutHydration } from '../runtime/withoutHydration.ts'
3
3
  import { enterNamespace } from './enterNamespace.ts'
4
4
 
5
5
  /*
@@ -29,15 +29,14 @@ export function fillBefore(end: Node, content: (into: Node) => void): () => void
29
29
  return () => {}
30
30
  }
31
31
  const fragment = document.createDocumentFragment()
32
- const previousHydration = RENDER.hydration
33
- RENDER.hydration = undefined
34
- try {
32
+ /* `fillBefore` is exclusively the CREATE path, so build with the claim cursor
33
+ cleared (see withoutHydration): a rebuild that runs mid-hydrate must not claim
34
+ SSR nodes that don't exist for this fresh content. */
35
+ return withoutHydration(() => {
35
36
  /* Build under the insertion parent's foreign namespace (if any), so foreign
36
37
  elements built into the fragment are namespaced off `end`'s live parent. */
37
38
  const dispose = enterNamespace(end.parentNode ?? end, () => scope(() => content(fragment)))
38
39
  ;(end.parentNode ?? end).insertBefore(fragment, end)
39
40
  return dispose
40
- } finally {
41
- RENDER.hydration = previousHydration
42
- }
41
+ })
43
42
  }
@@ -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
  }