@abide/abide 0.48.0 → 0.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +85 -59
- package/CHANGELOG.md +216 -0
- package/README.md +21 -10
- package/package.json +15 -4
- package/src/abideLsp.ts +5 -6
- package/src/abideResolverPlugin.ts +197 -169
- package/src/build.ts +79 -56
- package/src/buildCli.ts +3 -5
- package/src/buildDisconnected.ts +2 -5
- package/src/bundleApp.ts +2 -3
- package/src/checkAbide.ts +12 -2
- package/src/compile.ts +2 -4
- 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/installDownloads.ts +13 -3
- package/src/lib/bundle/installMacMenu.ts +13 -3
- package/src/lib/bundle/spawnEmbeddedServer.ts +13 -4
- package/src/lib/bundle/waitForServer.ts +6 -1
- package/src/lib/cli/parseArgvForRpc.ts +36 -9
- package/src/lib/cli/tokenizeArgvFlags.ts +4 -2
- package/src/lib/mcp/createMcpResourceServer.ts +13 -2
- package/src/lib/mcp/mcpTools.ts +15 -6
- package/src/lib/mcp/toolResultFromResponse.ts +40 -12
- 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 +113 -14
- package/src/lib/server/rpc/resolveRpcJsonSchema.ts +28 -0
- package/src/lib/server/rpc/runWithRpcTimeout.ts +12 -1
- 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/finalizeResponse.ts +11 -1
- 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 +24 -16
- 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 +20 -0
- package/src/lib/shared/buildRpcRequest.ts +6 -2
- 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/canonicalJson.ts +24 -1
- package/src/lib/shared/changeAffectsClient.ts +12 -7
- package/src/lib/shared/createCacheStore.ts +31 -8
- package/src/lib/shared/createChannelLog.ts +24 -0
- 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/shared/exitOnBuildFailure.ts +4 -3
- 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/parseEnv.ts +17 -6
- 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/serializeEnv.ts +18 -6
- package/src/lib/shared/snapshotShippable.ts +8 -7
- package/src/lib/shared/snippet.ts +11 -6
- 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/url.ts +5 -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 -7
- 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/bindListenEvent.ts +5 -6
- 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 +52 -13
- package/src/lib/ui/compile/compileShadow.ts +265 -42
- package/src/lib/ui/compile/composeProps.ts +15 -4
- 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 +419 -37
- package/src/lib/ui/compile/expressionIsPrefixEvaluable.ts +19 -0
- package/src/lib/ui/compile/generateBuild.ts +74 -17
- package/src/lib/ui/compile/generateSSR.ts +286 -75
- 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/isWhitespaceText.ts +10 -2
- 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 +51 -10
- 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 -1090
- 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 +26 -40
- package/src/lib/ui/compile/resolveReactiveExport.ts +4 -7
- package/src/lib/ui/compile/scopeCss.ts +27 -4
- 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 +62 -75
- 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/discardBoundary.ts +24 -6
- package/src/lib/ui/dom/disposeRange.ts +2 -1
- package/src/lib/ui/dom/each.ts +41 -12
- package/src/lib/ui/dom/eachAsync.ts +39 -3
- package/src/lib/ui/dom/fillBefore.ts +6 -7
- 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/mergeProps.ts +1 -1
- 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/mountSlot.ts +1 -1
- 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/restProps.ts +2 -2
- package/src/lib/ui/dom/spreadProps.ts +3 -4
- package/src/lib/ui/dom/switchBlock.ts +30 -7
- package/src/lib/ui/dom/tryBlock.ts +233 -70
- 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 +9 -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/props.ts +17 -0
- package/src/lib/ui/remoteProxy.ts +84 -159
- package/src/lib/ui/renderChain.ts +53 -13
- package/src/lib/ui/renderToStream.ts +22 -18
- package/src/lib/ui/resumeSeedScript.ts +1 -1
- package/src/lib/ui/router.ts +86 -53
- package/src/lib/ui/runtime/AsyncCellError.ts +20 -0
- package/src/lib/ui/runtime/CELL_SEED.ts +14 -0
- package/src/lib/ui/runtime/CHILD_PRESENT.ts +2 -2
- 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 +17 -4
- 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 +31 -54
- 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 +33 -1
- 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/scope.ts +11 -0
- package/src/lib/ui/runtime/toTeardown.ts +10 -3
- 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/types/UiProps.ts +6 -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/runtime/withoutHydration.ts +22 -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/state.ts +9 -3
- 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/scopeLabel.ts +0 -21
- package/src/lib/ui/dom/text.ts +0 -20
- package/src/lib/ui/history.ts +0 -101
- package/src/lib/ui/installHotBridge.ts +0 -93
- 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 -28
- 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
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
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/*
|
|
2
|
+
The prop reader. In a `.abide` component the compiler rewrites `const { name } =
|
|
3
|
+
props()` to reads off the component's prop bag (`$props`), so this runtime body
|
|
4
|
+
never executes there — it exists for import resolution and for typing `props()`
|
|
5
|
+
in plain `.ts` modules. Called directly (only possible outside a compiled
|
|
6
|
+
component) it throws, like `request()` outside a request scope, rather than
|
|
7
|
+
silently returning undefined.
|
|
8
|
+
|
|
9
|
+
The return type is `T` (default `Record<string, unknown>`); inside a `.abide`
|
|
10
|
+
file the check shadow supplies the file-contextual `RouteShape & T` instead.
|
|
11
|
+
*/
|
|
12
|
+
// @documentation reactive-state
|
|
13
|
+
export function props<T = Record<string, unknown>>(): T {
|
|
14
|
+
throw new Error(
|
|
15
|
+
'[abide] props() is compiler-lowered inside a .abide component and has no runtime meaning when called directly',
|
|
16
|
+
)
|
|
17
|
+
}
|
|
@@ -6,32 +6,49 @@ import { HttpError } from '../shared/HttpError.ts'
|
|
|
6
6
|
import { OFFLINE_HEADER } from '../shared/OFFLINE_HEADER.ts'
|
|
7
7
|
import { rpcTimeoutSlot } from '../shared/rpcTimeoutSlot.ts'
|
|
8
8
|
import { trace } from '../shared/trace.ts'
|
|
9
|
+
import type { CachePolicy } from '../shared/types/CachePolicy.ts'
|
|
9
10
|
import type { HttpMethod } from '../shared/types/HttpMethod.ts'
|
|
10
|
-
import type {
|
|
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 {
|
|
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
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
): RemoteFunction<Args, Return
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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:
|
|
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.
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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 `
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
block-id counter map (`$ctx`): each `await`/`try` block draws a path-namespaced id
|
|
19
|
+
(`${render-path}:${n}`, ADR-0037), so ids stay unique across layers by path rather than by
|
|
20
|
+
a shared sequential draw — keeping the streamed fragments and the resume manifest aligned
|
|
21
|
+
with the client (which composes the same path-keyed ids). The layers render IN PARALLEL
|
|
22
|
+
(`Promise.all`, ADR-0038): each roots a distinct route-key path so their block ids never collide,
|
|
23
|
+
and each runs under `isolateCellBarrier` so their async-cell barriers don't cross-drain — the html
|
|
24
|
+
fold + state/awaits/resume aggregation below run after all settle and are order-independent.
|
|
21
25
|
|
|
22
26
|
The html nests inner-to-outer: each parent layout's empty outlet boundary
|
|
23
27
|
(`<!--abide:outlet--><!--/abide:outlet-->`) is filled with the accumulated child html —
|
|
@@ -30,22 +34,58 @@ A layout missing its `<slot/>` is a build error surfaced here.
|
|
|
30
34
|
export async function renderChain(
|
|
31
35
|
views: UiComponent[],
|
|
32
36
|
params: Record<string, string>,
|
|
37
|
+
/* Each view's route key (layout directory URL / page route pattern), aligned 1:1 with `views`
|
|
38
|
+
and byte-identical to the client router's `chainKeys`/`pageKey`. Each roots its layer's
|
|
39
|
+
render-path (`withPath`) so a cell's scope id matches the client's for the warm-seed key. */
|
|
40
|
+
keys: string[] = [],
|
|
33
41
|
): Promise<SsrRender> {
|
|
34
|
-
const ctx: RenderContext =
|
|
42
|
+
const ctx: RenderContext = new Map()
|
|
35
43
|
const renders: SsrRender[] = []
|
|
36
44
|
/* Route params as thunks (static server-side — only shape parity with the client so
|
|
37
45
|
`props()` reads `$props["id"]?.()` resolve). A layout (every view but the last) also
|
|
38
|
-
gets
|
|
39
|
-
the client does, keeping hydration congruent.
|
|
46
|
+
gets `children` set to `CHILD_PRESENT` so SSR renders `{#if children}` the same way
|
|
47
|
+
the client does, keeping hydration congruent. The sentinel itself must be thunk-wrapped
|
|
48
|
+
(`() => CHILD_PRESENT`) since the destructure lowering CALLS every bag entry — leaving
|
|
49
|
+
it raw would invoke `CHILD_PRESENT` (a function) and read back `undefined`. */
|
|
40
50
|
const paramThunks: UiProps = {}
|
|
41
51
|
for (const key of Object.keys(params)) {
|
|
42
52
|
paramThunks[key] = () => params[key]
|
|
43
53
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
54
|
+
if (views.length > 1) {
|
|
55
|
+
/* ADR-0038: a route WITH layouts renders its layers IN PARALLEL. Block ids are path-keyed
|
|
56
|
+
(ADR-0037) and each layer roots a DISTINCT route-key path, so their id allocations never
|
|
57
|
+
collide even as the async continuations interleave; the fold + state/awaits/resume
|
|
58
|
+
aggregation below run AFTER all settle and are order-independent (keyed merges /
|
|
59
|
+
index-ordered). Each layer runs under `isolateCellBarrier` so its async-cell barrier drains
|
|
60
|
+
its OWN pending list — without it, two layers registering cells concurrently into the one
|
|
61
|
+
request-scoped list would splice-drain each other (the hazard ADR-0037 fixed for sibling
|
|
62
|
+
children). Scope needs no isolation: the shipped parallel child renders tolerate the
|
|
63
|
+
identical per-request CURRENT_SCOPE clobber (all scope-sensitive construction is
|
|
64
|
+
synchronous in each render's prefix). */
|
|
65
|
+
const collected = await Promise.all(
|
|
66
|
+
views.map((view, index) => {
|
|
67
|
+
const hasChild = index < views.length - 1
|
|
68
|
+
const props: UiProps = hasChild
|
|
69
|
+
? { ...paramThunks, children: () => CHILD_PRESENT }
|
|
70
|
+
: paramThunks
|
|
71
|
+
const key = keys[index]
|
|
72
|
+
const run = () => isolateCellBarrier(() => view.render(props, ctx))
|
|
73
|
+
return key === undefined ? run() : withPath(key, run)
|
|
74
|
+
}),
|
|
75
|
+
)
|
|
76
|
+
renders.push(...collected)
|
|
77
|
+
} else if (views.length === 1) {
|
|
78
|
+
/* A lone page (no layouts) — the common case — renders DIRECTLY: no parallelism to gain, and
|
|
79
|
+
no `Promise.all`/`isolateCellBarrier` wrap, so its bare-read/settle timing is byte-identical
|
|
80
|
+
to the pre-ADR-0038 path (a fast in-process read stays pending → streams, rather than
|
|
81
|
+
slipping settled → inline behind an extra microtask). */
|
|
82
|
+
const view = views[0] as UiComponent
|
|
83
|
+
const key = keys[0]
|
|
84
|
+
renders.push(
|
|
85
|
+
key === undefined
|
|
86
|
+
? await view.render(paramThunks, ctx)
|
|
87
|
+
: await withPath(key, () => view.render(paramThunks, ctx)),
|
|
88
|
+
)
|
|
49
89
|
}
|
|
50
90
|
let html = renders[renders.length - 1]?.html ?? ''
|
|
51
91
|
for (let index = renders.length - 2; index >= 0; index -= 1) {
|
|
@@ -8,8 +8,8 @@ Out-of-order SSR streaming. Yields the shell first (so the browser paints
|
|
|
8
8
|
immediately), then one resolved fragment per STREAMING await block as its promise
|
|
9
9
|
settles — in completion order, not source order, so a slow read never blocks a fast
|
|
10
10
|
one. Each resolved fragment is a `<abide-resolve data-id="ID"><script
|
|
11
|
-
type="application/json">…</script>…</abide-resolve>` that
|
|
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<
|
|
42
|
-
const resumeDelta = (): Record<
|
|
43
|
-
const delta: Record<
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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<
|
|
59
|
-
const enqueued = new Set<
|
|
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:
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
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
|
|
121
|
-
|
|
122
|
-
function encodeStreamResume(resume: ResumeEntry, id:
|
|
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<
|
|
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),
|