@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,4 +1,9 @@
1
+ import { isAsyncIterable } from '../shared/isAsyncIterable.ts'
2
+ import type { AsyncState } from '../shared/types/AsyncState.ts'
3
+ import type { NamedAsyncIterable } from '../shared/types/NamedAsyncIterable.ts'
4
+ import { createAsyncCell } from './runtime/createAsyncCell.ts'
1
5
  import { createEffectNode } from './runtime/createEffectNode.ts'
6
+ import { isAsyncFunction } from './runtime/isAsyncFunction.ts'
2
7
  import type { State } from './runtime/types/State.ts'
3
8
  import { state } from './state.ts'
4
9
 
@@ -16,8 +21,49 @@ entering the store — explicit `.value =` writes and reseeds alike — so the s
16
21
  never holds an un-coerced value (`return previous` rejects via the `Object.is`
17
22
  no-op). The seed is captured by reference: callers clone in the thunk
18
23
  (`linked(() => structuredClone(x))`) when they want isolation.
24
+
25
+ When the seed tracks an async source it becomes a writable async cell (`AsyncState<T>`,
26
+ ADR-0019 D1): an `async () => await …` thunk unwraps its promise, a `NamedAsyncIterable`
27
+ seed auto-tracks its frames. Same probe surface as `AsyncComputed` plus `set()`, which
28
+ latches until the next reseed — an arriving frame never clobbers an in-progress edit.
19
29
  */
20
- export function linked<T>(seed: () => T, transform?: (next: T, previous: T) => T): State<T> {
30
+ export function linked<T>(
31
+ seed: () => NamedAsyncIterable<T>,
32
+ transform?: (next: T, previous: T) => T,
33
+ ): AsyncState<T>
34
+ export function linked<T>(
35
+ seed: () => Promise<T>,
36
+ transform?: (next: T, previous: T) => T,
37
+ ): AsyncState<T>
38
+ export function linked<T>(seed: () => T, transform?: (next: T, previous: T) => T): State<T>
39
+ export function linked<T>(
40
+ seed: () => T | Promise<T> | NamedAsyncIterable<T>,
41
+ transform?: (next: T, previous: T) => T,
42
+ ): State<T> | AsyncState<T> {
43
+ const coerce = transform as ((next: unknown, previous: unknown) => unknown) | undefined
44
+ /* `await` marker: an async-function seed unwraps its promise into a writable async cell. */
45
+ if (isAsyncFunction(seed)) {
46
+ return createAsyncCell(seed as () => unknown, {
47
+ writable: true,
48
+ transform: coerce,
49
+ }) as AsyncState<T>
50
+ }
51
+ /* Peek the seed once to detect a self-identifying stream source (auto-track); `linked`
52
+ already runs its seed eagerly, so an early probe matches its semantics. A throw or a
53
+ non-stream value falls through to the plain synchronous cell. */
54
+ let probe: unknown
55
+ let threw = false
56
+ try {
57
+ probe = seed()
58
+ } catch {
59
+ threw = true
60
+ }
61
+ if (!threw && isAsyncIterable(probe)) {
62
+ return createAsyncCell(seed as () => unknown, {
63
+ writable: true,
64
+ transform: coerce,
65
+ }) as AsyncState<T>
66
+ }
21
67
  /* The cell is a plain `state` — same store, same write path, so `transform` gates
22
68
  reseeds and explicit writes identically. */
23
69
  const cell = state<T>(undefined as T, transform)
@@ -27,7 +73,7 @@ export function linked<T>(seed: () => T, transform?: (next: T, previous: T) => T
27
73
  what `seed` reads can retrigger it. `createEffectNode` registers the disposer
28
74
  with the enclosing scope. */
29
75
  createEffectNode(() => {
30
- cell.value = seed()
76
+ cell.value = seed() as T
31
77
  })
32
78
  return cell
33
79
  }
@@ -0,0 +1,17 @@
1
+ /*
2
+ The prop reader. In a `.abide` component the compiler rewrites `const { name } =
3
+ props()` to reads off the component's prop bag (`$props`), so this runtime body
4
+ never executes there — it exists for import resolution and for typing `props()`
5
+ in plain `.ts` modules. Called directly (only possible outside a compiled
6
+ component) it throws, like `request()` outside a request scope, rather than
7
+ silently returning undefined.
8
+
9
+ The return type is `T` (default `Record<string, unknown>`); inside a `.abide`
10
+ file the check shadow supplies the file-contextual `RouteShape & T` instead.
11
+ */
12
+ // @documentation reactive-state
13
+ export function props<T = Record<string, unknown>>(): T {
14
+ throw new Error(
15
+ '[abide] props() is compiler-lowered inside a .abide component and has no runtime meaning when called directly',
16
+ )
17
+ }
@@ -6,32 +6,49 @@ import { HttpError } from '../shared/HttpError.ts'
6
6
  import { OFFLINE_HEADER } from '../shared/OFFLINE_HEADER.ts'
7
7
  import { rpcTimeoutSlot } from '../shared/rpcTimeoutSlot.ts'
8
8
  import { trace } from '../shared/trace.ts'
9
+ import type { CachePolicy } from '../shared/types/CachePolicy.ts'
9
10
  import type { HttpMethod } from '../shared/types/HttpMethod.ts'
10
- import type { Outbox } from '../shared/types/Outbox.ts'
11
+ import type { OutputWirePlan } from '../shared/types/OutputWirePlan.ts'
11
12
  import type { RemoteFunction } from '../shared/types/RemoteFunction.ts'
12
13
  import type { RpcOptions } from '../shared/types/RpcOptions.ts'
13
- import { UNREACHABLE_STATUSES } from '../shared/UNREACHABLE_STATUSES.ts'
14
+ import type { StandardSchemaV1 } from '../shared/types/StandardSchemaV1.ts'
15
+ import type { StreamPolicy } from '../shared/types/StreamPolicy.ts'
16
+ import { validationHttpError } from '../shared/validationHttpError.ts'
14
17
  import { withBase } from '../shared/withBase.ts'
15
- import { createOutboxQueue, type OutboxQueue } from './rpcOutbox/createOutboxQueue.ts'
16
- import { outboxRegistry } from './rpcOutbox/outboxRegistry.ts'
17
18
  import { currentAbortSignal } from './runtime/currentAbortSignal.ts'
18
19
  import { REQUEST_SUPERSEDED } from './runtime/REQUEST_SUPERSEDED.ts'
19
- import type { PersistenceStore } from './types/PersistenceStore.ts'
20
20
  import { watch } from './watch.ts'
21
21
 
22
- /* The framework-reserved `HttpError.kind` for a request the durable outbox parked because
23
- the server was unreachable distinct from a handler-declared error name. Lets a caller
24
- branch with `error instanceof HttpError && error.kind === 'queued'`. */
25
- const QUEUED = 'queued'
26
-
27
- /* Build-time options the bundler stamps onto the client proxy stub. `outbox: true` parks an
28
- unreachable call for replay; `streaming: true` (handler returns jsonl()/sse()) makes the bare
29
- call return the Subscribable directly; `store` exists for testing (production uses the default
30
- persistence store). */
31
- export type DurableOptions = {
32
- outbox?: boolean
22
+ /* The proxy's third argument. ADR-0022 D2: the client rpc transform passes the endpoint's LIVE
23
+ `opts` object here verbatim, so the type widens to the endpoint opts shape — remoteProxy reads
24
+ only the keys below and IGNORES the rest (`crossOrigin` / `timeout` / `maxBodySize`), which ride
25
+ along harmlessly. `streaming: true` (handler returns jsonl()/sse(), build-injected) makes the
26
+ bare call return the NamedAsyncIterable directly; `cache` / `stream` carry the endpoint's declared
27
+ policy (ADR-0020) so the client honours the ttl (staleness/SWR), the refetch clock
28
+ (throttle/debounce), and tags. `schemas` — its live `input` validator now reaches the stub (ADR-0022
29
+ D2) drives the ADR-0026 client-side pre-flight. */
30
+ export type RemoteProxyOptions<Args = unknown> = {
33
31
  streaming?: boolean
34
- store?: PersistenceStore
32
+ /* The client output wire codec plan (ADR-0029) the resolver plugin baked onto the stub — the
33
+ handler's structured success fields, so the proxy revives a `Set`/`Map`/`bigint`/`Date` off a
34
+ decoded response. Absent on the fail-open path (a response array then stays an array). */
35
+ outputWirePlan?: OutputWirePlan
36
+ cache?: CachePolicy<Args>
37
+ stream?: StreamPolicy
38
+ /* The endpoint's live schema group (ADR-0022 D2 forwards it verbatim). `input` is the
39
+ Standard Schema the ADR-0026 pre-flight validates the typed args against. */
40
+ schemas?: {
41
+ input?: StandardSchemaV1
42
+ output?: StandardSchemaV1
43
+ files?: StandardSchemaV1
44
+ }
45
+ /* Ignored endpoint opts keys, present only so the author's live `opts` object type-checks
46
+ when the client transform forwards it verbatim (ADR-0022). `clients` (surface flags) is
47
+ read only on the server. */
48
+ clients?: unknown
49
+ crossOrigin?: unknown
50
+ timeout?: unknown
51
+ maxBodySize?: unknown
35
52
  }
36
53
 
37
54
  /*
@@ -46,40 +63,33 @@ side.
46
63
  the query string (GET/DELETE/HEAD). Plain `fn(args)` decodes the Response
47
64
  by Content-Type and throws HttpError on non-2xx; `.raw(args)` is the
48
65
  escape hatch that returns the Response untouched.
49
-
50
- A durable (`outbox: true`) rpc is still a normal RemoteFunction — it fetches and
51
- throws exactly the same. The differences: when the server can't be reached
52
- (a transport failure, or a 502/503/504/52x), the request is `park`ed onto the
53
- RPC's app-owned outbox as a SIDE-EFFECT and the throw is a `kind: 'queued'`
54
- HttpError; and once a backlog exists, a fresh call parks straight to the TAIL
55
- (no fetch) so writes can't land out of order. The parked write waits for
56
- `rpc.outbox.retry()` (or the global `outbox.retry()`) — there is no auto-drain;
57
- the app owns when to replay. `rpc.outbox()` exposes the queue.
58
66
  */
59
67
  // @documentation plumbing
60
68
  export function remoteProxy<Args, Return>(
61
69
  method: HttpMethod,
62
70
  url: string,
63
- durable: DurableOptions & { outbox: true },
64
- ): RemoteFunction<Args, Return, Record<never, never>, true>
65
- export function remoteProxy<Args, Return>(
66
- method: HttpMethod,
67
- url: string,
68
- durable?: DurableOptions,
69
- ): RemoteFunction<Args, Return>
70
- export function remoteProxy<Args, Return>(
71
- method: HttpMethod,
72
- url: string,
73
- durable?: DurableOptions,
74
- ): RemoteFunction<Args, Return, Record<never, never>, boolean> {
75
- /* Assigned after `createRemoteFunction` so the invoke closure (which runs later, per
76
- call) parks through the shared queue; undefined leaves the plain fetch path. */
77
- let queue: OutboxQueue<Args> | undefined
71
+ options?: RemoteProxyOptions<Args>,
72
+ ): RemoteFunction<Args, Return> {
73
+ /* ADR-0026 client-side pre-flight: validate the typed args against the endpoint's input
74
+ schema (forwarded to the stub, ADR-0022 D2) BEFORE the fetch whenever one is present — always
75
+ on, no opt-in. This is a UX optimization ONLY: the server's unconditional inputSchema validate
76
+ 422 (defineRpc.ts `validateThenHandle`) stays the trust boundary, so a client that skips or
77
+ fakes this check is still fully validated on the server. undefined here (no input schema)
78
+ keeps today's behaviour — the client serializes and sends unvalidated. */
79
+ const preflightSchema = options?.schemas?.input
78
80
  const fn = createRemoteFunction<Args, Return>({
79
81
  method,
80
82
  url,
81
83
  clients: browserClientFlags,
82
- streaming: durable?.streaming ?? false,
84
+ streaming: options?.streaming ?? false,
85
+ /* The client revives a decoded response's structured fields through this plan (ADR-0029
86
+ output path); undefined leaves the honest-JSON body untouched. */
87
+ outputWirePlan: options?.outputWirePlan,
88
+ /* Endpoint policy the resolver plugin spliced onto the stub — governs client cache
89
+ behaviour (ttl/staleness, refetch clock, tags). createRemoteFunction stamps it onto
90
+ `fn.cache` / `fn.stream` so readThrough reads it as the bottom policy layer. */
91
+ cache: options?.cache,
92
+ stream: options?.stream,
83
93
  /*
84
94
  The Request URL carries the mount base so the fetch routes through the
85
95
  proxy (/v2/rpc/…); the cache key keeps the bare `url` (keyForRemoteCall
@@ -96,56 +106,42 @@ export function remoteProxy<Args, Return>(
96
106
  /*
97
107
  Forcing `getRequest()` once builds the Request and seeds the cache meta thunk in
98
108
  createRemoteFunction with the same instance, so cache() readers don't reconstruct
99
- it. On a durable rpc, an unreachable result parks a pristine CLONE and throws a
100
- `queued`-tagged HttpError — `fetch` consumes the original (its body stream is read
101
- and locked), so parking that same instance would leave the queue a request a resend
102
- can't reconstruct and a capture can't read. The clone is parked, the original is
103
- sent. The throw lets the caller branch on `error.kind === 'queued'` (parked, will
104
- retry) vs. a real server rejection; `error.data` is the parked entry, so a caller
105
- can `await (error.data as OutboxEntry).settled` for the eventual outcome.
109
+ it.
106
110
  */
107
111
  invoke: (args, getRequest, opts) => {
108
- if (queue === undefined) {
109
- return fetchWithTimeout(getRequest(), opts)
110
- }
111
- /* A non-empty queue means an undelivered backlog: park this call at the TAIL
112
- and throw, rather than let a live fetch leapfrog the older writes and land
113
- out of order. `retry()` then flushes the whole queue FIFO. */
114
- if (queue.size() > 0) {
115
- return Promise.reject(
116
- queuedThrow(
117
- queue,
118
- args as Args,
119
- getRequest().clone(),
120
- unreachableResponse(),
121
- undefined,
122
- ),
112
+ /* ADR-0026 D2/D3: an input schema present → validate the TYPED args (pre-serialization
113
+ NOT the string-shaped serialized form, which sidesteps parseArgs's query-coercion
114
+ gap) before any fetch. The validate may return a Promise (StandardSchemaV1), so await
115
+ it (D3). On a returned failure throw an HttpError shaped IDENTICALLY to the server's
116
+ 422 (validationHttpError) and make NO fetch saving the round-trip. */
117
+ if (preflightSchema !== undefined) {
118
+ return (
119
+ Promise.resolve()
120
+ .then(() => preflightSchema['~standard'].validate(args))
121
+ .then(
122
+ (result) => {
123
+ /* A returned `issues` result is a definitive "this input is invalid" —
124
+ throw the same 422-shaped HttpError the server would (validationHttpError)
125
+ so it rejects the chain and makes NO fetch, saving the round-trip. */
126
+ if (result.issues) {
127
+ throw validationHttpError(result.issues)
128
+ }
129
+ },
130
+ () => {
131
+ /* A validate REJECTION (not an `issues` verdict) means the validator itself
132
+ could not run here (a non-portable / async-resource refinement), which is
133
+ NOT a verdict on the input: swallow it and fall through to the fetch, letting
134
+ the server (the authoritative validator) decide. So a schema can never break
135
+ a call merely by failing to run client-side. */
136
+ },
137
+ )
138
+ /* Fetch exactly ONCE, only after validation settles — outside the validate handlers
139
+ so a fetch/timeout rejection propagates to the caller untouched rather than being
140
+ mistaken for a "validator couldn't run" fall-through and resending the request. */
141
+ .then(() => fetchWithTimeout(getRequest(), opts))
123
142
  )
124
143
  }
125
- const request = getRequest()
126
- const parkable = request.clone()
127
- return fetchWithTimeout(request, opts).then(
128
- (response) => {
129
- if (UNREACHABLE_STATUSES.has(response.status)) {
130
- throw queuedThrow(
131
- queue,
132
- args as Args,
133
- parkable,
134
- response,
135
- new HttpError(response.clone()),
136
- )
137
- }
138
- return response
139
- },
140
- (error: unknown) => {
141
- if (shouldParkRejection(error)) {
142
- const response =
143
- error instanceof HttpError ? error.response : unreachableResponse()
144
- throw queuedThrow(queue, args as Args, parkable, response, error)
145
- }
146
- throw error
147
- },
148
- )
144
+ return fetchWithTimeout(getRequest(), opts)
149
145
  },
150
146
  })
151
147
  /* Overwrite the inert `.watch` the shared attach bound: on the client the real reaction
@@ -159,80 +155,9 @@ export function remoteProxy<Args, Return>(
159
155
  handler,
160
156
  ),
161
157
  })
162
- if (durable?.outbox === true) {
163
- queue = getOrCreateOutboxQueue<Args, Return>(url, fn, durable)
164
- Object.assign(fn, { outbox: outboxFace(queue) })
165
- }
166
158
  return fn
167
159
  }
168
160
 
169
- /* The synthetic "unreachable" Response a park reuses when there is no real one — a
170
- transport failure (fetch rejected) or a backlog park that never fetched. */
171
- function unreachableResponse(): Response {
172
- return new Response('queued', { status: 503, statusText: 'Service Unavailable' })
173
- }
174
-
175
- /* Park the unreachable request (`cause` becomes the entry's parked reason, `entry.error`)
176
- and return the `kind: 'queued'` HttpError to throw — its `.data` is the parked entry, so
177
- a caller can `await (error.data as OutboxEntry).settled` for the eventual delivered
178
- result or server refusal. */
179
- function queuedThrow<Args>(
180
- queue: OutboxQueue<Args> | undefined,
181
- args: Args,
182
- request: Request,
183
- response: Response,
184
- cause: unknown,
185
- ): HttpError {
186
- const entry = queue?.park(args, request, cause)
187
- return new HttpError(response, QUEUED, entry)
188
- }
189
-
190
- /* A fetch REJECTION (no Response) the durable rpc should park: a transport failure or
191
- the synthesized client-timeout 504. NOT a caller/scope abort — that's a deliberate
192
- cancel, not the server being unreachable. (HTTP error STATUSES never reject — `fetch`
193
- resolves with them — so 4xx/500 are classified on the response, not here.) */
194
- function shouldParkRejection(error: unknown): boolean {
195
- if (error === REQUEST_SUPERSEDED) {
196
- return false
197
- }
198
- if (error instanceof DOMException && error.name === 'AbortError') {
199
- return false
200
- }
201
- if (error instanceof HttpError) {
202
- return UNREACHABLE_STATUSES.has(error.response.status)
203
- }
204
- return true
205
- }
206
-
207
- /* The single app-owned queue for a durable RPC url — created + registered on first use
208
- so every call site (and the global `outbox()`) shares one queue. The send is a plain
209
- `fetch`; createOutboxQueue rides the entry's abort signal on the resent Request and
210
- keeps scope-abort + the client timeout out. */
211
- function getOrCreateOutboxQueue<Args, Return>(
212
- url: string,
213
- rpc: RemoteFunction<Args, Return>,
214
- durable: DurableOptions,
215
- ): OutboxQueue<Args> {
216
- const existing = outboxRegistry.get(url)
217
- if (existing !== undefined) {
218
- return existing as OutboxQueue<Args>
219
- }
220
- const queue = createOutboxQueue<Args>({
221
- url,
222
- send: (request) => fetch(request),
223
- store: durable.store,
224
- })
225
- outboxRegistry.register(url, queue as OutboxQueue<unknown>, rpc)
226
- return queue
227
- }
228
-
229
- /* The `.outbox` face: callable for the live entries, `retry()` to drain on demand. */
230
- function outboxFace<Args>(queue: OutboxQueue<Args>): Outbox<Args> {
231
- const face = (() => queue.entries()) as Outbox<Args>
232
- face.retry = () => queue.retry()
233
- return face
234
- }
235
-
236
161
  /*
237
162
  Fetches under three optional aborts: the reactive scope that fired the call (so a
238
163
  superseded/torn-down read cancels its in-flight request — currentAbortSignal), the
@@ -1,9 +1,11 @@
1
+ import { isolateCellBarrier } from './isolateCellBarrier.ts'
1
2
  import { CHILD_PRESENT } from './runtime/CHILD_PRESENT.ts'
2
3
  import { OUTLET_CLOSE, OUTLET_OPEN } from './runtime/OUTLET_MARKER.ts'
3
4
  import type { RenderContext } from './runtime/types/RenderContext.ts'
4
5
  import type { SsrRender } from './runtime/types/SsrRender.ts'
5
6
  import type { UiComponent } from './runtime/types/UiComponent.ts'
6
7
  import type { UiProps } from './runtime/types/UiProps.ts'
8
+ import { withPath } from './runtime/withPath.ts'
7
9
 
8
10
  const OPEN = `<!--${OUTLET_OPEN}-->`
9
11
  const CLOSE = `<!--${OUTLET_CLOSE}-->`
@@ -13,11 +15,13 @@ const OUTLET_PLACEHOLDER = `${OPEN}${CLOSE}`
13
15
  /*
14
16
  Server-renders a route's layout chain wrapped around its page into one SsrRender.
15
17
  `views` is ordered outermost layout → … → page. The whole chain shares ONE request-local
16
- block-id counter (`$ctx`): each `render()` is awaited sequentially (render is async), so
17
- every `await`/`try` block across all layers draws a unique id from the shared counter in
18
- the same layer-sequential order the client hydrates them, keeping the streamed fragments
19
- and the resume manifest aligned. Sequential (not `Promise.all`) so the counter advances
20
- deterministically and the reactive scopes never interleave.
18
+ block-id counter map (`$ctx`): each `await`/`try` block draws a path-namespaced id
19
+ (`${render-path}:${n}`, ADR-0037), so ids stay unique across layers by path rather than by
20
+ a shared sequential draw — keeping the streamed fragments and the resume manifest aligned
21
+ with the client (which composes the same path-keyed ids). The layers render IN PARALLEL
22
+ (`Promise.all`, ADR-0038): each roots a distinct route-key path so their block ids never collide,
23
+ and each runs under `isolateCellBarrier` so their async-cell barriers don't cross-drain — the html
24
+ fold + state/awaits/resume aggregation below run after all settle and are order-independent.
21
25
 
22
26
  The html nests inner-to-outer: each parent layout's empty outlet boundary
23
27
  (`<!--abide:outlet--><!--/abide:outlet-->`) is filled with the accumulated child html —
@@ -30,22 +34,58 @@ A layout missing its `<slot/>` is a build error surfaced here.
30
34
  export async function renderChain(
31
35
  views: UiComponent[],
32
36
  params: Record<string, string>,
37
+ /* Each view's route key (layout directory URL / page route pattern), aligned 1:1 with `views`
38
+ and byte-identical to the client router's `chainKeys`/`pageKey`. Each roots its layer's
39
+ render-path (`withPath`) so a cell's scope id matches the client's for the warm-seed key. */
40
+ keys: string[] = [],
33
41
  ): Promise<SsrRender> {
34
- const ctx: RenderContext = { next: 0 }
42
+ const ctx: RenderContext = new Map()
35
43
  const renders: SsrRender[] = []
36
44
  /* Route params as thunks (static server-side — only shape parity with the client so
37
45
  `props()` reads `$props["id"]?.()` resolve). A layout (every view but the last) also
38
- gets `$children` set to `CHILD_PRESENT` so SSR renders `{#if children}` the same way
39
- the client does, keeping hydration congruent. */
46
+ gets `children` set to `CHILD_PRESENT` so SSR renders `{#if children}` the same way
47
+ the client does, keeping hydration congruent. The sentinel itself must be thunk-wrapped
48
+ (`() => CHILD_PRESENT`) since the destructure lowering CALLS every bag entry — leaving
49
+ it raw would invoke `CHILD_PRESENT` (a function) and read back `undefined`. */
40
50
  const paramThunks: UiProps = {}
41
51
  for (const key of Object.keys(params)) {
42
52
  paramThunks[key] = () => params[key]
43
53
  }
44
- for (let index = 0; index < views.length; index += 1) {
45
- const view = views[index] as UiComponent
46
- const hasChild = index < views.length - 1
47
- const props: UiProps = hasChild ? { ...paramThunks, $children: CHILD_PRESENT } : paramThunks
48
- renders.push(await view.render(props, ctx))
54
+ if (views.length > 1) {
55
+ /* ADR-0038: a route WITH layouts renders its layers IN PARALLEL. Block ids are path-keyed
56
+ (ADR-0037) and each layer roots a DISTINCT route-key path, so their id allocations never
57
+ collide even as the async continuations interleave; the fold + state/awaits/resume
58
+ aggregation below run AFTER all settle and are order-independent (keyed merges /
59
+ index-ordered). Each layer runs under `isolateCellBarrier` so its async-cell barrier drains
60
+ its OWN pending list — without it, two layers registering cells concurrently into the one
61
+ request-scoped list would splice-drain each other (the hazard ADR-0037 fixed for sibling
62
+ children). Scope needs no isolation: the shipped parallel child renders tolerate the
63
+ identical per-request CURRENT_SCOPE clobber (all scope-sensitive construction is
64
+ synchronous in each render's prefix). */
65
+ const collected = await Promise.all(
66
+ views.map((view, index) => {
67
+ const hasChild = index < views.length - 1
68
+ const props: UiProps = hasChild
69
+ ? { ...paramThunks, children: () => CHILD_PRESENT }
70
+ : paramThunks
71
+ const key = keys[index]
72
+ const run = () => isolateCellBarrier(() => view.render(props, ctx))
73
+ return key === undefined ? run() : withPath(key, run)
74
+ }),
75
+ )
76
+ renders.push(...collected)
77
+ } else if (views.length === 1) {
78
+ /* A lone page (no layouts) — the common case — renders DIRECTLY: no parallelism to gain, and
79
+ no `Promise.all`/`isolateCellBarrier` wrap, so its bare-read/settle timing is byte-identical
80
+ to the pre-ADR-0038 path (a fast in-process read stays pending → streams, rather than
81
+ slipping settled → inline behind an extra microtask). */
82
+ const view = views[0] as UiComponent
83
+ const key = keys[0]
84
+ renders.push(
85
+ key === undefined
86
+ ? await view.render(paramThunks, ctx)
87
+ : await withPath(key, () => view.render(paramThunks, ctx)),
88
+ )
49
89
  }
50
90
  let html = renders[renders.length - 1]?.html ?? ''
51
91
  for (let index = renders.length - 2; index >= 0; index -= 1) {
@@ -8,8 +8,8 @@ Out-of-order SSR streaming. Yields the shell first (so the browser paints
8
8
  immediately), then one resolved fragment per STREAMING await block as its promise
9
9
  settles — in completion order, not source order, so a slow read never blocks a fast
10
10
  one. Each resolved fragment is a `<abide-resolve data-id="ID"><script
11
- type="application/json">…</script>…</abide-resolve>` that `applyResolved` swaps into
12
- the matching `<!--abide:await:ID-->` boundary; the leading script holds the
11
+ type="application/json">…</script>…</abide-resolve>` that the inline swap script swaps
12
+ into the matching `<!--abide:await:ID-->` boundary; the leading script holds the
13
13
  JSON-serialized value, registered for hydration so an `await` block adopts the
14
14
  resolved branch on resume instead of re-running.
15
15
 
@@ -38,14 +38,13 @@ export async function* renderToStream(
38
38
  alongside each streamed fragment, so the client adopts the nested blocking branch
39
39
  instead of refetching. (`resume` is the render body's live object, so late writes
40
40
  appear here.) */
41
- const seededResume = new Set<number>(Object.keys(resume).map(Number))
42
- const resumeDelta = (): Record<number, ResumeEntry> => {
43
- const delta: Record<number, ResumeEntry> = {}
41
+ const seededResume = new Set<string>(Object.keys(resume))
42
+ const resumeDelta = (): Record<string, ResumeEntry> => {
43
+ const delta: Record<string, ResumeEntry> = {}
44
44
  for (const [key, entry] of Object.entries(resume)) {
45
- const id = Number(key)
46
- if (!seededResume.has(id)) {
47
- seededResume.add(id)
48
- delta[id] = entry
45
+ if (!seededResume.has(key)) {
46
+ seededResume.add(key)
47
+ delta[key] = entry
49
48
  }
50
49
  }
51
50
  return delta
@@ -55,8 +54,8 @@ export async function* renderToStream(
55
54
  awaits — its `branchContent` runs `$awaits.push(...)` onto this same `awaits` array
56
55
  during `settle`, AFTER the initial scan. So re-scan for newly-appended blocks after
57
56
  every settle (tracking which ids are already enqueued), composing to any depth. */
58
- const inflight = new Map<number, Promise<Settled>>()
59
- const enqueued = new Set<number>()
57
+ const inflight = new Map<string, Promise<Settled>>()
58
+ const enqueued = new Set<string>()
60
59
  const enqueueNew = (): void => {
61
60
  for (const block of awaits) {
62
61
  if (!enqueued.has(block.id)) {
@@ -71,11 +70,14 @@ export async function* renderToStream(
71
70
  inflight.delete(resolved.id)
72
71
  enqueueNew()
73
72
  /* An unserializable value (e.g. a cyclic media tree) streams its rendered HTML
74
- with NO seed script: both swap consumers (SSR_SWAP_SCRIPT, applyResolved) skip
73
+ with NO seed script: the inline swap script (SSR_SWAP_SCRIPT) skips
75
74
  registration when the leading child isn't a parseable script, so hydration
76
75
  re-runs this one branch's promise — degrading to a refetch instead of aborting
77
76
  the whole stream. */
78
- const encoded = encodeStreamResume(resolved.resume, resolved.id)
77
+ const encoded =
78
+ resolved.resume === undefined
79
+ ? undefined
80
+ : encodeStreamResume(resolved.resume, resolved.id)
79
81
  yield resumeSeedScript(resumeDelta()) +
80
82
  `<abide-resolve data-id="${resolved.id}">` +
81
83
  (encoded === undefined ? '' : `<script type="application/json">${encoded}</script>`) +
@@ -83,7 +85,7 @@ export async function* renderToStream(
83
85
  }
84
86
  }
85
87
 
86
- type Settled = { id: number; html: string; resume: ResumeEntry }
88
+ type Settled = { id: string; html: string; resume: ResumeEntry | undefined }
87
89
 
88
90
  /* Awaits one streaming block's promise and renders the resolved or error branch to
89
91
  HTML (the renderers are async so a nested `await` block composes), capturing the
@@ -95,7 +97,9 @@ function settle(block: SsrAwait): Promise<Settled> {
95
97
  async (value) => ({
96
98
  id: block.id,
97
99
  html: await block.then(value),
98
- resume: { ok: true, value },
100
+ /* A standalone-unit (streamed component) boundary carries no resume value of its own —
101
+ its `then` already merged the child's nested awaits/resume for streaming composition. */
102
+ resume: block.htmlOnly === true ? undefined : { ok: true, value },
99
103
  }),
100
104
  async (error) => {
101
105
  /* No catch branch → surface the rejection (500 before the first flush,
@@ -117,8 +121,8 @@ function settle(block: SsrAwait): Promise<Settled> {
117
121
  literal `</script>` from closing the block early — quotes stay raw, and the escape
118
122
  survives `decodeRefJson`'s inner JSON.parse since `<` only ever appears inside JSON
119
123
  strings. `tryEncodeResume` handles the serialize-or-refetch policy (undefined → no
120
- script → the swap consumers skip registration → hydration re-runs that one promise).
121
- `applyResolved`/the inline swap script store it via `.textContent`; `awaitBlock` decodes it. */
122
- function encodeStreamResume(resume: ResumeEntry, id: number): string | undefined {
124
+ script → the inline swap script skips registration → hydration re-runs that one promise).
125
+ The inline swap script stores it via `.textContent`; `awaitBlock` decodes it. */
126
+ function encodeStreamResume(resume: ResumeEntry, id: string): string | undefined {
123
127
  return tryEncodeResume(resume, id)?.replace(/</g, '\\u003c')
124
128
  }
@@ -11,7 +11,7 @@ import { tryEncodeResume } from './tryEncodeResume.ts'
11
11
  early or parse as a line terminator. Shared by the buffered (`createUiPageRenderer`)
12
12
  and streaming (`renderToStream`) paths. */
13
13
  // @documentation plumbing
14
- export function resumeSeedScript(resume: Record<number, ResumeEntry>): string {
14
+ export function resumeSeedScript(resume: Record<string, ResumeEntry>): string {
15
15
  /* ref-json (not JSON) so a value carrying cycles or shared back-references — a
16
16
  media tree with parent↔child links — seeds instead of being dropped. `tryEncodeResume`
17
17
  drops just an unserializable entry (the client re-runs that one branch's promise),