@abide/abide 0.49.0 → 0.50.1
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.
- package/AGENTS.md +564 -437
- package/CHANGELOG.md +183 -0
- package/README.md +165 -202
- package/package.json +14 -5
- package/src/abideLsp.ts +5 -6
- package/src/abideResolverPlugin.ts +197 -169
- package/src/build.ts +79 -56
- package/src/buildDisconnected.ts +0 -2
- package/src/checkAbide.ts +12 -2
- package/src/devEntry.ts +113 -28
- package/src/discoveryEntry.ts +3 -2
- package/src/lib/bundle/disconnected.abide +69 -73
- package/src/lib/bundle/infoPlist.ts +13 -7
- package/src/lib/bundle/spawnEmbeddedServer.ts +13 -4
- package/src/lib/bundle/waitForServer.ts +6 -1
- package/src/lib/mcp/mcpTools.ts +15 -6
- package/src/lib/server/error.ts +6 -6
- package/src/lib/server/json.ts +17 -3
- package/src/lib/server/rpc/defineRpc.ts +58 -24
- package/src/lib/server/rpc/parseArgs.ts +98 -13
- package/src/lib/server/rpc/resolveRpcJsonSchema.ts +28 -0
- package/src/lib/server/rpc/types/RpcHelper.ts +75 -105
- package/src/lib/server/rpc/types/RpcRegistryEntry.ts +24 -9
- package/src/lib/server/rpc/validationError.ts +1 -1
- package/src/lib/server/runtime/DEV_RELOAD_CLIENT_SCRIPT.ts +0 -15
- package/src/lib/server/runtime/buildCacheSnapshot.ts +10 -9
- package/src/lib/server/runtime/buildInspectorSurface.ts +6 -4
- package/src/lib/server/runtime/buildOpenApiSpec.ts +55 -11
- package/src/lib/server/runtime/buildPreloadManifest.ts +12 -10
- package/src/lib/server/runtime/createAppAssetServer.ts +18 -15
- package/src/lib/server/runtime/createAppRouteResolver.ts +145 -0
- package/src/lib/server/runtime/createPlumbingRouter.ts +212 -0
- package/src/lib/server/runtime/createRouteDispatcher.ts +4 -10
- package/src/lib/server/runtime/createServer.ts +120 -324
- package/src/lib/server/runtime/createUiPageRenderer.ts +137 -26
- package/src/lib/server/runtime/devClientFingerprint.ts +19 -34
- package/src/lib/server/runtime/installAmbientScopeStore.ts +30 -9
- package/src/lib/server/runtime/logExposedSurfaces.ts +8 -7
- package/src/lib/server/runtime/pathStore.ts +15 -0
- package/src/lib/server/runtime/registryManifests.ts +1 -1
- package/src/lib/server/runtime/renderCellBarrierStore.ts +15 -0
- package/src/lib/server/runtime/runWithRequestScope.ts +3 -0
- package/src/lib/server/runtime/serializeCacheSnapshot.ts +6 -4
- package/src/lib/server/runtime/snapshotEntryFromCache.ts +14 -14
- package/src/lib/server/runtime/streamCacheResolutions.ts +14 -7
- package/src/lib/server/runtime/streamFromIterator.ts +30 -2
- package/src/lib/server/runtime/textResponse.ts +23 -0
- package/src/lib/server/runtime/types/DevReloadStamp.ts +6 -10
- package/src/lib/server/runtime/types/InspectorCacheEntry.ts +1 -1
- package/src/lib/server/runtime/types/RequestStore.ts +27 -0
- package/src/lib/server/runtime/warnUnguardedMcp.ts +15 -5
- package/src/lib/server/sockets/createSocketDispatcher.ts +9 -11
- package/src/lib/server/sockets/defineSocket.ts +5 -4
- package/src/lib/server/sse.ts +5 -1
- package/src/lib/shared/ASYNC_CELL.ts +5 -0
- package/src/lib/shared/DEV_REBUILD_PATH.ts +6 -0
- package/src/lib/shared/HTTP_METHODS.ts +6 -0
- package/src/lib/shared/HttpError.ts +1 -5
- package/src/lib/shared/MCP_PATH.ts +6 -0
- package/src/lib/shared/PROXIED_SERVER_SUBDIRS.ts +15 -0
- package/src/lib/shared/REF_JSON_TAGS.ts +13 -1
- package/src/lib/shared/RPC_SHIM_GLOBALS.ts +9 -0
- package/src/lib/shared/activeCacheStore.ts +1 -0
- package/src/lib/shared/activePage.ts +1 -0
- package/src/lib/shared/buildArtifact.ts +4 -1
- package/src/lib/shared/buildSocketOverChannel.ts +4 -3
- package/src/lib/shared/bundleGraphFromMetafile.ts +68 -0
- package/src/lib/shared/cache.ts +156 -131
- package/src/lib/shared/changeAffectsClient.ts +12 -7
- package/src/lib/shared/createCacheStore.ts +31 -8
- package/src/lib/shared/createLifecycleChannel.ts +7 -1
- package/src/lib/shared/createReachable.ts +47 -78
- package/src/lib/shared/createRemoteFunction.ts +52 -30
- package/src/lib/shared/createRpcServerProgram.ts +820 -0
- package/src/lib/shared/decodeRefJson.ts +4 -4
- package/src/lib/shared/decodeResponse.ts +2 -2
- package/src/lib/shared/decodeWireBody.ts +35 -0
- package/src/lib/shared/detectRpcMethod.ts +8 -1
- package/src/lib/shared/done.ts +8 -2
- package/src/lib/shared/encodeRefJson.ts +4 -4
- package/src/lib/shared/encodeWireBody.ts +18 -0
- package/src/lib/{server/rpc → shared}/fieldErrorsFromIssues.ts +5 -1
- package/src/lib/shared/generateDeclarations.ts +72 -0
- package/src/lib/shared/hasSeedableRequest.ts +25 -0
- package/src/lib/shared/hydrationWindow.ts +53 -0
- package/src/lib/shared/isAsyncCell.ts +11 -0
- package/src/lib/shared/isAsyncIterable.ts +8 -0
- package/src/lib/shared/isSubscribable.ts +3 -3
- package/src/lib/shared/isThenable.ts +9 -0
- package/src/lib/shared/jsonSchemaForType.ts +258 -0
- package/src/lib/shared/lenientDecode.ts +15 -0
- package/src/lib/shared/loadProjectTsConfig.ts +28 -0
- package/src/lib/shared/markFrameworkSourcesIgnored.ts +1 -1
- package/src/lib/shared/matchRoute.ts +4 -14
- package/src/lib/shared/peek.ts +14 -0
- package/src/lib/shared/pending.ts +9 -6
- package/src/lib/shared/pendingAsyncCellsSlot.ts +13 -0
- package/src/lib/shared/prepareRpcModule.ts +91 -93
- package/src/lib/shared/prepareSocketModule.ts +3 -2
- package/src/lib/shared/probeRegistries.ts +5 -18
- package/src/lib/shared/reachable.ts +7 -10
- package/src/lib/shared/refresh.ts +12 -2
- package/src/lib/shared/refreshing.ts +8 -2
- package/src/lib/shared/resolvedCellsSlot.ts +13 -0
- package/src/lib/shared/reviveWireField.ts +49 -0
- package/src/lib/shared/reviveWireOutput.ts +36 -0
- package/src/lib/shared/rpcServerForRoot.ts +25 -0
- package/src/lib/shared/scanPages.ts +25 -0
- package/src/lib/shared/snapshotShippable.ts +8 -7
- package/src/lib/shared/streamedCellsSlot.ts +14 -0
- package/src/lib/shared/subscribableFromResponse.ts +4 -4
- package/src/lib/shared/subscribableProbes.ts +1 -1
- package/src/lib/shared/tailProbeSlot.ts +1 -1
- package/src/lib/shared/types/AsyncComputed.ts +20 -0
- package/src/lib/shared/types/AsyncState.ts +13 -0
- package/src/lib/shared/types/CacheEntry.ts +7 -7
- package/src/lib/shared/types/CacheOptions.ts +23 -37
- package/src/lib/shared/types/CachePolicy.ts +25 -0
- package/src/lib/shared/types/CacheSnapshotEntry.ts +6 -5
- package/src/lib/shared/types/ErrorJsonSchemas.ts +8 -0
- package/src/lib/shared/types/HttpMethod.ts +3 -1
- package/src/lib/shared/types/InputCoercion.ts +17 -0
- package/src/lib/shared/types/{Subscribable.ts → NamedAsyncIterable.ts} +1 -1
- package/src/lib/shared/types/OutputWirePlan.ts +17 -0
- package/src/lib/shared/types/PagesScan.ts +7 -0
- package/src/lib/shared/types/PendingAsyncCells.ts +10 -0
- package/src/lib/shared/types/RawRemoteFunction.ts +6 -0
- package/src/lib/shared/types/RemoteCallable.ts +8 -7
- package/src/lib/shared/types/RemoteFunction.ts +16 -15
- package/src/lib/shared/types/ResolvedCells.ts +11 -0
- package/src/lib/shared/types/ReturnBody.ts +15 -0
- package/src/lib/shared/types/RpcBuildStamps.ts +23 -0
- package/src/lib/shared/types/RpcErrorGuard.ts +3 -8
- package/src/lib/shared/types/Socket.ts +4 -4
- package/src/lib/shared/types/SsrPayload.ts +5 -1
- package/src/lib/shared/types/StreamPolicy.ts +11 -0
- package/src/lib/shared/types/StreamedCells.ts +19 -0
- package/src/lib/shared/types/StreamedResolution.ts +24 -6
- package/src/lib/shared/types/TailHooks.ts +1 -1
- package/src/lib/shared/types/WireKind.ts +11 -0
- package/src/lib/shared/validationHttpError.ts +33 -0
- package/src/lib/shared/warmSeedKey.ts +16 -0
- package/src/lib/shared/wireJsonReplacer.ts +30 -0
- package/src/lib/shared/writeRpcDts.ts +11 -1
- package/src/lib/shared/writeTestSocketsDts.ts +1 -1
- package/src/lib/test/createTestApp.ts +19 -1
- package/src/lib/ui/README.md +1 -1
- package/src/lib/ui/activePendingCells.ts +14 -0
- package/src/lib/ui/compile/BLOCK_KEYWORDS.ts +21 -0
- package/src/lib/ui/compile/SSR_ESCAPE.ts +4 -1
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +35 -8
- package/src/lib/ui/compile/abideUiPlugin.ts +19 -2
- package/src/lib/ui/compile/analyzeComponent.ts +89 -6
- package/src/lib/ui/compile/assignmentTargetNames.ts +54 -0
- package/src/lib/ui/compile/asyncInterpolationFields.ts +152 -0
- package/src/lib/ui/compile/asyncValuePositionError.ts +21 -0
- package/src/lib/ui/compile/asyncValuePositionInterpolations.ts +64 -0
- package/src/lib/ui/compile/attrLiftPosition.ts +18 -0
- package/src/lib/ui/compile/cachedSourceFile.ts +40 -0
- package/src/lib/ui/compile/catchBinding.ts +7 -5
- package/src/lib/ui/compile/classifyInterpolationType.ts +46 -0
- package/src/lib/ui/compile/collectAbideDiagnostics.ts +96 -0
- package/src/lib/ui/compile/compileComponent.ts +19 -3
- package/src/lib/ui/compile/compileModule.ts +27 -58
- package/src/lib/ui/compile/compileSSR.ts +51 -12
- package/src/lib/ui/compile/compileShadow.ts +145 -18
- package/src/lib/ui/compile/composeProps.ts +12 -2
- package/src/lib/ui/compile/createShadowLanguageService.ts +116 -68
- package/src/lib/ui/compile/createShadowProgram.ts +35 -6
- package/src/lib/ui/compile/declaredNames.ts +36 -0
- package/src/lib/ui/compile/desugarSignals.ts +418 -36
- package/src/lib/ui/compile/expressionIsPrefixEvaluable.ts +19 -0
- package/src/lib/ui/compile/generateBuild.ts +52 -9
- package/src/lib/ui/compile/generateSSR.ts +242 -43
- package/src/lib/ui/compile/hoistableAwaits.ts +193 -0
- package/src/lib/ui/compile/hoistableChildRenders.ts +112 -0
- package/src/lib/ui/compile/interpolatedTemplateLiteral.ts +3 -1
- package/src/lib/ui/compile/interpolationClassifierForRoot.ts +37 -0
- package/src/lib/ui/compile/isSpuriousAsyncReadDiagnostic.ts +174 -0
- package/src/lib/ui/compile/liftAsyncSubExpressions.ts +166 -0
- package/src/lib/ui/compile/lowerAsyncInterpolations.ts +92 -0
- package/src/lib/ui/compile/lowerContext.ts +10 -0
- package/src/lib/ui/compile/lowerDocAccess.ts +26 -23
- package/src/lib/ui/compile/lowerScript.ts +40 -4
- package/src/lib/ui/compile/nodeAtShadowOffset.ts +28 -0
- package/src/lib/ui/compile/parseTemplate.ts +18 -1092
- package/src/lib/ui/compile/parseTemplateRecovering.ts +1385 -0
- package/src/lib/ui/compile/referencedIdentifiers.ts +35 -0
- package/src/lib/ui/compile/renameSignalRefs.ts +21 -22
- package/src/lib/ui/compile/seedTypeClassifierForRoot.ts +65 -0
- package/src/lib/ui/compile/shadowInterpolationClassifier.ts +47 -0
- package/src/lib/ui/compile/sourceFileOptionsSignature.ts +20 -0
- package/src/lib/ui/compile/structuralHeadTokens.ts +107 -0
- package/src/lib/ui/compile/templateSemanticTokens.ts +21 -0
- package/src/lib/ui/compile/templateStartOffset.ts +15 -0
- package/src/lib/ui/compile/tryPlan.ts +4 -3
- package/src/lib/ui/compile/types/AnalyzedComponent.ts +4 -0
- package/src/lib/ui/compile/types/AsyncInterpolationField.ts +19 -0
- package/src/lib/ui/compile/types/InterpolationClassifier.ts +12 -0
- package/src/lib/ui/compile/types/InterpolationKind.ts +7 -0
- package/src/lib/ui/compile/types/ParseDiagnostic.ts +8 -0
- package/src/lib/ui/compile/types/SeedTypeClassifier.ts +15 -0
- package/src/lib/ui/compile/types/TemplateNode.ts +36 -4
- package/src/lib/ui/compile/types/ValuePositionInterpolation.ts +13 -0
- package/src/lib/ui/compile/writtenTemplateNames.ts +84 -0
- package/src/lib/ui/computed.ts +28 -1
- package/src/lib/ui/createScope.ts +35 -68
- package/src/lib/ui/dom/anchoredBranch.ts +142 -0
- package/src/lib/ui/dom/appendText.ts +8 -3
- package/src/lib/ui/dom/awaitBlock.ts +43 -94
- package/src/lib/ui/dom/bindProp.ts +22 -0
- package/src/lib/ui/dom/bindableProp.ts +47 -0
- package/src/lib/ui/dom/cellPending.ts +24 -0
- package/src/lib/ui/dom/disposeRange.ts +2 -1
- package/src/lib/ui/dom/each.ts +26 -4
- package/src/lib/ui/dom/eachAsync.ts +39 -3
- package/src/lib/ui/dom/fillBoundary.ts +2 -3
- package/src/lib/ui/dom/fillRange.ts +4 -4
- package/src/lib/ui/dom/hydrate.ts +6 -12
- package/src/lib/ui/dom/isComment.ts +1 -1
- package/src/lib/ui/dom/matchingRangeClose.ts +29 -0
- package/src/lib/ui/dom/mount.ts +1 -2
- package/src/lib/ui/dom/mountChild.ts +9 -40
- package/src/lib/ui/dom/mountRange.ts +2 -3
- package/src/lib/ui/dom/mountStreamedChild.ts +84 -0
- package/src/lib/ui/dom/mountSwappableRange.ts +46 -4
- package/src/lib/ui/dom/mutateDocContainer.ts +65 -0
- package/src/lib/ui/dom/on.ts +2 -2
- package/src/lib/ui/dom/readCell.ts +40 -0
- package/src/lib/ui/dom/switchBlock.ts +30 -7
- package/src/lib/ui/dom/tryBlock.ts +230 -66
- package/src/lib/ui/dom/types/SwitchCase.ts +5 -1
- package/src/lib/ui/dom/when.ts +12 -2
- package/src/lib/ui/dom/withScope.ts +2 -2
- package/src/lib/ui/finalizeStreamedChildren.ts +89 -0
- package/src/lib/ui/flight.ts +56 -0
- package/src/lib/ui/html.ts +12 -1
- package/src/lib/ui/isolateCellBarrier.ts +17 -0
- package/src/lib/ui/linked.ts +48 -2
- package/src/lib/ui/remoteProxy.ts +84 -159
- package/src/lib/ui/renderChain.ts +49 -13
- package/src/lib/ui/renderToStream.ts +22 -18
- package/src/lib/ui/resumeSeedScript.ts +1 -1
- package/src/lib/ui/router.ts +63 -36
- package/src/lib/ui/runtime/AsyncCellError.ts +20 -0
- package/src/lib/ui/runtime/CELL_SEED.ts +14 -0
- package/src/lib/ui/runtime/CURRENT_BOUNDARY.ts +11 -0
- package/src/lib/ui/runtime/CURRENT_PATH.ts +29 -0
- package/src/lib/ui/runtime/RENDER.ts +10 -7
- package/src/lib/ui/runtime/RESUME.ts +4 -4
- package/src/lib/ui/runtime/STREAMED_CELLS.ts +57 -0
- package/src/lib/ui/runtime/ambientPathBacking.ts +32 -0
- package/src/lib/ui/runtime/applyPatchToTree.ts +1 -1
- package/src/lib/ui/runtime/blockId.ts +31 -0
- package/src/lib/ui/runtime/boundaryFor.ts +11 -0
- package/src/lib/ui/runtime/cellBarrierBacking.ts +30 -0
- package/src/lib/ui/runtime/createAsyncCell.ts +300 -0
- package/src/lib/ui/runtime/createDoc.ts +3 -42
- package/src/lib/ui/runtime/createEffectNode.ts +9 -0
- package/src/lib/ui/runtime/enterRenderPass.ts +5 -4
- package/src/lib/ui/runtime/flushEffects.ts +16 -6
- package/src/lib/ui/runtime/isAsyncFunction.ts +14 -0
- package/src/lib/ui/runtime/nextBlockId.ts +8 -7
- package/src/lib/ui/runtime/renderPath.ts +16 -0
- package/src/lib/ui/runtime/types/Boundary.ts +9 -0
- package/src/lib/ui/runtime/types/RenderContext.ts +9 -7
- package/src/lib/ui/runtime/types/SsrRender.ts +9 -2
- package/src/lib/ui/runtime/types/UiComponent.ts +1 -5
- package/src/lib/ui/runtime/withOptionalPath.ts +8 -0
- package/src/lib/ui/runtime/withPath.ts +16 -0
- package/src/lib/ui/runtime/withPathFrom.ts +20 -0
- package/src/lib/ui/seedStreamedResolution.ts +31 -6
- package/src/lib/ui/settleAsyncCells.ts +24 -0
- package/src/lib/ui/socketProxy.ts +1 -1
- package/src/lib/ui/startClient.ts +16 -31
- package/src/lib/ui/trackedComputed.ts +68 -0
- package/src/lib/ui/types/Scope.ts +8 -24
- package/src/lib/ui/watch.ts +3 -3
- package/src/serverEntry.ts +14 -0
- package/template/src/server/rpc/getHello.ts +15 -13
- package/template/test/app.test.ts +1 -1
- package/src/lib/server/runtime/devHotModuleResponse.ts +0 -41
- package/src/lib/shared/DEV_HOT_PREFIX.ts +0 -7
- package/src/lib/shared/UNREACHABLE_STATUSES.ts +0 -13
- package/src/lib/shared/hasReplayableRequest.ts +0 -17
- package/src/lib/shared/hydratingSlot.ts +0 -12
- package/src/lib/shared/outboxProbeSlot.ts +0 -20
- package/src/lib/shared/types/Outbox.ts +0 -9
- package/src/lib/shared/types/OutboxEntry.ts +0 -27
- package/src/lib/shared/types/SmartReadOptions.ts +0 -36
- package/src/lib/shared/wakeHydrationPeeks.ts +0 -20
- package/src/lib/ui/COMPONENT_WRAPPER_PREFIX.ts +0 -5
- package/src/lib/ui/compile/REACTIVE_CALLEES.ts +0 -11
- package/src/lib/ui/compile/assertRuntimeHelpersBound.ts +0 -80
- package/src/lib/ui/compile/markupTokens.ts +0 -313
- package/src/lib/ui/compile/structuralBlockTokens.ts +0 -100
- package/src/lib/ui/dom/applyResolved.ts +0 -87
- package/src/lib/ui/dom/mutateDocArray.ts +0 -38
- package/src/lib/ui/dom/scopeLabel.ts +0 -21
- package/src/lib/ui/dom/text.ts +0 -20
- package/src/lib/ui/history.ts +0 -108
- package/src/lib/ui/installHotBridge.ts +0 -95
- package/src/lib/ui/installInspectorBridge.ts +0 -140
- package/src/lib/ui/outbox.ts +0 -35
- package/src/lib/ui/persist.ts +0 -115
- package/src/lib/ui/rpcOutbox/createOutboxQueue.ts +0 -281
- package/src/lib/ui/rpcOutbox/outboxRegistry.ts +0 -69
- package/src/lib/ui/runtime/PATCH_BUS.ts +0 -28
- package/src/lib/ui/runtime/captureModelDoc.ts +0 -43
- package/src/lib/ui/runtime/hotInstances.ts +0 -10
- package/src/lib/ui/runtime/hotReloadEnabled.ts +0 -8
- package/src/lib/ui/runtime/hotReplace.ts +0 -38
- package/src/lib/ui/runtime/liveScopes.ts +0 -15
- package/src/lib/ui/runtime/localStoragePersistence.ts +0 -47
- package/src/lib/ui/runtime/registerHotInstance.ts +0 -23
- package/src/lib/ui/runtime/seedModelDoc.ts +0 -26
- package/src/lib/ui/runtime/types/HotInstance.ts +0 -22
- package/src/lib/ui/runtime/types/PatchEvent.ts +0 -16
- package/src/lib/ui/seedResolved.ts +0 -28
- package/src/lib/ui/sync.ts +0 -48
- package/src/lib/ui/types/History.ts +0 -14
- package/src/lib/ui/types/PersistHandle.ts +0 -11
- package/src/lib/ui/types/PersistenceStore.ts +0 -12
- package/src/lib/ui/types/ResolvedFrame.ts +0 -15
- package/src/lib/ui/types/SyncTransport.ts +0 -13
|
@@ -1,101 +1,265 @@
|
|
|
1
|
+
import { effect } from '../effect.ts'
|
|
2
|
+
import { AsyncCellError } from '../runtime/AsyncCellError.ts'
|
|
3
|
+
import { CURRENT_BOUNDARY } from '../runtime/CURRENT_BOUNDARY.ts'
|
|
1
4
|
import { claimExpected } from '../runtime/claimExpected.ts'
|
|
2
5
|
import { OWNER } from '../runtime/OWNER.ts'
|
|
3
6
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
4
7
|
import { scopeGroup } from '../runtime/scopeGroup.ts'
|
|
8
|
+
import type { Boundary } from '../runtime/types/Boundary.ts'
|
|
9
|
+
import type { State } from '../runtime/types/State.ts'
|
|
5
10
|
import { withoutHydration } from '../runtime/withoutHydration.ts'
|
|
11
|
+
import { state } from '../state.ts'
|
|
12
|
+
import { anchoredBranch } from './anchoredBranch.ts'
|
|
6
13
|
import { discardBoundary } from './discardBoundary.ts'
|
|
7
|
-
import { enterNamespace } from './enterNamespace.ts'
|
|
8
14
|
|
|
9
15
|
/*
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
propagates to the nearest enclosing boundary. The block renders once and never
|
|
16
|
-
re-renders, so its content needs no range markers — an enclosing block's range
|
|
17
|
-
removes it on teardown.
|
|
16
|
+
Reactive error boundary — the runtime for `<template try>` (ADR-0019 D3). Unlike the old
|
|
17
|
+
synchronous render-once version it is render-MANY and reactive both directions: it catches
|
|
18
|
+
a build throw AND an initial reactive-read throw (as before) AND — the headline capability
|
|
19
|
+
— a throw from a LATER re-run, where an async error lives (an async cell that rejects after
|
|
20
|
+
mount, read via a throwing peek → `AsyncCellError`).
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
builds
|
|
22
|
-
(
|
|
23
|
-
|
|
22
|
+
Each branch's content lives in a `[`…`]`-bracketed RANGE (the model `when`/`await`/`each`
|
|
23
|
+
share) parked before an anchor, so a swap detaches one unit and builds the other. The
|
|
24
|
+
guarded branch builds under an ambient `CURRENT_BOUNDARY` so every effect created inside
|
|
25
|
+
associates with this boundary (`boundaryFor`); when such an effect throws on a re-run the
|
|
26
|
+
scheduler (`flushEffects.drain`) calls `boundary.handle(error)` — which swaps to the catch
|
|
27
|
+
branch. No `renderCatch` (no `<template catch>`) means no boundary is installed, so a later
|
|
28
|
+
throw propagates to the nearest ENCLOSING `{#try}` unchanged, and an initial throw rethrows.
|
|
29
|
+
|
|
30
|
+
Keep-the-watch: the terminal alternative would dispose the guarded scope on throw, killing
|
|
31
|
+
the only subscription to the failing cell — the boundary would go deaf to recovery. So the
|
|
32
|
+
throw carries the originating cell (`AsyncCellError.cell`); on catching an async-cell error
|
|
33
|
+
the boundary subscribes to that cell's lifecycle and, when it recovers (error→value via a
|
|
34
|
+
`refresh()` or a dep change), rebuilds the guarded content fresh — one rebuild, then
|
|
35
|
+
fine-grained. On catch→catch (a fresh error while still failing) the catch branch rebuilds
|
|
36
|
+
with the new error (the codegen catch binding is a plain local, so the error passes by
|
|
37
|
+
value; in-place `err` update is a v2 refinement).
|
|
38
|
+
|
|
39
|
+
Hydration claims the SSR boundary (`<!--abide:try:N-->…<!--/abide:try:N-->`): the happy
|
|
40
|
+
path adopts the guarded nodes in place and brackets them into a range (so the first swap
|
|
41
|
+
detaches them like any later one); a throw discards the server nodes and builds the catch
|
|
42
|
+
fresh.
|
|
24
43
|
*/
|
|
25
44
|
// @documentation plumbing
|
|
26
45
|
export function tryBlock(
|
|
27
46
|
parent: Node,
|
|
28
|
-
id:
|
|
47
|
+
id: string,
|
|
29
48
|
renderTry: (parent: Node) => void,
|
|
30
|
-
renderCatch?: (parent: Node, error: unknown) => void,
|
|
49
|
+
renderCatch?: (parent: Node, error: State<unknown>) => void,
|
|
31
50
|
before: Node | null = null,
|
|
32
51
|
): void {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
52
|
+
const hydration = RENDER.hydration
|
|
53
|
+
/* The live branch's scope, registered with the owner so it disposes on owner teardown —
|
|
54
|
+
not only when a swap detaches it. */
|
|
36
55
|
const group = scopeGroup()
|
|
37
|
-
/*
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
56
|
+
/* Only a block WITH a catch branch installs a boundary: with no catch it cannot handle a
|
|
57
|
+
throw, so its guarded effects must inherit the ENCLOSING boundary (a nested throw then
|
|
58
|
+
propagates outward) and an initial throw rethrows. */
|
|
59
|
+
const hasCatch = renderCatch !== undefined
|
|
60
|
+
/* The single active branch mounted before an anchor — owns `active`/`detach`/`place`/
|
|
61
|
+
adopt-strand guard (shared with `awaitBlock`); this block keeps its bespoke boundary
|
|
62
|
+
recovery. */
|
|
63
|
+
const branch = anchoredBranch(parent, group)
|
|
64
|
+
/* The keep-the-watch subscription on the throwing cell, live while the catch branch is
|
|
65
|
+
shown (async-cell errors only); disposed on recover / swap / teardown. */
|
|
66
|
+
let watchDispose: (() => void) | undefined
|
|
67
|
+
/* Which branch is mounted, and the reactive error cell the catch branch reads. A
|
|
68
|
+
catch→catch (a fresh error while still failing) WRITES this cell — the `err` binding
|
|
69
|
+
re-runs in place, no rebuild — mirroring how `awaitBlock` updates its then-value cell. */
|
|
70
|
+
let activeKind: 'try' | 'catch' | undefined
|
|
71
|
+
let errorCell: State<unknown> | undefined
|
|
72
|
+
|
|
73
|
+
const boundary: Boundary = { handle: (error) => showCatch(error) }
|
|
74
|
+
|
|
75
|
+
const disposeWatch = (): void => {
|
|
76
|
+
if (watchDispose !== undefined) {
|
|
77
|
+
watchDispose()
|
|
78
|
+
watchDispose = undefined
|
|
49
79
|
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* Install this boundary as the ambient one around the guarded branch's build (passed as
|
|
83
|
+
`place`'s `wrapBuild`) so its effects associate for later-throw routing; the catch branch
|
|
84
|
+
builds WITHOUT it, so a throw in catch content propagates to the enclosing boundary rather
|
|
85
|
+
than back to this one. */
|
|
86
|
+
const withBoundary = (run: () => void): void => {
|
|
87
|
+
const previousBoundary = CURRENT_BOUNDARY.current
|
|
88
|
+
CURRENT_BOUNDARY.current = boundary
|
|
50
89
|
try {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
} catch (error) {
|
|
55
|
-
OWNER.current = previous
|
|
56
|
-
disposeAll()
|
|
57
|
-
throw error
|
|
90
|
+
run()
|
|
91
|
+
} finally {
|
|
92
|
+
CURRENT_BOUNDARY.current = previousBoundary
|
|
58
93
|
}
|
|
59
94
|
}
|
|
60
95
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
96
|
+
/* Build (or rebuild) the guarded branch. A SYNCHRONOUS throw during the build — an
|
|
97
|
+
initial reactive-read throw or a plain build throw — falls to the catch branch (or
|
|
98
|
+
rethrows with no catch, preserving the old boundary's semantics). Later-run throws
|
|
99
|
+
arrive via `boundary.handle` instead. */
|
|
100
|
+
const showTry = (): void => {
|
|
101
|
+
disposeWatch()
|
|
67
102
|
try {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
103
|
+
branch.place((host) => renderTry(host), hasCatch ? withBoundary : undefined)
|
|
104
|
+
activeKind = 'try'
|
|
105
|
+
errorCell = undefined
|
|
71
106
|
} catch (error) {
|
|
72
|
-
|
|
73
|
-
catch fresh in its place. */
|
|
74
|
-
const after = discardBoundary(parent, open, `/abide:try:${id}`, hydration)
|
|
75
|
-
if (renderCatch === undefined) {
|
|
107
|
+
if (!hasCatch) {
|
|
76
108
|
throw error
|
|
77
109
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
110
|
+
showCatch(error)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* Swap to the catch branch. A catch→catch (a fresh error while the catch branch is
|
|
115
|
+
already mounted) updates the reactive `err` cell IN PLACE — the binding re-runs, no
|
|
116
|
+
rebuild, focus/scroll inside the catch branch survive. Otherwise (try→catch, or the
|
|
117
|
+
initial throw) it builds the catch branch fresh around a new error cell. Either way it
|
|
118
|
+
keeps a watch on the throwing async cell so a recovery re-arms the guarded branch. */
|
|
119
|
+
const showCatch = (error: unknown): void => {
|
|
120
|
+
if (activeKind === 'catch' && errorCell !== undefined) {
|
|
121
|
+
errorCell.value = error
|
|
122
|
+
rewatch(error)
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
const cell = state<unknown>(error)
|
|
126
|
+
errorCell = cell
|
|
127
|
+
branch.place((host) => (renderCatch as NonNullable<typeof renderCatch>)(host, cell))
|
|
128
|
+
activeKind = 'catch'
|
|
129
|
+
rewatch(error)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* (Re)subscribe the keep-the-watch to whatever cell threw this time. Only a reactive
|
|
133
|
+
async-cell error carries a source to watch; a plain render bug is terminal in v1 (a bug
|
|
134
|
+
is not a data state that self-heals). */
|
|
135
|
+
const rewatch = (error: unknown): void => {
|
|
136
|
+
disposeWatch()
|
|
137
|
+
if (error instanceof AsyncCellError) {
|
|
138
|
+
watchCell(error.cell)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/* Subscribe to the throwing cell's lifecycle. On recovery (its error clears or a value
|
|
143
|
+
arrives — error→value via `refresh()` or a dep change) rebuild the guarded branch
|
|
144
|
+
fresh; on a fresh error while still failing, rebuild catch with the new error. Created
|
|
145
|
+
OUTSIDE this boundary and the owner (it must not route its own reads here and must not
|
|
146
|
+
double-register), tracked in the group so it disposes with the block. */
|
|
147
|
+
const watchCell = (cell: AsyncCellError['cell']): void => {
|
|
148
|
+
const previousBoundary = CURRENT_BOUNDARY.current
|
|
149
|
+
const previousOwner = OWNER.current
|
|
150
|
+
CURRENT_BOUNDARY.current = undefined
|
|
151
|
+
OWNER.current = undefined
|
|
152
|
+
let firstRun = true
|
|
153
|
+
let dispose: () => void
|
|
154
|
+
try {
|
|
155
|
+
dispose = effect(() => {
|
|
156
|
+
const cellError = cell.error()
|
|
157
|
+
const value = cell.peek()
|
|
158
|
+
/* The first run only subscribes — the cell is still in its error state that
|
|
159
|
+
drove us here, so acting on it would immediately re-arm. */
|
|
160
|
+
if (firstRun) {
|
|
161
|
+
firstRun = false
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
/* Recovered: rebuild guarded fresh. Still failing with a fresh error: rebuild
|
|
165
|
+
catch with it. Either path disposes THIS watch first (safe: an effect fully
|
|
166
|
+
unlinks itself, so its own end-of-run tracking trims nothing). */
|
|
167
|
+
if (cellError === undefined || value !== undefined) {
|
|
168
|
+
showTry()
|
|
169
|
+
} else {
|
|
170
|
+
showCatch(new AsyncCellError(cell, cellError))
|
|
171
|
+
}
|
|
82
172
|
})
|
|
173
|
+
} finally {
|
|
174
|
+
CURRENT_BOUNDARY.current = previousBoundary
|
|
175
|
+
OWNER.current = previousOwner
|
|
83
176
|
}
|
|
177
|
+
watchDispose = group.track(dispose)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (hydration !== undefined) {
|
|
181
|
+
firstHydrate()
|
|
84
182
|
return
|
|
85
183
|
}
|
|
184
|
+
const anchorNode = document.createTextNode('')
|
|
185
|
+
branch.anchor = anchorNode
|
|
186
|
+
parent.insertBefore(anchorNode, before)
|
|
187
|
+
showTry()
|
|
188
|
+
|
|
189
|
+
/* The first run when hydrating: claim the boundary's open marker, adopt the guarded
|
|
190
|
+
server nodes in place, then bracket them into a range so the first swap detaches them
|
|
191
|
+
like every later one. A guarded throw (the server rendered catch, or the client build
|
|
192
|
+
throws too) discards the boundary and builds the catch fresh. */
|
|
193
|
+
function firstHydrate(): void {
|
|
194
|
+
const cursor = hydration as NonNullable<typeof hydration>
|
|
195
|
+
const open = claimExpected(cursor, parent, `abide:try:${id} open marker`)
|
|
196
|
+
try {
|
|
197
|
+
adopt(open)
|
|
198
|
+
} catch (error) {
|
|
199
|
+
rebuildCold(open, error)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/* Adopt the guarded branch in place: claim the server's `[` range-open marker, build the
|
|
204
|
+
guarded content (which claims its nodes) under the boundary, then claim the `]`
|
|
205
|
+
range-close and the boundary close, parking an anchor so a later swap detaches this
|
|
206
|
+
range like any other. A build throw disposes the partial scope and rethrows so
|
|
207
|
+
`firstHydrate` rebuilds cold. */
|
|
208
|
+
function adopt(open: Node): void {
|
|
209
|
+
const cursor = hydration as NonNullable<typeof hydration>
|
|
210
|
+
cursor.next.set(parent, open.nextSibling ?? null)
|
|
211
|
+
/* The server emits the `[ … ]` range inside the boundary — claim `[` as the live
|
|
212
|
+
range start, then advance the cursor to the first content node. */
|
|
213
|
+
const start = claimExpected(cursor, parent, `abide:try:${id} range-open marker`) as Comment
|
|
214
|
+
cursor.next.set(parent, start.nextSibling ?? null)
|
|
215
|
+
/* Install the boundary around the whole adopt (build + marker claims); the shared
|
|
216
|
+
strand-dispose guard in `adoptStrand` disposes the partial scope and rethrows on any
|
|
217
|
+
failure, and this `finally` restores the boundary before the rethrow reaches
|
|
218
|
+
`firstHydrate`. */
|
|
219
|
+
const previousBoundary = CURRENT_BOUNDARY.current
|
|
220
|
+
if (hasCatch) {
|
|
221
|
+
CURRENT_BOUNDARY.current = boundary
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
branch.adoptStrand(
|
|
225
|
+
(host) => renderTry(host),
|
|
226
|
+
() => {
|
|
227
|
+
const end = claimExpected(
|
|
228
|
+
cursor,
|
|
229
|
+
parent,
|
|
230
|
+
`abide:try:${id} range-close marker`,
|
|
231
|
+
) as Comment
|
|
232
|
+
cursor.next.set(parent, end.nextSibling ?? null)
|
|
233
|
+
const close = claimExpected(cursor, parent, `/abide:try:${id} close marker`)
|
|
234
|
+
cursor.next.set(parent, close.nextSibling ?? null)
|
|
235
|
+
const anchorNode = document.createTextNode('')
|
|
236
|
+
parent.insertBefore(anchorNode, close)
|
|
237
|
+
branch.anchor = anchorNode
|
|
238
|
+
return { start, end }
|
|
239
|
+
},
|
|
240
|
+
)
|
|
241
|
+
activeKind = 'try'
|
|
242
|
+
} finally {
|
|
243
|
+
CURRENT_BOUNDARY.current = previousBoundary
|
|
244
|
+
}
|
|
245
|
+
}
|
|
86
246
|
|
|
87
|
-
/*
|
|
88
|
-
(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
247
|
+
/* Discard the SSR boundary and build the catch branch fresh in its place (hydration off).
|
|
248
|
+
No catch → rethrow (the throw surfaces past the boundary, as the sync version did). */
|
|
249
|
+
function rebuildCold(open: Node, error: unknown): void {
|
|
250
|
+
branch.detach()
|
|
251
|
+
const after = discardBoundary(
|
|
252
|
+
parent,
|
|
253
|
+
open,
|
|
254
|
+
`/abide:try:${id}`,
|
|
255
|
+
hydration as NonNullable<typeof hydration>,
|
|
256
|
+
)
|
|
257
|
+
if (!hasCatch) {
|
|
95
258
|
throw error
|
|
96
259
|
}
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
parent.insertBefore(
|
|
260
|
+
const anchorNode = document.createTextNode('')
|
|
261
|
+
branch.anchor = anchorNode
|
|
262
|
+
parent.insertBefore(anchorNode, after)
|
|
263
|
+
withoutHydration(() => showCatch(error))
|
|
100
264
|
}
|
|
101
265
|
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/* One branch of a `switch`: `match` returns the value this case selects on
|
|
2
2
|
(undefined = the default branch); `render` builds the branch's content into the
|
|
3
|
-
parent (the block tracks it as a range between markers).
|
|
3
|
+
parent (the block tracks it as a range between markers). `pending` (only on an
|
|
4
|
+
async `{#if}`/`{:elseif}` cond-chain branch) reports whether this branch's own
|
|
5
|
+
async condition is still loading — a true reading holds the whole chain here, so a
|
|
6
|
+
later branch never renders on a not-yet-known earlier condition. */
|
|
4
7
|
export type SwitchCase = {
|
|
5
8
|
match: (() => unknown) | undefined
|
|
6
9
|
render: (parent: Node) => void
|
|
10
|
+
pending?: () => boolean
|
|
7
11
|
}
|
package/src/lib/ui/dom/when.ts
CHANGED
|
@@ -8,6 +8,12 @@ just element roots. A 2-case swappable range tracks `condition()` and swaps the
|
|
|
8
8
|
range's content on a truthy↔falsy flip (`render` truthy, `renderElse` falsy); an
|
|
9
9
|
unchanged condition is a no-op. See `mountSwappableRange` for the shared
|
|
10
10
|
hydrate/swap/teardown semantics.
|
|
11
|
+
|
|
12
|
+
`isPending` (compiler-supplied only for a bare async subject) adds a third state: while
|
|
13
|
+
it returns true the block renders NEITHER branch, so a still-loading promise never flashes
|
|
14
|
+
its `{:else}` before settling — "pending" is not conflated with a settled falsy value. It is
|
|
15
|
+
consulted first, so `condition()` (the throwing peek) runs only once the cell is no longer
|
|
16
|
+
pending; an error then throws to the nearest `{#try}`.
|
|
11
17
|
*/
|
|
12
18
|
// @documentation plumbing
|
|
13
19
|
export function when(
|
|
@@ -16,11 +22,15 @@ export function when(
|
|
|
16
22
|
render: (parent: Node) => void,
|
|
17
23
|
renderElse?: (parent: Node) => void,
|
|
18
24
|
before: Node | null = null,
|
|
25
|
+
isPending?: () => boolean,
|
|
19
26
|
): void {
|
|
20
27
|
mountSwappableRange(
|
|
21
28
|
parent,
|
|
22
|
-
() => (condition() ? 'then' : 'else'),
|
|
23
|
-
(branch) => (branch === 'then' ? render : renderElse),
|
|
29
|
+
() => (isPending?.() === true ? 'pending' : condition() ? 'then' : 'else'),
|
|
30
|
+
(branch) => (branch === 'then' ? render : branch === 'else' ? renderElse : undefined),
|
|
24
31
|
before,
|
|
32
|
+
/* An async subject (compiler supplies `isPending`) can render a different branch on the
|
|
33
|
+
server than the pending client, so hydration must not adopt the SSR range in place. */
|
|
34
|
+
isPending !== undefined,
|
|
25
35
|
)
|
|
26
36
|
}
|
|
@@ -15,9 +15,9 @@ build, so the restore is exact). `build` returns its reactivity stopper (from `s
|
|
|
15
15
|
the caller wraps its own DOM teardown (clear host vs clear range) around one
|
|
16
16
|
`lexical.dispose()`, no longer composing a separate `stop()` at every site.
|
|
17
17
|
*/
|
|
18
|
-
export function withScope(
|
|
18
|
+
export function withScope(build: () => () => void): { lexical: Scope } {
|
|
19
19
|
const parentScope = CURRENT_SCOPE.current
|
|
20
|
-
const lexical = createScope({}, parentScope, true
|
|
20
|
+
const lexical = createScope({}, parentScope, true)
|
|
21
21
|
enterRenderPass()
|
|
22
22
|
CURRENT_SCOPE.current = lexical
|
|
23
23
|
try {
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { FlightPromise } from './flight.ts'
|
|
2
|
+
import { RANGE_CLOSE, RANGE_OPEN } from './runtime/RANGE_MARKER.ts'
|
|
3
|
+
import type { ResumeEntry } from './runtime/RESUME.ts'
|
|
4
|
+
import type { SsrAwait, SsrRender } from './runtime/types/SsrRender.ts'
|
|
5
|
+
|
|
6
|
+
/* Comment-wrapped range markers — imported from the shared constant (not a local literal) so the
|
|
7
|
+
inline branch below can never drift from `generateSSR`'s own range emit. */
|
|
8
|
+
const OPEN = `<!--${RANGE_OPEN}-->`
|
|
9
|
+
const CLOSE = `<!--${RANGE_CLOSE}-->`
|
|
10
|
+
|
|
11
|
+
/* One hoistable child render staged by generateSSR: the reserved index in its output array, the
|
|
12
|
+
array itself, the child's render-path (the streamed boundary id), and its in-flight render. */
|
|
13
|
+
export type StagedChild = {
|
|
14
|
+
slot: number
|
|
15
|
+
out: string[]
|
|
16
|
+
id: string
|
|
17
|
+
flight: FlightPromise
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/*
|
|
21
|
+
The ADR-0039 WHEN-TO-STREAM decision, run once after a component's body walk (compileSSR emits
|
|
22
|
+
`await $$finalizeStreamedChildren($childSlots, $awaits, $resume)` before the render returns). Each
|
|
23
|
+
hoistable child was started as an isolated `$$flight` in the prefix and its output position RESERVED
|
|
24
|
+
(an empty `$out` slot); here we fill that slot per child:
|
|
25
|
+
|
|
26
|
+
- A flight that has already SETTLED (a synchronous / warm-cache child, or a fast read) inlines its
|
|
27
|
+
html into the reserved slot as `<!--[-->…<!--]-->` — BYTE-IDENTICAL to the pre-ADR-0039 inline
|
|
28
|
+
`await Child.render()` path, so an all-fast page's wire and hydration are unchanged. Its awaits /
|
|
29
|
+
resume merge exactly as the inline path did.
|
|
30
|
+
- A flight settled REJECTED rethrows here — same as a rejecting inline await (500 before flush).
|
|
31
|
+
- A still-PENDING flight (genuine I/O) STREAMS: the slot gets an empty `abide:await:CHILDPATH`
|
|
32
|
+
boundary, and an html-only SsrAwait is pushed so `renderToStream` flushes the shell now and streams
|
|
33
|
+
the child's fragment when it settles (its own nested awaits/resume compose through the same drain).
|
|
34
|
+
|
|
35
|
+
Timing: one microtask drain settles every synchronous child; a single shared macrotask is paid ONLY
|
|
36
|
+
if some child is still pending after that (a page whose children are all fast never reaches it), so
|
|
37
|
+
the inline fast path costs one microtask and a genuinely-slow child — which was going to block the
|
|
38
|
+
shell anyway — trades ~one tick for a progressively-flushed shell.
|
|
39
|
+
*/
|
|
40
|
+
// @documentation plumbing
|
|
41
|
+
export async function finalizeStreamedChildren(
|
|
42
|
+
slots: StagedChild[],
|
|
43
|
+
awaits: SsrAwait[],
|
|
44
|
+
resume: Record<string, ResumeEntry>,
|
|
45
|
+
): Promise<void> {
|
|
46
|
+
if (slots.length === 0) {
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
await Promise.resolve()
|
|
50
|
+
if (slots.some((staged) => !staged.flight.settled)) {
|
|
51
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 0))
|
|
52
|
+
}
|
|
53
|
+
for (const staged of slots) {
|
|
54
|
+
if (staged.flight.settled && staged.flight.error === undefined) {
|
|
55
|
+
const rendered = staged.flight.value as SsrRender
|
|
56
|
+
staged.out[staged.slot] = OPEN + rendered.html + CLOSE
|
|
57
|
+
mergeChildRender(rendered, awaits, resume)
|
|
58
|
+
} else if (staged.flight.settled) {
|
|
59
|
+
throw staged.flight.error
|
|
60
|
+
} else {
|
|
61
|
+
staged.out[staged.slot] =
|
|
62
|
+
`<!--abide:await:${staged.id}--><!--/abide:await:${staged.id}-->`
|
|
63
|
+
awaits.push({
|
|
64
|
+
id: staged.id,
|
|
65
|
+
htmlOnly: true,
|
|
66
|
+
promise: () => staged.flight,
|
|
67
|
+
// biome-ignore lint/suspicious/noThenProperty: `then` is the SsrAwait renderer callback the stream drain invokes explicitly with the resolved value — this object is stored in `awaits[]`, never awaited as a thenable
|
|
68
|
+
then: async (rendered) => {
|
|
69
|
+
mergeChildRender(rendered as SsrRender, awaits, resume)
|
|
70
|
+
return (rendered as SsrRender).html
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* Fold a child render's contribution (its nested awaits, its resume map) up into the parent's — the
|
|
78
|
+
single definition both the inlined-settled arm and the still-pending stream arm merge through, so
|
|
79
|
+
the two can never drift on what a child contributes upward. */
|
|
80
|
+
function mergeChildRender(
|
|
81
|
+
rendered: SsrRender,
|
|
82
|
+
awaits: SsrAwait[],
|
|
83
|
+
resume: Record<string, ResumeEntry>,
|
|
84
|
+
): void {
|
|
85
|
+
for (const nested of rendered.awaits) {
|
|
86
|
+
awaits.push(nested)
|
|
87
|
+
}
|
|
88
|
+
Object.assign(resume, rendered.resume)
|
|
89
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Server-only flight-starter behind the SSR codegen's `$$flight` alias (ADR-0034). The
|
|
3
|
+
SSR back-end hoists a *hoistable* await's promise into the synchronous render prefix as
|
|
4
|
+
`const $flightN = $$flight(() => (<promise expr>))`, so the flight is already in-flight
|
|
5
|
+
when the body walk reaches its blocking `await $flightN` / streaming `promise: () =>
|
|
6
|
+
$flightN` — independent flights overlap instead of serializing (prefix → barrier → body
|
|
7
|
+
→ drain). Server-only: the client build never emits `$$flight`, so it tree-shakes out of
|
|
8
|
+
the browser bundle (like `$$settleAsyncCells`).
|
|
9
|
+
|
|
10
|
+
Two behaviours the raw promise expression can't provide on its own:
|
|
11
|
+
|
|
12
|
+
- A SYNCHRONOUS throw in the loader (before it returns a promise) is normalised to a
|
|
13
|
+
rejected promise, so a hoisted flight's throw still lands in the block's `{:catch}` /
|
|
14
|
+
500 path exactly as an inline `await (expr)` would — the expression no longer evaluates
|
|
15
|
+
inside the block's try/catch, so the thunk wrapper restores that reach.
|
|
16
|
+
- The settledness `.then(onValue, onError)` below is itself a rejection handler attached
|
|
17
|
+
synchronously to `promise`, so a flight that rejects in the window before its real
|
|
18
|
+
consumer (the inline await / the `renderToStream` drain) attaches a handler is never a
|
|
19
|
+
Bun-fatal unhandled rejection — the `onError` branch never rethrows, so the block's
|
|
20
|
+
surface-rejection → 500 / catch-branch semantics are unchanged. Mirrors the inline
|
|
21
|
+
`.then(onValue, onError)` guard in `createAsyncCell`.
|
|
22
|
+
*/
|
|
23
|
+
// @documentation plumbing
|
|
24
|
+
export function flight(thunk: () => unknown): FlightPromise {
|
|
25
|
+
let promise: Promise<unknown>
|
|
26
|
+
try {
|
|
27
|
+
promise = Promise.resolve(thunk())
|
|
28
|
+
} catch (error) {
|
|
29
|
+
promise = Promise.reject(error)
|
|
30
|
+
}
|
|
31
|
+
/* Synchronous settledness signal (ADR-0039): the post-walk `finalizeStreamedChildren` reads
|
|
32
|
+
`.settled` to decide inline-vs-stream — a child flight that already resolved by finalize time
|
|
33
|
+
inlines its html into the shell (byte-identical to today), a still-pending one streams. Its
|
|
34
|
+
`onError` branch doubles as the rejection keeper (see the header note): it marks `promise`
|
|
35
|
+
handled, so a pre-consumer rejection is never a Bun-fatal unhandled rejection. */
|
|
36
|
+
const flightPromise = promise as FlightPromise
|
|
37
|
+
flightPromise.settled = false
|
|
38
|
+
promise.then(
|
|
39
|
+
(value) => {
|
|
40
|
+
flightPromise.value = value
|
|
41
|
+
flightPromise.settled = true
|
|
42
|
+
},
|
|
43
|
+
(error) => {
|
|
44
|
+
flightPromise.error = error
|
|
45
|
+
flightPromise.settled = true
|
|
46
|
+
},
|
|
47
|
+
)
|
|
48
|
+
return flightPromise
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* A flight promise carrying a synchronous settledness snapshot for `finalizeStreamedChildren`. */
|
|
52
|
+
export type FlightPromise = Promise<unknown> & {
|
|
53
|
+
settled: boolean
|
|
54
|
+
value?: unknown
|
|
55
|
+
error?: unknown
|
|
56
|
+
}
|
package/src/lib/ui/html.ts
CHANGED
|
@@ -16,7 +16,18 @@ written inside a template, so it lives in `ui/` (the reader, `rawHtmlString`, is
|
|
|
16
16
|
isomorphic plumbing and stays in `shared/`).
|
|
17
17
|
*/
|
|
18
18
|
// @documentation templating
|
|
19
|
-
export function html(
|
|
19
|
+
export function html(
|
|
20
|
+
strings: TemplateStringsArray | string | null | undefined,
|
|
21
|
+
...values: unknown[]
|
|
22
|
+
): RawHtml {
|
|
23
|
+
/* A plain call with a nullish argument renders nothing. A bare async read (ADR-0032)
|
|
24
|
+
hands `html()` `undefined` while pending — `{html(highlight(code)?.html)}` — so the
|
|
25
|
+
plain-call path degrades to empty raw instead of throwing on `strings[0]`, matching
|
|
26
|
+
how a bare `{value}` stringifies `undefined` to `""`. Mirrors the null-tolerance
|
|
27
|
+
ADR-0032 added to `done()`/`peek()` for the same inline-async ergonomics. */
|
|
28
|
+
if (strings === null || strings === undefined) {
|
|
29
|
+
return { [RAW_HTML]: '' }
|
|
30
|
+
}
|
|
20
31
|
if (typeof strings === 'string') {
|
|
21
32
|
return { [RAW_HTML]: strings }
|
|
22
33
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { cellBarrierBacking } from './runtime/cellBarrierBacking.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Runs a hoisted child render (`$$isolateCellBarrier`) under its OWN async-cell barrier list, so its
|
|
5
|
+
cells register and its `$$settleAsyncCells` drain are isolated from every concurrent sibling and from
|
|
6
|
+
the page (ADR-0037 Phase 2). Wraps the render start emitted by the SSR flight hoist:
|
|
7
|
+
`$$flight(() => $$isolateCellBarrier(() => $$withPath(ordinal, () => Child.render(props, $ctx))))`.
|
|
8
|
+
The server backing runs it inside an AsyncLocalStorage so the fresh list is inherited across the
|
|
9
|
+
child render's own awaits (its top-level script await, its barrier); the default (client / no server
|
|
10
|
+
install) backing is an inert passthrough — the client mounts synchronously and has no SSR barrier, so
|
|
11
|
+
there is nothing to isolate. Only the pending (barrier) list is isolated; resolved/streamed cell
|
|
12
|
+
values still aggregate on the per-request store for the warm-seed snapshot.
|
|
13
|
+
*/
|
|
14
|
+
// @documentation plumbing
|
|
15
|
+
export function isolateCellBarrier<T>(render: () => T): T {
|
|
16
|
+
return cellBarrierBacking.active.run({ promises: [] }, render)
|
|
17
|
+
}
|
package/src/lib/ui/linked.ts
CHANGED
|
@@ -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>(
|
|
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
|
}
|