@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
@@ -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
+ }
@@ -3,7 +3,7 @@ import type { UiProps } from '../runtime/types/UiProps.ts'
3
3
  /*
4
4
  Composes a child's props from ordered layers — explicit prop runs (`{ name: () =>
5
5
  value }` thunk maps), `{...spread}` layers (`spreadProps`), and a trailing slot
6
- layer (`$children`) — into one prop bag, last layer wins per key (source order, like
6
+ layer (`children`) — into one prop bag, last layer wins per key (source order, like
7
7
  JSX). A key resolves by scanning layers in reverse for the first that carries it, so
8
8
  an explicit prop after a spread overrides it and vice-versa. Emitted only when a
9
9
  component carries a spread; the plain object literal stays the path otherwise.
@@ -1,5 +1,4 @@
1
1
  import { scope } from '../runtime/scope.ts'
2
- import { scopeLabel } from './scopeLabel.ts'
3
2
  import { withScope } from './withScope.ts'
4
3
 
5
4
  /*
@@ -23,7 +22,7 @@ export function mount(
23
22
  /* Establish this component's lexical scope (nested, `awaiting` so it adopts the model
24
23
  doc the build's first `doc()` creates) and render pass, run the build under it, and
25
24
  restore the previous scope — the shared mount core (see `withScope`). */
26
- const { lexical } = withScope(scopeLabel(host), () => scope(() => build(host, props)))
25
+ const { lexical } = withScope(() => scope(() => build(host, props)))
27
26
  return () => {
28
27
  lexical.dispose()
29
28
  host.textContent = ''
@@ -1,25 +1,12 @@
1
- import { captureModelDoc } from '../runtime/captureModelDoc.ts'
2
- import { hotReloadEnabled } from '../runtime/hotReloadEnabled.ts'
3
1
  import { OWNER } from '../runtime/OWNER.ts'
4
- import { registerHotInstance } from '../runtime/registerHotInstance.ts'
5
2
  import type { UiComponent } from '../runtime/types/UiComponent.ts'
3
+ import { withOptionalPath } from '../runtime/withOptionalPath.ts'
6
4
  import { mountRange } from './mountRange.ts'
7
5
 
8
- /* Build-time flag the production client defines false (see build.ts `define`) so the hot
9
- path below — and its captureModelDoc/registerHotInstance imports — dead-code-eliminate
10
- (`!false` folds to a constant the minifier can prove) instead of shipping behind a runtime
11
- flag it can't. Dev defines it true; the test preload sets it on globalThis so the bare
12
- reference resolves to true there, leaving `hotReloadEnabled` alone in charge as before. */
13
- declare const __ABIDE_DEV__: boolean
14
-
15
6
  /*
16
7
  Mounts a child component as a marker-bounded range at `before` in `parent` — no
17
8
  wrapper element, so the child's root is a true direct child of the parent (see
18
- `mountRange`). Plain path (production, and dev without the hot bridge): just run the
19
- range mount with the component's own `build`. Hot path (hotReloadEnabled and the
20
- factory carries a module id): keep the mount handle (its range markers + disposer)
21
- and record the instance so an edit can re-fill the same range in place (see
22
- `hotReplace`), and file a cleanup with the mounting owner so the record and its scope
9
+ `mountRange`). Files the range's dispose with the mounting owner so its scope and DOM
23
10
  leave together when the parent (or branch/row) tears down.
24
11
  */
25
12
  // @documentation plumbing
@@ -28,30 +15,12 @@ export function mountChild(
28
15
  factory: UiComponent,
29
16
  props: Parameters<UiComponent>[1],
30
17
  before: Node | null = null,
31
- label?: string,
18
+ /* The compiler's source-order ordinal for this `<Child/>` mount site — pushed onto the render
19
+ path so the child's scope (and its cells) get a serialization-stable id under this parent
20
+ (two same-type siblings differ by ordinal; the same site across `{#each}` rows differs by the
21
+ row key the each block already pushed). Absent (a non-compiled caller) → no path segment. */
22
+ ordinal?: number,
32
23
  ): void {
33
- const moduleId = factory.__abideId
34
- if (!__ABIDE_DEV__ || !hotReloadEnabled.current || moduleId === undefined) {
35
- mountRange(parent, factory.build, props, before, label)
36
- return
37
- }
38
- /* Capture the component's model alongside its mount handle, so a later swap can
39
- carry its state across (see `hotReplace`). */
40
- const { value: handle, model } = captureModelDoc(() =>
41
- mountRange(parent, factory.build, props, before, label),
42
- )
43
- const instance = {
44
- factory,
45
- props,
46
- label,
47
- start: handle.start,
48
- end: handle.end,
49
- dispose: handle.dispose,
50
- model,
51
- }
52
- const remove = registerHotInstance(moduleId, instance)
53
- OWNER.current?.push(() => {
54
- instance.dispose()
55
- remove()
56
- })
24
+ const handle = withOptionalPath(ordinal, () => mountRange(parent, factory.build, props, before))
25
+ OWNER.current?.push(handle.dispose)
57
26
  }
@@ -29,18 +29,17 @@ export function mountRange(
29
29
  build: (host: Node, props?: UiProps) => void,
30
30
  props: UiProps | undefined,
31
31
  before: Node | null = null,
32
- label: string | undefined = undefined,
33
32
  ): { start: Comment; end: Comment; dispose: () => void } {
34
33
  const hydration = RENDER.hydration
35
34
  const start = openMarker(parent, RANGE_OPEN, before)
36
35
  if (hydration === undefined) {
37
36
  const end = openMarker(parent, RANGE_CLOSE, before)
38
- return fillRange(start, end, build, props, label)
37
+ return fillRange(start, end, build, props)
39
38
  }
40
39
  /* Hydrate: adopt the server range in place. Establish the child's lexical scope
41
40
  and render pass (same as `fillRange`), build claiming the existing nodes, then
42
41
  claim the end marker the build's content stops before. */
43
- const scoped = withScope(label, () => scope(() => build(parent, props)))
42
+ const scoped = withScope(() => scope(() => build(parent, props)))
44
43
  const end = openMarker(parent, RANGE_CLOSE)
45
44
  return { start, end, dispose: disposeRange(scoped, start, end) }
46
45
  }
@@ -8,7 +8,7 @@ import { openMarker } from './openMarker.ts'
8
8
  /*
9
9
  Mounts a component's `<slot>` content as a marker-bounded range, so a slot positions among
10
10
  static siblings exactly like a control-flow block — by `before` (create) or the claim
11
- cursor (hydrate). `render` appends the parent-supplied `$children`, or the slot's own
11
+ cursor (hydrate). `render` appends the parent-supplied `children`, or the slot's own
12
12
  fallback when none was passed; it runs once (a slot never toggles), so there is no effect or
13
13
  re-render — the markers exist only to delimit the range for create insertion and hydrate
14
14
  claiming.
@@ -0,0 +1,84 @@
1
+ import { CURRENT_PATH } from '../runtime/CURRENT_PATH.ts'
2
+ import { claimChild } from '../runtime/claimChild.ts'
3
+ import { OWNER } from '../runtime/OWNER.ts'
4
+ import { RANGE_OPEN } from '../runtime/RANGE_MARKER.ts'
5
+ import { RENDER } from '../runtime/RENDER.ts'
6
+ import type { UiComponent } from '../runtime/types/UiComponent.ts'
7
+ import { withOptionalPath } from '../runtime/withOptionalPath.ts'
8
+ import { withoutHydration } from '../runtime/withoutHydration.ts'
9
+ import { discardBoundary } from './discardBoundary.ts'
10
+ import { mountRange } from './mountRange.ts'
11
+ import { openMarker } from './openMarker.ts'
12
+
13
+ /*
14
+ The client mount for a HOISTABLE child (ADR-0039) — a DUAL-MODE adopter. The server decides per
15
+ render whether a hoistable child inlined (fast/settled) or streamed (still-pending), and the client
16
+ build can't know which, so this probes the hydration cursor:
17
+
18
+ - `<!--[-->` (RANGE_OPEN) next → the server INLINED the child; adopt its range exactly as
19
+ `mountChild` does (byte-identical to the pre-ADR-0039 path).
20
+ - `<!--abide:await:CHILDPATH-->` next → the server STREAMED it; `__abideSwap` has already spliced
21
+ the child's `<!--[-->…<!--]-->` between the boundary markers before the bundle ran, so claim the
22
+ boundary open, adopt the inner range in place, claim the boundary close.
23
+ - no hydration (a client-side navigation) → a plain create-mode mount, like `mountChild`.
24
+
25
+ CHILDPATH is the child's render-path — `withPath(ordinal, () => CURRENT_PATH.current)` — computed the
26
+ same way the server's `renderPath(ordinal)` wrote the boundary id, so the two agree with no counter.
27
+ A streamed child re-mounts (no RESUME value of its own — the boundary is html-only); nested `{#await}`
28
+ blocks and async cells inside it adopt through their own existing channels.
29
+ */
30
+ // @documentation plumbing
31
+ export function mountStreamedChild(
32
+ parent: Node,
33
+ factory: UiComponent,
34
+ props: Parameters<UiComponent>[1],
35
+ before: Node | null = null,
36
+ ordinal?: number,
37
+ ): void {
38
+ const run = <T>(build: () => T): T => withOptionalPath(ordinal, build)
39
+ const mount = (): { dispose: () => void } =>
40
+ run(() => mountRange(parent, factory.build, props, before))
41
+
42
+ const hydration = RENDER.hydration
43
+ if (hydration === undefined) {
44
+ /* Call mount() first, THEN register its dispose — `OWNER.current?.push(mount())` would
45
+ short-circuit the whole expression (never mounting) when there is no owner. */
46
+ const handle = mount()
47
+ OWNER.current?.push(handle.dispose)
48
+ return
49
+ }
50
+
51
+ /* Probe the cursor without consuming it. Streamed iff the next node is this child's boundary. */
52
+ const childPath = run(() => CURRENT_PATH.current)
53
+ const cursor = claimChild(hydration, parent)
54
+ const streamed =
55
+ cursor !== null &&
56
+ cursor.nodeType === 8 &&
57
+ (cursor as Comment).data === `abide:await:${childPath}`
58
+
59
+ if (!streamed) {
60
+ /* INLINE — the server inlined a settled child; adopt its `[ … ]` range in place. */
61
+ const handle = mount()
62
+ OWNER.current?.push(handle.dispose)
63
+ return
64
+ }
65
+
66
+ /* STREAMED — claim the boundary open (advances the cursor to the swapped-in `[`). */
67
+ const open = openMarker(parent, `abide:await:${childPath}`)
68
+ const inner = claimChild(hydration, parent)
69
+ const warm = inner !== null && inner.nodeType === 8 && (inner as Comment).data === RANGE_OPEN
70
+ if (warm) {
71
+ const handle = mount()
72
+ openMarker(parent, `/abide:await:${childPath}`)
73
+ OWNER.current?.push(handle.dispose)
74
+ return
75
+ }
76
+ /* COLD (defensive — renderToStream drains every await before closing, so a warm boundary is the
77
+ deterministic path). The fragment never arrived: discard the empty boundary and create-mount
78
+ the child fresh at that position; its streamed cells resolve post-mount via receiveStreamedCell. */
79
+ const after = discardBoundary(parent, open, `/abide:await:${childPath}`, hydration)
80
+ const handle = withoutHydration(() =>
81
+ run(() => mountRange(parent, factory.build, props, after)),
82
+ )
83
+ OWNER.current?.push(handle.dispose)
84
+ }
@@ -1,9 +1,13 @@
1
1
  import { effect } from '../effect.ts'
2
+ import { CURRENT_PATH } from '../runtime/CURRENT_PATH.ts'
2
3
  import { RANGE_CLOSE, RANGE_OPEN } from '../runtime/RANGE_MARKER.ts'
3
4
  import { RENDER } from '../runtime/RENDER.ts'
4
5
  import { scope } from '../runtime/scope.ts'
5
6
  import { scopeGroup } from '../runtime/scopeGroup.ts'
7
+ import { withPathFrom } from '../runtime/withPathFrom.ts'
8
+ import { clearBetween } from './clearBetween.ts'
6
9
  import { fillBefore } from './fillBefore.ts'
10
+ import { matchingRangeClose } from './matchingRangeClose.ts'
7
11
  import { openMarker } from './openMarker.ts'
8
12
  import { replaceRange } from './replaceRange.ts'
9
13
 
@@ -35,6 +39,15 @@ export function mountSwappableRange<Key>(
35
39
  key: () => Key,
36
40
  contentFor: (key: Key) => ((parent: Node) => void) | undefined,
37
41
  before: Node | null = null,
42
+ /* The chosen branch MAY differ between SSR and the client's first render — set only for an
43
+ ASYNC subject (a bare async `{#if}`/`{#switch}`, or an async cond-chain branch), whose cell
44
+ can be settled on the server (a barrier-baked value) yet pending on the client, so each side
45
+ selects a different branch. Adopting SSR nodes in place then orphans them (they are neither
46
+ claimed nor cleared) and the next swap duplicates. When set, hydration DISCARDS the SSR range
47
+ content and builds the client's branch fresh instead — correct regardless of what the server
48
+ rendered. Left false for a sync range, where SSR and client always agree and in-place
49
+ adoption (node reuse, preserved focus/scroll) is safe. */
50
+ mayDiverge = false,
38
51
  ): void {
39
52
  const hydration = RENDER.hydration
40
53
  /* The live branch's scope, registered with the owner so it disposes on owner
@@ -44,10 +57,39 @@ export function mountSwappableRange<Key>(
44
57
  let activeKey: Key
45
58
  let end: Comment
46
59
 
60
+ /* Capture the render path at construction (this block's ancestry), and wrap each chosen
61
+ branch's builder so its content builds under `basePath/branchKey` — the key is the swap
62
+ discriminator (`'then'`/`'else'` or a case index), so a component/cell in one branch gets a
63
+ distinct, stable id from the other, re-established on every reactive swap (when the ambient
64
+ path is gone). An empty branch (`undefined`) needs no wrap. */
65
+ const basePath = CURRENT_PATH.current
66
+ const chosenFor = (branchKey: Key): ((parent: Node) => void) | undefined => {
67
+ const content = contentFor(branchKey)
68
+ if (content === undefined) {
69
+ return undefined
70
+ }
71
+ return (host) => withPathFrom(basePath, String(branchKey), () => content(host))
72
+ }
73
+
47
74
  const start = openMarker(parent, RANGE_OPEN, before)
48
- if (hydration !== undefined) {
75
+ if (hydration !== undefined && mayDiverge) {
76
+ /* Async range: don't trust the SSR content to match the client's branch. Discard the
77
+ server's rendered range (balanced `[`…`]`), park the cursor on the real close so it
78
+ claims correctly, then build the client's chosen branch FRESH (`fillBefore` clears the
79
+ claim cursor for the build). Usually the client is pending here → nothing built → an
80
+ empty range the later reactive swap fills, matching a clean create. */
81
+ activeKey = key()
82
+ const chosen = chosenFor(activeKey)
83
+ const matchedEnd = matchingRangeClose(start)
84
+ clearBetween(start, matchedEnd)
85
+ hydration.next.set(parent, matchedEnd)
86
+ end = openMarker(parent, RANGE_CLOSE)
87
+ if (chosen !== undefined) {
88
+ dispose = group.track(fillBefore(end, chosen))
89
+ }
90
+ } else if (hydration !== undefined) {
49
91
  activeKey = key()
50
- const chosen = contentFor(activeKey)
92
+ const chosen = chosenFor(activeKey)
51
93
  if (chosen !== undefined) {
52
94
  dispose = group.track(scope(() => chosen(parent))) // content claims the SSR nodes in place
53
95
  }
@@ -55,7 +97,7 @@ export function mountSwappableRange<Key>(
55
97
  } else {
56
98
  end = openMarker(parent, RANGE_CLOSE, before)
57
99
  activeKey = key()
58
- const chosen = contentFor(activeKey)
100
+ const chosen = chosenFor(activeKey)
59
101
  if (chosen !== undefined) {
60
102
  dispose = group.track(fillBefore(end, chosen))
61
103
  }
@@ -67,7 +109,7 @@ export function mountSwappableRange<Key>(
67
109
  return
68
110
  }
69
111
  activeKey = next
70
- const chosen = contentFor(next)
112
+ const chosen = chosenFor(next)
71
113
  /* Null `dispose` before `replaceRange` builds the new branch: a reentrant swap
72
114
  during that build (an effect in the new content writing the source) would
73
115
  otherwise re-enter with the already-disposed disposer and clear it twice. */
@@ -0,0 +1,65 @@
1
+ import type { Doc } from '../runtime/types/Doc.ts'
2
+ import { readCall } from './readCall.ts'
3
+
4
+ /*
5
+ An in-place-mutating container method lowered on a reactive-document value. The
6
+ doc-access lowering rewrites e.g. `model.items.splice(i, 1)` to
7
+ `$$mutateDocContainer(model, "items", "splice", [i, 1])` rather than the bare
8
+ `$$readCall(model.read("items"), …)` it emits for non-mutating calls. The reason:
9
+ `read` returns the live tree value by reference, so a bare `.splice()`/`.sort()`/
10
+ `.add()`/`.set()`/… would mutate the document in place while never emitting a
11
+ patch — so no reader would re-render (`.push` is the one already-handled exception,
12
+ lowered to `add` patches). Cloning the container, applying the mutation to the copy,
13
+ and writing it back through `replace` emits a real patch: readers wake.
14
+
15
+ Covers the three mutable containers the doc codec serializes (encodeRefJson):
16
+ Array (cloned by `slice`), Map (`new Map`) and Set (`new Set`) — the mutating
17
+ method names are disjoint across the three (`pop`/`splice`/… vs `add`/`set`/…), so
18
+ the caller routes them all here and the actual kind is decided at runtime by the
19
+ value's type, no compile-time check needed. Element identity is preserved for
20
+ untouched entries (a shallow copy), so a keyed `{#for}` reconciles instead of
21
+ rebuilding. The native method's return value (spliced-out elements, the new
22
+ length, the deleted flag, …) is returned unchanged.
23
+ */
24
+ // @documentation plumbing
25
+ export function mutateDocContainer(
26
+ doc: Doc,
27
+ path: string,
28
+ member: string,
29
+ args: unknown[],
30
+ ): unknown {
31
+ const current = doc.read<unknown>(path)
32
+ const copy = cloneContainer(current)
33
+ /* Not a container we clone (array/Map/Set) — fall back to the guarded in-place call so
34
+ the author still gets readCall's authored-scope error message (a plain doc object has
35
+ no methods, so this is the same throw the bare call would raise). */
36
+ if (copy === undefined) {
37
+ return readCall(current, path, member, args)
38
+ }
39
+ const method = (copy as unknown as Record<string, unknown>)[member]
40
+ if (typeof method !== 'function') {
41
+ throw new TypeError(
42
+ `abide: cannot call .${member}() — "${path}".${member} is not a function (got ${typeof method})`,
43
+ )
44
+ }
45
+ const returned = (method as (...callArgs: unknown[]) => unknown).apply(copy, args)
46
+ doc.replace(path, copy)
47
+ return returned
48
+ }
49
+
50
+ /* A shallow clone of a mutable doc container, or undefined when the value is not one — the
51
+ shallow copy preserves element/entry identity so keyed reconciliation survives the patch. */
52
+ function cloneContainer(
53
+ current: unknown,
54
+ ): unknown[] | Map<unknown, unknown> | Set<unknown> | undefined {
55
+ if (Array.isArray(current)) {
56
+ return current.slice()
57
+ }
58
+ if (current instanceof Map) {
59
+ return new Map(current)
60
+ }
61
+ if (current instanceof Set) {
62
+ return new Set(current)
63
+ }
64
+ return undefined
65
+ }
@@ -8,8 +8,8 @@ Attaches an event listener and registers its removal with the current ownership
8
8
  scope, so a component's listeners detach when it disposes. This is the runtime
9
9
  target for an `onclick={…}` binding; the handler body is where the compiler's
10
10
  lowered patches (`model.replace(...)`) run. The handler is pinned to the scope it
11
- was attached under, so an ambient `scope()` inside it (e.g. `scope().undo()`)
12
- resolves the component, not whatever is current when the event fires.
11
+ was attached under, so an ambient `scope()` inside it resolves the component, not
12
+ whatever is current when the event fires.
13
13
  */
14
14
  // @documentation plumbing
15
15
  export function on(element: Element, type: string, handler: EventListener): void {
@@ -0,0 +1,40 @@
1
+ import { isAsyncCell } from '../../shared/isAsyncCell.ts'
2
+ import type { AsyncComputed } from '../../shared/types/AsyncComputed.ts'
3
+ import { AsyncCellError } from '../runtime/AsyncCellError.ts'
4
+
5
+ /*
6
+ Unified read for a `computed`/`linked` reference the compiler lowers to `$$readCell(NAME)`:
7
+ an async cell (`AsyncComputed`/`AsyncState`) yields its latest retained value, a `derive`
8
+ reader (a function) is called, and a sync `Computed`/`State` yields its `.value`. One read
9
+ shape lets a `linked`/`computed` binding auto-track whichever source it resolved to — a
10
+ settling promise, a stream, or a plain value — with no read-site branching in codegen.
11
+
12
+ For an async cell this is a **throwing peek** (ADR-0019 D3.2), the value-aware rule that
13
+ routes a "nothing to render" error to the nearest `{#try}`:
14
+ - error AND no retained value → throw `AsyncCellError` (carries the cell for keep-the-watch);
15
+ - error WITH a retained value → the value (a failed background refresh stays visible — SWR);
16
+ - pending (no value) → `undefined`;
17
+ - resolved → the value.
18
+ The throw is a codegen behaviour only — `cell.peek()`/`cell.error()` in JS never throw; the
19
+ author handles an error locally via the probes, or lets it reach a `{#try}`.
20
+ */
21
+ // @documentation plumbing
22
+ export function readCell(cell: unknown): unknown {
23
+ if (isAsyncCell(cell)) {
24
+ const asyncCell = cell as AsyncComputed<unknown>
25
+ const value = asyncCell.peek()
26
+ /* A retained value wins over an error (SWR) and over pending — return it. Only when
27
+ nothing is retained does an error become "nothing to render" and throw. */
28
+ if (value === undefined) {
29
+ const error = asyncCell.error()
30
+ if (error !== undefined) {
31
+ throw new AsyncCellError(asyncCell, error)
32
+ }
33
+ }
34
+ return value
35
+ }
36
+ if (typeof cell === 'function') {
37
+ return (cell as () => unknown)()
38
+ }
39
+ return (cell as { value: unknown }).value
40
+ }
@@ -6,14 +6,14 @@ object of the UNCONSUMED prop values. A child receives prop THUNKS, so `rest.key
6
6
  unwraps them (`$props[key]?.()`), tracking as a reactive dependency; a top-level
7
7
  page/layout instead receives its route params as PLAIN values, so a non-function
8
8
  value is returned as-is rather than called (which would throw `value is not a
9
- function`). The explicitly-destructured keys and the `$children` slot are excluded.
9
+ function`). The explicitly-destructured keys and the `children` slot are excluded.
10
10
  Enumerable (`ownKeys`/`for…in`/`Object.keys`), so `{...rest}` can forward the
11
11
  remaining props onto a child or a native element. Key membership is live, but a
12
12
  consumer that captures the key SET (a `{...rest}` spread) snapshots it at that point.
13
13
  */
14
14
  // @documentation plumbing
15
15
  export function restProps(props: UiProps, consumed: string[]): Record<string, unknown> {
16
- const skip = new Set([...consumed, '$children'])
16
+ const skip = new Set([...consumed, 'children'])
17
17
  const bag = props as Record<string, unknown>
18
18
  const visible = (key: string | symbol): key is string =>
19
19
  typeof key === 'string' && !skip.has(key) && key in bag
@@ -5,7 +5,7 @@ reads a spread key exactly like an authored one (`$props[key]?.()`). `source` is
5
5
  THUNK over the spread expression (not its value), re-evaluated on each key read and
6
6
  membership test, so a reactive source stays live both when its keys mutate and when
7
7
  the whole object is replaced — and the read registers as the reader's dependency. A
8
- nullish source spreads nothing. The reserved `$children` slot key is never surfaced —
8
+ nullish source spreads nothing. The reserved `children` slot key is never surfaced —
9
9
  a source happening to carry one must not masquerade as slot content.
10
10
  */
11
11
  // @documentation plumbing
@@ -13,9 +13,8 @@ export function spreadProps(
13
13
  source: () => Record<string, unknown> | null | undefined,
14
14
  ): Record<string, () => unknown> {
15
15
  /* A spread key the merged bag exposes — present on the current source and not the
16
- reserved `$children` slot key (a source carrying one must not become slot content). */
17
- const carries = (key: string | symbol): boolean =>
18
- key !== '$children' && key in (source() ?? {})
16
+ reserved `children` slot key (a source carrying one must not become slot content). */
17
+ const carries = (key: string | symbol): boolean => key !== 'children' && key in (source() ?? {})
19
18
  return new Proxy(
20
19
  {},
21
20
  {
@@ -18,22 +18,45 @@ export function switchBlock(
18
18
  subject: () => unknown,
19
19
  cases: SwitchCase[],
20
20
  before: Node | null = null,
21
+ isPending?: () => boolean,
21
22
  ): void {
22
- /* Pick the first case matching the subject (`===`), else the default (`match`
23
- undefined), else -1 for no match. */
23
+ /* Walk the cases in source order (the same order a `{#if}`/`{:elseif}` chain reads): the
24
+ first whose own async condition is still `pending()` HOLDS the chain — returns -1, so no
25
+ later branch renders on a not-yet-known earlier condition; the first whose `match() ===`
26
+ the subject wins; a match-less `default` is the fallback once every case settled without a
27
+ match. A plain `{#switch}` (no per-case `pending`) reduces to first-match-else-default. */
24
28
  const select = (value: unknown): number => {
25
- const matched = cases.findIndex(
26
- (entry) => entry.match !== undefined && entry.match() === value,
27
- )
28
- return matched === -1 ? cases.findIndex((entry) => entry.match === undefined) : matched
29
+ let fallback = -1
30
+ for (let index = 0; index < cases.length; index++) {
31
+ const entry = cases[index]
32
+ if (entry === undefined) {
33
+ continue
34
+ }
35
+ if (entry.pending?.()) {
36
+ return -1
37
+ }
38
+ if (entry.match === undefined) {
39
+ fallback = index
40
+ } else if (entry.match() === value) {
41
+ return index
42
+ }
43
+ }
44
+ return fallback
29
45
  }
30
46
  mountSwappableRange(
31
47
  parent,
32
- () => select(subject()),
48
+ /* A pending bare async subject (compiler-supplied `isPending`) selects no case — not
49
+ even the default — so the block renders nothing until the cell settles, rather than
50
+ conflating "still loading" with a subject that matched the default. */
51
+ () => (isPending?.() === true ? -1 : select(subject())),
33
52
  (index) => {
34
53
  const chosen = index === -1 ? undefined : cases[index]
35
54
  return chosen && ((p) => chosen.render(p))
36
55
  },
37
56
  before,
57
+ /* Any async subject/branch (a bare async subject, or an async `{:else if}` in a cond-chain)
58
+ can select a different branch on the server than the pending client, so hydration must
59
+ discard the SSR range rather than adopt it in place. */
60
+ isPending !== undefined || cases.some((entry) => entry.pending !== undefined),
38
61
  )
39
62
  }