@abide/abide 0.49.0 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (325) hide show
  1. package/AGENTS.md +70 -52
  2. package/CHANGELOG.md +177 -0
  3. package/README.md +12 -7
  4. package/package.json +14 -5
  5. package/src/abideLsp.ts +5 -6
  6. package/src/abideResolverPlugin.ts +197 -169
  7. package/src/build.ts +79 -56
  8. package/src/buildDisconnected.ts +0 -2
  9. package/src/checkAbide.ts +12 -2
  10. package/src/devEntry.ts +113 -28
  11. package/src/discoveryEntry.ts +3 -2
  12. package/src/lib/bundle/disconnected.abide +69 -73
  13. package/src/lib/bundle/infoPlist.ts +13 -7
  14. package/src/lib/bundle/spawnEmbeddedServer.ts +13 -4
  15. package/src/lib/bundle/waitForServer.ts +6 -1
  16. package/src/lib/mcp/mcpTools.ts +15 -6
  17. package/src/lib/server/error.ts +6 -6
  18. package/src/lib/server/json.ts +17 -3
  19. package/src/lib/server/rpc/defineRpc.ts +58 -24
  20. package/src/lib/server/rpc/parseArgs.ts +98 -13
  21. package/src/lib/server/rpc/resolveRpcJsonSchema.ts +28 -0
  22. package/src/lib/server/rpc/types/RpcHelper.ts +75 -105
  23. package/src/lib/server/rpc/types/RpcRegistryEntry.ts +24 -9
  24. package/src/lib/server/rpc/validationError.ts +1 -1
  25. package/src/lib/server/runtime/DEV_RELOAD_CLIENT_SCRIPT.ts +0 -15
  26. package/src/lib/server/runtime/buildCacheSnapshot.ts +10 -9
  27. package/src/lib/server/runtime/buildInspectorSurface.ts +6 -4
  28. package/src/lib/server/runtime/buildOpenApiSpec.ts +55 -11
  29. package/src/lib/server/runtime/buildPreloadManifest.ts +12 -10
  30. package/src/lib/server/runtime/createAppAssetServer.ts +18 -15
  31. package/src/lib/server/runtime/createAppRouteResolver.ts +145 -0
  32. package/src/lib/server/runtime/createPlumbingRouter.ts +212 -0
  33. package/src/lib/server/runtime/createRouteDispatcher.ts +4 -10
  34. package/src/lib/server/runtime/createServer.ts +120 -324
  35. package/src/lib/server/runtime/createUiPageRenderer.ts +137 -26
  36. package/src/lib/server/runtime/devClientFingerprint.ts +19 -34
  37. package/src/lib/server/runtime/installAmbientScopeStore.ts +30 -9
  38. package/src/lib/server/runtime/logExposedSurfaces.ts +8 -7
  39. package/src/lib/server/runtime/pathStore.ts +15 -0
  40. package/src/lib/server/runtime/registryManifests.ts +1 -1
  41. package/src/lib/server/runtime/renderCellBarrierStore.ts +15 -0
  42. package/src/lib/server/runtime/runWithRequestScope.ts +3 -0
  43. package/src/lib/server/runtime/serializeCacheSnapshot.ts +6 -4
  44. package/src/lib/server/runtime/snapshotEntryFromCache.ts +14 -14
  45. package/src/lib/server/runtime/streamCacheResolutions.ts +14 -7
  46. package/src/lib/server/runtime/streamFromIterator.ts +30 -2
  47. package/src/lib/server/runtime/textResponse.ts +23 -0
  48. package/src/lib/server/runtime/types/DevReloadStamp.ts +6 -10
  49. package/src/lib/server/runtime/types/InspectorCacheEntry.ts +1 -1
  50. package/src/lib/server/runtime/types/RequestStore.ts +27 -0
  51. package/src/lib/server/runtime/warnUnguardedMcp.ts +15 -5
  52. package/src/lib/server/sockets/createSocketDispatcher.ts +9 -11
  53. package/src/lib/server/sockets/defineSocket.ts +5 -4
  54. package/src/lib/server/sse.ts +5 -1
  55. package/src/lib/shared/ASYNC_CELL.ts +5 -0
  56. package/src/lib/shared/DEV_REBUILD_PATH.ts +6 -0
  57. package/src/lib/shared/HTTP_METHODS.ts +6 -0
  58. package/src/lib/shared/HttpError.ts +1 -5
  59. package/src/lib/shared/MCP_PATH.ts +6 -0
  60. package/src/lib/shared/PROXIED_SERVER_SUBDIRS.ts +15 -0
  61. package/src/lib/shared/REF_JSON_TAGS.ts +13 -1
  62. package/src/lib/shared/RPC_SHIM_GLOBALS.ts +9 -0
  63. package/src/lib/shared/activeCacheStore.ts +1 -0
  64. package/src/lib/shared/activePage.ts +1 -0
  65. package/src/lib/shared/buildArtifact.ts +4 -1
  66. package/src/lib/shared/buildSocketOverChannel.ts +4 -3
  67. package/src/lib/shared/bundleGraphFromMetafile.ts +68 -0
  68. package/src/lib/shared/cache.ts +156 -131
  69. package/src/lib/shared/changeAffectsClient.ts +12 -7
  70. package/src/lib/shared/createCacheStore.ts +31 -8
  71. package/src/lib/shared/createLifecycleChannel.ts +7 -1
  72. package/src/lib/shared/createReachable.ts +47 -78
  73. package/src/lib/shared/createRemoteFunction.ts +52 -30
  74. package/src/lib/shared/createRpcServerProgram.ts +820 -0
  75. package/src/lib/shared/decodeRefJson.ts +4 -4
  76. package/src/lib/shared/decodeResponse.ts +2 -2
  77. package/src/lib/shared/decodeWireBody.ts +35 -0
  78. package/src/lib/shared/detectRpcMethod.ts +8 -1
  79. package/src/lib/shared/done.ts +8 -2
  80. package/src/lib/shared/encodeRefJson.ts +4 -4
  81. package/src/lib/shared/encodeWireBody.ts +18 -0
  82. package/src/lib/{server/rpc → shared}/fieldErrorsFromIssues.ts +5 -1
  83. package/src/lib/shared/generateDeclarations.ts +72 -0
  84. package/src/lib/shared/hasSeedableRequest.ts +25 -0
  85. package/src/lib/shared/hydrationWindow.ts +53 -0
  86. package/src/lib/shared/isAsyncCell.ts +11 -0
  87. package/src/lib/shared/isAsyncIterable.ts +8 -0
  88. package/src/lib/shared/isSubscribable.ts +3 -3
  89. package/src/lib/shared/isThenable.ts +9 -0
  90. package/src/lib/shared/jsonSchemaForType.ts +258 -0
  91. package/src/lib/shared/lenientDecode.ts +15 -0
  92. package/src/lib/shared/loadProjectTsConfig.ts +28 -0
  93. package/src/lib/shared/markFrameworkSourcesIgnored.ts +1 -1
  94. package/src/lib/shared/matchRoute.ts +4 -14
  95. package/src/lib/shared/peek.ts +14 -0
  96. package/src/lib/shared/pending.ts +9 -6
  97. package/src/lib/shared/pendingAsyncCellsSlot.ts +13 -0
  98. package/src/lib/shared/prepareRpcModule.ts +91 -93
  99. package/src/lib/shared/prepareSocketModule.ts +3 -2
  100. package/src/lib/shared/probeRegistries.ts +5 -18
  101. package/src/lib/shared/reachable.ts +7 -10
  102. package/src/lib/shared/refresh.ts +12 -2
  103. package/src/lib/shared/refreshing.ts +8 -2
  104. package/src/lib/shared/resolvedCellsSlot.ts +13 -0
  105. package/src/lib/shared/reviveWireField.ts +49 -0
  106. package/src/lib/shared/reviveWireOutput.ts +36 -0
  107. package/src/lib/shared/rpcServerForRoot.ts +25 -0
  108. package/src/lib/shared/scanPages.ts +25 -0
  109. package/src/lib/shared/snapshotShippable.ts +8 -7
  110. package/src/lib/shared/streamedCellsSlot.ts +14 -0
  111. package/src/lib/shared/subscribableFromResponse.ts +4 -4
  112. package/src/lib/shared/subscribableProbes.ts +1 -1
  113. package/src/lib/shared/tailProbeSlot.ts +1 -1
  114. package/src/lib/shared/types/AsyncComputed.ts +20 -0
  115. package/src/lib/shared/types/AsyncState.ts +13 -0
  116. package/src/lib/shared/types/CacheEntry.ts +7 -7
  117. package/src/lib/shared/types/CacheOptions.ts +23 -37
  118. package/src/lib/shared/types/CachePolicy.ts +25 -0
  119. package/src/lib/shared/types/CacheSnapshotEntry.ts +6 -5
  120. package/src/lib/shared/types/ErrorJsonSchemas.ts +8 -0
  121. package/src/lib/shared/types/HttpMethod.ts +3 -1
  122. package/src/lib/shared/types/InputCoercion.ts +17 -0
  123. package/src/lib/shared/types/{Subscribable.ts → NamedAsyncIterable.ts} +1 -1
  124. package/src/lib/shared/types/OutputWirePlan.ts +17 -0
  125. package/src/lib/shared/types/PagesScan.ts +7 -0
  126. package/src/lib/shared/types/PendingAsyncCells.ts +10 -0
  127. package/src/lib/shared/types/RawRemoteFunction.ts +6 -0
  128. package/src/lib/shared/types/RemoteCallable.ts +8 -7
  129. package/src/lib/shared/types/RemoteFunction.ts +16 -15
  130. package/src/lib/shared/types/ResolvedCells.ts +11 -0
  131. package/src/lib/shared/types/ReturnBody.ts +15 -0
  132. package/src/lib/shared/types/RpcBuildStamps.ts +23 -0
  133. package/src/lib/shared/types/RpcErrorGuard.ts +3 -8
  134. package/src/lib/shared/types/Socket.ts +4 -4
  135. package/src/lib/shared/types/SsrPayload.ts +5 -1
  136. package/src/lib/shared/types/StreamPolicy.ts +11 -0
  137. package/src/lib/shared/types/StreamedCells.ts +19 -0
  138. package/src/lib/shared/types/StreamedResolution.ts +24 -6
  139. package/src/lib/shared/types/TailHooks.ts +1 -1
  140. package/src/lib/shared/types/WireKind.ts +11 -0
  141. package/src/lib/shared/validationHttpError.ts +33 -0
  142. package/src/lib/shared/warmSeedKey.ts +16 -0
  143. package/src/lib/shared/wireJsonReplacer.ts +30 -0
  144. package/src/lib/shared/writeRpcDts.ts +11 -1
  145. package/src/lib/shared/writeTestSocketsDts.ts +1 -1
  146. package/src/lib/test/createTestApp.ts +19 -1
  147. package/src/lib/ui/README.md +1 -1
  148. package/src/lib/ui/activePendingCells.ts +14 -0
  149. package/src/lib/ui/compile/BLOCK_KEYWORDS.ts +21 -0
  150. package/src/lib/ui/compile/SSR_ESCAPE.ts +4 -1
  151. package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +35 -8
  152. package/src/lib/ui/compile/abideUiPlugin.ts +19 -2
  153. package/src/lib/ui/compile/analyzeComponent.ts +89 -6
  154. package/src/lib/ui/compile/assignmentTargetNames.ts +54 -0
  155. package/src/lib/ui/compile/asyncInterpolationFields.ts +152 -0
  156. package/src/lib/ui/compile/asyncValuePositionError.ts +21 -0
  157. package/src/lib/ui/compile/asyncValuePositionInterpolations.ts +64 -0
  158. package/src/lib/ui/compile/attrLiftPosition.ts +18 -0
  159. package/src/lib/ui/compile/cachedSourceFile.ts +40 -0
  160. package/src/lib/ui/compile/catchBinding.ts +7 -5
  161. package/src/lib/ui/compile/classifyInterpolationType.ts +46 -0
  162. package/src/lib/ui/compile/collectAbideDiagnostics.ts +96 -0
  163. package/src/lib/ui/compile/compileComponent.ts +19 -3
  164. package/src/lib/ui/compile/compileModule.ts +27 -58
  165. package/src/lib/ui/compile/compileSSR.ts +51 -12
  166. package/src/lib/ui/compile/compileShadow.ts +145 -18
  167. package/src/lib/ui/compile/composeProps.ts +12 -2
  168. package/src/lib/ui/compile/createShadowLanguageService.ts +116 -68
  169. package/src/lib/ui/compile/createShadowProgram.ts +35 -6
  170. package/src/lib/ui/compile/declaredNames.ts +36 -0
  171. package/src/lib/ui/compile/desugarSignals.ts +418 -36
  172. package/src/lib/ui/compile/expressionIsPrefixEvaluable.ts +19 -0
  173. package/src/lib/ui/compile/generateBuild.ts +52 -9
  174. package/src/lib/ui/compile/generateSSR.ts +242 -43
  175. package/src/lib/ui/compile/hoistableAwaits.ts +193 -0
  176. package/src/lib/ui/compile/hoistableChildRenders.ts +112 -0
  177. package/src/lib/ui/compile/interpolatedTemplateLiteral.ts +3 -1
  178. package/src/lib/ui/compile/interpolationClassifierForRoot.ts +37 -0
  179. package/src/lib/ui/compile/isSpuriousAsyncReadDiagnostic.ts +174 -0
  180. package/src/lib/ui/compile/liftAsyncSubExpressions.ts +166 -0
  181. package/src/lib/ui/compile/lowerAsyncInterpolations.ts +92 -0
  182. package/src/lib/ui/compile/lowerContext.ts +10 -0
  183. package/src/lib/ui/compile/lowerDocAccess.ts +26 -23
  184. package/src/lib/ui/compile/lowerScript.ts +40 -4
  185. package/src/lib/ui/compile/nodeAtShadowOffset.ts +28 -0
  186. package/src/lib/ui/compile/parseTemplate.ts +18 -1092
  187. package/src/lib/ui/compile/parseTemplateRecovering.ts +1385 -0
  188. package/src/lib/ui/compile/referencedIdentifiers.ts +35 -0
  189. package/src/lib/ui/compile/renameSignalRefs.ts +21 -22
  190. package/src/lib/ui/compile/seedTypeClassifierForRoot.ts +65 -0
  191. package/src/lib/ui/compile/shadowInterpolationClassifier.ts +47 -0
  192. package/src/lib/ui/compile/sourceFileOptionsSignature.ts +20 -0
  193. package/src/lib/ui/compile/structuralHeadTokens.ts +107 -0
  194. package/src/lib/ui/compile/templateSemanticTokens.ts +21 -0
  195. package/src/lib/ui/compile/templateStartOffset.ts +15 -0
  196. package/src/lib/ui/compile/tryPlan.ts +4 -3
  197. package/src/lib/ui/compile/types/AnalyzedComponent.ts +4 -0
  198. package/src/lib/ui/compile/types/AsyncInterpolationField.ts +19 -0
  199. package/src/lib/ui/compile/types/InterpolationClassifier.ts +12 -0
  200. package/src/lib/ui/compile/types/InterpolationKind.ts +7 -0
  201. package/src/lib/ui/compile/types/ParseDiagnostic.ts +8 -0
  202. package/src/lib/ui/compile/types/SeedTypeClassifier.ts +15 -0
  203. package/src/lib/ui/compile/types/TemplateNode.ts +36 -4
  204. package/src/lib/ui/compile/types/ValuePositionInterpolation.ts +13 -0
  205. package/src/lib/ui/compile/writtenTemplateNames.ts +84 -0
  206. package/src/lib/ui/computed.ts +28 -1
  207. package/src/lib/ui/createScope.ts +35 -68
  208. package/src/lib/ui/dom/anchoredBranch.ts +142 -0
  209. package/src/lib/ui/dom/appendText.ts +8 -3
  210. package/src/lib/ui/dom/awaitBlock.ts +43 -94
  211. package/src/lib/ui/dom/bindProp.ts +22 -0
  212. package/src/lib/ui/dom/bindableProp.ts +47 -0
  213. package/src/lib/ui/dom/cellPending.ts +24 -0
  214. package/src/lib/ui/dom/disposeRange.ts +2 -1
  215. package/src/lib/ui/dom/each.ts +26 -4
  216. package/src/lib/ui/dom/eachAsync.ts +39 -3
  217. package/src/lib/ui/dom/fillBoundary.ts +2 -3
  218. package/src/lib/ui/dom/fillRange.ts +4 -4
  219. package/src/lib/ui/dom/hydrate.ts +6 -12
  220. package/src/lib/ui/dom/isComment.ts +1 -1
  221. package/src/lib/ui/dom/matchingRangeClose.ts +29 -0
  222. package/src/lib/ui/dom/mount.ts +1 -2
  223. package/src/lib/ui/dom/mountChild.ts +9 -40
  224. package/src/lib/ui/dom/mountRange.ts +2 -3
  225. package/src/lib/ui/dom/mountStreamedChild.ts +84 -0
  226. package/src/lib/ui/dom/mountSwappableRange.ts +46 -4
  227. package/src/lib/ui/dom/mutateDocContainer.ts +65 -0
  228. package/src/lib/ui/dom/on.ts +2 -2
  229. package/src/lib/ui/dom/readCell.ts +40 -0
  230. package/src/lib/ui/dom/switchBlock.ts +30 -7
  231. package/src/lib/ui/dom/tryBlock.ts +230 -66
  232. package/src/lib/ui/dom/types/SwitchCase.ts +5 -1
  233. package/src/lib/ui/dom/when.ts +12 -2
  234. package/src/lib/ui/dom/withScope.ts +2 -2
  235. package/src/lib/ui/finalizeStreamedChildren.ts +89 -0
  236. package/src/lib/ui/flight.ts +56 -0
  237. package/src/lib/ui/html.ts +12 -1
  238. package/src/lib/ui/isolateCellBarrier.ts +17 -0
  239. package/src/lib/ui/linked.ts +48 -2
  240. package/src/lib/ui/remoteProxy.ts +84 -159
  241. package/src/lib/ui/renderChain.ts +49 -13
  242. package/src/lib/ui/renderToStream.ts +22 -18
  243. package/src/lib/ui/resumeSeedScript.ts +1 -1
  244. package/src/lib/ui/router.ts +63 -36
  245. package/src/lib/ui/runtime/AsyncCellError.ts +20 -0
  246. package/src/lib/ui/runtime/CELL_SEED.ts +14 -0
  247. package/src/lib/ui/runtime/CURRENT_BOUNDARY.ts +11 -0
  248. package/src/lib/ui/runtime/CURRENT_PATH.ts +29 -0
  249. package/src/lib/ui/runtime/RENDER.ts +10 -7
  250. package/src/lib/ui/runtime/RESUME.ts +4 -4
  251. package/src/lib/ui/runtime/STREAMED_CELLS.ts +57 -0
  252. package/src/lib/ui/runtime/ambientPathBacking.ts +32 -0
  253. package/src/lib/ui/runtime/applyPatchToTree.ts +1 -1
  254. package/src/lib/ui/runtime/blockId.ts +31 -0
  255. package/src/lib/ui/runtime/boundaryFor.ts +11 -0
  256. package/src/lib/ui/runtime/cellBarrierBacking.ts +30 -0
  257. package/src/lib/ui/runtime/createAsyncCell.ts +300 -0
  258. package/src/lib/ui/runtime/createDoc.ts +3 -42
  259. package/src/lib/ui/runtime/createEffectNode.ts +9 -0
  260. package/src/lib/ui/runtime/enterRenderPass.ts +5 -4
  261. package/src/lib/ui/runtime/flushEffects.ts +16 -6
  262. package/src/lib/ui/runtime/isAsyncFunction.ts +14 -0
  263. package/src/lib/ui/runtime/nextBlockId.ts +8 -7
  264. package/src/lib/ui/runtime/renderPath.ts +16 -0
  265. package/src/lib/ui/runtime/types/Boundary.ts +9 -0
  266. package/src/lib/ui/runtime/types/RenderContext.ts +9 -7
  267. package/src/lib/ui/runtime/types/SsrRender.ts +9 -2
  268. package/src/lib/ui/runtime/types/UiComponent.ts +1 -5
  269. package/src/lib/ui/runtime/withOptionalPath.ts +8 -0
  270. package/src/lib/ui/runtime/withPath.ts +16 -0
  271. package/src/lib/ui/runtime/withPathFrom.ts +20 -0
  272. package/src/lib/ui/seedStreamedResolution.ts +31 -6
  273. package/src/lib/ui/settleAsyncCells.ts +24 -0
  274. package/src/lib/ui/socketProxy.ts +1 -1
  275. package/src/lib/ui/startClient.ts +16 -31
  276. package/src/lib/ui/trackedComputed.ts +68 -0
  277. package/src/lib/ui/types/Scope.ts +8 -24
  278. package/src/lib/ui/watch.ts +3 -3
  279. package/src/serverEntry.ts +14 -0
  280. package/template/src/server/rpc/getHello.ts +15 -13
  281. package/template/test/app.test.ts +1 -1
  282. package/src/lib/server/runtime/devHotModuleResponse.ts +0 -41
  283. package/src/lib/shared/DEV_HOT_PREFIX.ts +0 -7
  284. package/src/lib/shared/UNREACHABLE_STATUSES.ts +0 -13
  285. package/src/lib/shared/hasReplayableRequest.ts +0 -17
  286. package/src/lib/shared/hydratingSlot.ts +0 -12
  287. package/src/lib/shared/outboxProbeSlot.ts +0 -20
  288. package/src/lib/shared/types/Outbox.ts +0 -9
  289. package/src/lib/shared/types/OutboxEntry.ts +0 -27
  290. package/src/lib/shared/types/SmartReadOptions.ts +0 -36
  291. package/src/lib/shared/wakeHydrationPeeks.ts +0 -20
  292. package/src/lib/ui/COMPONENT_WRAPPER_PREFIX.ts +0 -5
  293. package/src/lib/ui/compile/REACTIVE_CALLEES.ts +0 -11
  294. package/src/lib/ui/compile/assertRuntimeHelpersBound.ts +0 -80
  295. package/src/lib/ui/compile/markupTokens.ts +0 -313
  296. package/src/lib/ui/compile/structuralBlockTokens.ts +0 -100
  297. package/src/lib/ui/dom/applyResolved.ts +0 -87
  298. package/src/lib/ui/dom/mutateDocArray.ts +0 -38
  299. package/src/lib/ui/dom/scopeLabel.ts +0 -21
  300. package/src/lib/ui/dom/text.ts +0 -20
  301. package/src/lib/ui/history.ts +0 -108
  302. package/src/lib/ui/installHotBridge.ts +0 -95
  303. package/src/lib/ui/installInspectorBridge.ts +0 -140
  304. package/src/lib/ui/outbox.ts +0 -35
  305. package/src/lib/ui/persist.ts +0 -115
  306. package/src/lib/ui/rpcOutbox/createOutboxQueue.ts +0 -281
  307. package/src/lib/ui/rpcOutbox/outboxRegistry.ts +0 -69
  308. package/src/lib/ui/runtime/PATCH_BUS.ts +0 -28
  309. package/src/lib/ui/runtime/captureModelDoc.ts +0 -43
  310. package/src/lib/ui/runtime/hotInstances.ts +0 -10
  311. package/src/lib/ui/runtime/hotReloadEnabled.ts +0 -8
  312. package/src/lib/ui/runtime/hotReplace.ts +0 -38
  313. package/src/lib/ui/runtime/liveScopes.ts +0 -15
  314. package/src/lib/ui/runtime/localStoragePersistence.ts +0 -47
  315. package/src/lib/ui/runtime/registerHotInstance.ts +0 -23
  316. package/src/lib/ui/runtime/seedModelDoc.ts +0 -26
  317. package/src/lib/ui/runtime/types/HotInstance.ts +0 -22
  318. package/src/lib/ui/runtime/types/PatchEvent.ts +0 -16
  319. package/src/lib/ui/seedResolved.ts +0 -28
  320. package/src/lib/ui/sync.ts +0 -48
  321. package/src/lib/ui/types/History.ts +0 -14
  322. package/src/lib/ui/types/PersistHandle.ts +0 -11
  323. package/src/lib/ui/types/PersistenceStore.ts +0 -12
  324. package/src/lib/ui/types/ResolvedFrame.ts +0 -15
  325. package/src/lib/ui/types/SyncTransport.ts +0 -13
@@ -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,8 +34,12 @@ 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
@@ -43,13 +51,41 @@ export async function renderChain(
43
51
  for (const key of Object.keys(params)) {
44
52
  paramThunks[key] = () => params[key]
45
53
  }
46
- for (let index = 0; index < views.length; index += 1) {
47
- const view = views[index] as UiComponent
48
- const hasChild = index < views.length - 1
49
- const props: UiProps = hasChild
50
- ? { ...paramThunks, children: () => CHILD_PRESENT }
51
- : paramThunks
52
- 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
+ )
53
89
  }
54
90
  let html = renders[renders.length - 1]?.html ?? ''
55
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),
@@ -1,7 +1,6 @@
1
- import { hydratingSlot } from '../shared/hydratingSlot.ts'
1
+ import { hydrationWindow } from '../shared/hydrationWindow.ts'
2
2
  import { layoutChainForRoute } from '../shared/layoutChainForRoute.ts'
3
3
  import { matchRoute } from '../shared/matchRoute.ts'
4
- import { wakeHydrationPeeks } from '../shared/wakeHydrationPeeks.ts'
5
4
  import { fillBoundary } from './dom/fillBoundary.ts'
6
5
  import { outlet } from './dom/outlet.ts'
7
6
  import { effect } from './effect.ts'
@@ -20,6 +19,7 @@ import type { Route } from './runtime/types/Route.ts'
20
19
  import type { RouteLoader } from './runtime/types/RouteLoader.ts'
21
20
  import type { UiProps } from './runtime/types/UiProps.ts'
22
21
  import { untrack } from './runtime/untrack.ts'
22
+ import { withPath } from './runtime/withPath.ts'
23
23
 
24
24
  /* An outlet boundary — the `<!--abide:outlet-->`…`<!--/abide:outlet-->` marker pair a
25
25
  layer's content lives between (a layout's `<slot/>`, or the router's root boundary in
@@ -220,15 +220,17 @@ export function router(
220
220
  for (let depth = index; depth < layoutViews.length; depth += 1) {
221
221
  const view = layoutViews[depth] as Route
222
222
  PENDING_OUTLET.current = undefined
223
- const { dispose } = fillBoundary(
224
- boundary.open,
225
- boundary.close,
226
- view.build,
227
- /* A layout always has a child below: a deeper layout, or the page. */
228
- propsBag(depth < layoutViews.length - 1 || pageView !== undefined),
229
- /* The layout's route key names its scope in the inspector's Reactive tab
230
- (no host element to read a tag from — see `scopeLabel`). */
231
- chainKeys[depth],
223
+ /* Root this layer's render-path at its route key (the directory URL — stable
224
+ across reloads and globally unique), so every scope/cell it builds gets a
225
+ serialization-stable id under it (`withPath`). */
226
+ const { dispose } = withPath(chainKeys[depth] as string, () =>
227
+ fillBoundary(
228
+ boundary.open,
229
+ boundary.close,
230
+ view.build,
231
+ /* A layout always has a child below: a deeper layout, or the page. */
232
+ propsBag(depth < layoutViews.length - 1 || pageView !== undefined),
233
+ ),
232
234
  )
233
235
  const slot = PENDING_OUTLET.current
234
236
  if (slot === undefined) {
@@ -241,34 +243,32 @@ export function router(
241
243
  mountedPageKey = undefined
242
244
  return
243
245
  }
244
- disposePage = fillBoundary(
245
- boundary.open,
246
- boundary.close,
247
- pageView.build,
248
- /* A page is a leaf — no child layer. */
249
- propsBag(false),
250
- /* The page's route key names its scope in the inspector (see above). */
251
- pageKey,
246
+ /* Root the page's render-path at its route key (stable, unique) — see the layout
247
+ layer above. */
248
+ disposePage = withPath(pageKey, () =>
249
+ fillBoundary(
250
+ boundary.open,
251
+ boundary.close,
252
+ pageView.build,
253
+ /* A page is a leaf — no child layer. */
254
+ propsBag(false),
255
+ ),
252
256
  ).dispose
253
257
  mountedPageKey = pageKey
254
258
  }
255
259
  if (hydrating) {
256
260
  const previous = RENDER.hydration
257
- const previousHydrating = hydratingSlot.active
258
261
  RENDER.hydration = { next: new Map() }
259
- hydratingSlot.active = true
262
+ hydrationWindow.enter()
260
263
  enterRenderPass()
261
264
  try {
262
265
  run()
263
266
  } finally {
264
267
  exitRenderPass()
265
268
  RENDER.hydration = previous
266
- hydratingSlot.active = previousHydrating
267
- /* Wake the peeks this pass withheld for SSR congruence, now that the pass is
268
- over and the warm value is congruent to show. Only on the outermost unwind. */
269
- if (!previousHydrating) {
270
- wakeHydrationPeeks()
271
- }
269
+ /* Outermost exit clears the window and wakes the peeks this pass withheld
270
+ for SSR congruence, now that the warm value is congruent to show. */
271
+ hydrationWindow.exit()
272
272
  }
273
273
  return
274
274
  }
@@ -532,15 +532,42 @@ export function router(
532
532
  disposeFrom(divergence)
533
533
  const url = resolveUrl(path)
534
534
  clientPage.value = { route: chainRoute, params, url, navigating: false }
535
- buildFrom(
536
- divergence,
537
- chainKeys,
538
- layoutViews,
539
- pageView,
540
- key,
541
- params,
542
- hydrating,
543
- )
535
+ try {
536
+ buildFrom(
537
+ divergence,
538
+ chainKeys,
539
+ layoutViews,
540
+ pageView,
541
+ key,
542
+ params,
543
+ hydrating,
544
+ )
545
+ } catch (error) {
546
+ /* A cold mount owns no claim cursor, so the SSR-vs-client claim asserts
547
+ (`assertClaimedText`, `claimExpected`) can't fire there — a throw from
548
+ it is a genuine codegen/user-render defect. Rethrow to `commit`, which
549
+ surfaces it and stops (a reload would re-run the same failure). */
550
+ if (!hydrating) {
551
+ throw error
552
+ }
553
+ /* A HYDRATING first paint threw — a server↔client markup divergence (a
554
+ blocking cell whose resolved value the server couldn't serialize into
555
+ `__SSR__.cells`, so the client reads pending where SSR baked a value;
556
+ or any other SSR/client mismatch). A reload re-runs the same SSR, so
557
+ recover CLIENT-side: discard the server markup and mount the page fresh
558
+ (cold — pending reads render their empty state and fill in on settle,
559
+ congruent because nothing is being adopted). Reset to the pristine
560
+ pre-mount state so `run` re-establishes the root boundary and clears the
561
+ host, then rebuild the whole chain cold. */
562
+ console.warn(
563
+ `[abide] hydration mismatch at ${path} — discarding server markup and rendering on the client:`,
564
+ error,
565
+ )
566
+ disposeFrom(0)
567
+ rootBoundary = undefined
568
+ mountedPageKey = undefined
569
+ buildFrom(0, chainKeys, layoutViews, pageView, key, params, false)
570
+ }
544
571
  /* Reapply the destination entry's scroll once its DOM exists — a
545
572
  back/forward restores its offset, a fresh nav scrolls to the `#hash`
546
573
  anchor (now built) or the top. SKIPPED on a hydrating first paint: