@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
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import { decodeRefJson } from '../../shared/decodeRefJson.ts'
|
|
2
2
|
import { effect } from '../effect.ts'
|
|
3
|
-
import {
|
|
3
|
+
import { CURRENT_BOUNDARY } from '../runtime/CURRENT_BOUNDARY.ts'
|
|
4
|
+
import { claimExpected } from '../runtime/claimExpected.ts'
|
|
4
5
|
import { generationGuard } from '../runtime/generationGuard.ts'
|
|
5
6
|
import { RANGE_CLOSE, RANGE_OPEN } from '../runtime/RANGE_MARKER.ts'
|
|
6
7
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
7
8
|
import type { ResumeEntry } from '../runtime/RESUME.ts'
|
|
8
9
|
import { RESUME } from '../runtime/RESUME.ts'
|
|
9
|
-
import { scope } from '../runtime/scope.ts'
|
|
10
10
|
import { scopeGroup } from '../runtime/scopeGroup.ts'
|
|
11
11
|
import type { State } from '../runtime/types/State.ts'
|
|
12
|
+
import { withoutHydration } from '../runtime/withoutHydration.ts'
|
|
12
13
|
import { state } from '../state.ts'
|
|
13
|
-
import {
|
|
14
|
+
import { anchoredBranch } from './anchoredBranch.ts'
|
|
14
15
|
import { discardBoundary } from './discardBoundary.ts'
|
|
15
|
-
import { removeRange } from './removeRange.ts'
|
|
16
16
|
|
|
17
17
|
/*
|
|
18
18
|
Async binding — the runtime for `<template await>`. Renders the pending branch,
|
|
@@ -38,12 +38,13 @@ parked just before the close marker.
|
|
|
38
38
|
// @documentation plumbing
|
|
39
39
|
export function awaitBlock(
|
|
40
40
|
parent: Node,
|
|
41
|
-
id:
|
|
41
|
+
id: string,
|
|
42
42
|
promiseThunk: () => unknown,
|
|
43
43
|
renderPending: ((parent: Node) => void) | undefined,
|
|
44
44
|
renderThen: (parent: Node, value: unknown) => void,
|
|
45
|
-
/* Absent when the block has no catch branch — a rejection then
|
|
46
|
-
|
|
45
|
+
/* Absent when the block has no catch branch — a rejection then routes to the enclosing
|
|
46
|
+
{#try} boundary captured at build (if any), else surfaces (re-throws to the
|
|
47
|
+
unhandled-rejection path) instead of rendering an empty branch. */
|
|
47
48
|
renderCatch: ((parent: Node, error: unknown) => void) | undefined,
|
|
48
49
|
/* A static node located by the skeleton: the block's anchor inserts before it on
|
|
49
50
|
create (block before a static suffix). Null appends (tail). insertBefore(x, null)
|
|
@@ -51,11 +52,16 @@ export function awaitBlock(
|
|
|
51
52
|
before: Node | null = null,
|
|
52
53
|
): void {
|
|
53
54
|
const hydration = RENDER.hydration
|
|
55
|
+
/* The enclosing {#try} boundary ambient at BUILD (like createEffectNode captures it). A
|
|
56
|
+
catch-less rejection settles LATER, after CURRENT_BOUNDARY has been restored, so we must
|
|
57
|
+
capture it into this closure now rather than read it at settle time. */
|
|
58
|
+
const capturedBoundary = CURRENT_BOUNDARY.current
|
|
54
59
|
/* The live branch's scope, registered with the owner so it disposes on owner
|
|
55
60
|
teardown — not only when a settle/re-run swaps branches via detach. */
|
|
56
61
|
const group = scopeGroup()
|
|
57
|
-
|
|
58
|
-
|
|
62
|
+
/* The single active branch mounted before an anchor — owns `active`/`detach`/`place`/
|
|
63
|
+
adopt-strand guard (shared with `tryBlock`); this block keeps its bespoke settlement. */
|
|
64
|
+
const branch = anchoredBranch(parent, group)
|
|
59
65
|
let first = true
|
|
60
66
|
/* Bumped each run so a prior run's in-flight promise can't clobber a newer one, AND on
|
|
61
67
|
owner teardown so an in-flight promise that settles AFTER the enclosing `{#if}`/
|
|
@@ -72,31 +78,6 @@ export function awaitBlock(
|
|
|
72
78
|
place (then→then) or must build a fresh branch. */
|
|
73
79
|
let activeKind: 'pending' | 'then' | 'catch' | undefined
|
|
74
80
|
|
|
75
|
-
const detach = (): void => {
|
|
76
|
-
if (active !== undefined) {
|
|
77
|
-
active.dispose()
|
|
78
|
-
/* `removeRange` evicts the markers AND everything between them via the end
|
|
79
|
-
marker's LIVE parent — not the captured `parent`, which (when this await is a
|
|
80
|
-
bare child of a control-flow branch) is the branch's build fragment, emptied
|
|
81
|
-
into the document once the enclosing block placed it. */
|
|
82
|
-
removeRange(active.start, active.end)
|
|
83
|
-
active = undefined
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/* Replace the current content with a freshly-built branch, before the anchor. The branch
|
|
88
|
-
builds into a detached `[`…`]`-bracketed fragment (so any content — components, text,
|
|
89
|
-
nested blocks — appends freely), the same create primitive the keyed-list runtimes use,
|
|
90
|
-
which lands as a marker-bounded range the next swap detaches with `removeRange`. */
|
|
91
|
-
const place = (build: (parent: Node) => void): void => {
|
|
92
|
-
detach()
|
|
93
|
-
const namespaceParent = anchor?.parentNode ?? parent
|
|
94
|
-
const { start, end, fragment, dispose } = buildDetachedRange(namespaceParent, build)
|
|
95
|
-
const tracked = group.track(dispose)
|
|
96
|
-
namespaceParent.insertBefore(fragment, anchor ?? null)
|
|
97
|
-
active = { start, end, dispose: tracked }
|
|
98
|
-
}
|
|
99
|
-
|
|
100
81
|
/* Settle to a resolved value. then→then updates the cell in place — the branch and its
|
|
101
82
|
inner each survive (no flash); any other prior kind builds a fresh then-branch around
|
|
102
83
|
a new cell. renderThen receives the CELL (not the raw value), so the branch reads it
|
|
@@ -108,17 +89,23 @@ export function awaitBlock(
|
|
|
108
89
|
}
|
|
109
90
|
const cell = state(value)
|
|
110
91
|
valueCell = cell
|
|
111
|
-
place((host) => renderThen(host, cell))
|
|
92
|
+
branch.place((host) => renderThen(host, cell))
|
|
112
93
|
activeKind = 'then'
|
|
113
94
|
}
|
|
114
95
|
|
|
115
|
-
/* Settle to a rejection:
|
|
96
|
+
/* Settle to a rejection: with a local catch branch, swap to it. With no local catch, route
|
|
97
|
+
to the enclosing {#try} boundary captured at build if one was ambient; otherwise surface
|
|
98
|
+
(unhandled rejection), preserving the prior behaviour when there's no boundary. */
|
|
116
99
|
const settleError = (error: unknown): void => {
|
|
117
100
|
if (renderCatch === undefined) {
|
|
101
|
+
if (capturedBoundary !== undefined) {
|
|
102
|
+
capturedBoundary.handle(error)
|
|
103
|
+
return
|
|
104
|
+
}
|
|
118
105
|
throw error
|
|
119
106
|
}
|
|
120
107
|
valueCell = undefined
|
|
121
|
-
place((host) => renderCatch(host, error))
|
|
108
|
+
branch.place((host) => renderCatch(host, error))
|
|
122
109
|
activeKind = 'catch'
|
|
123
110
|
}
|
|
124
111
|
|
|
@@ -135,10 +122,10 @@ export function awaitBlock(
|
|
|
135
122
|
shows the pending branch (or detaches) while the promise is in flight. */
|
|
136
123
|
if (activeKind !== 'then') {
|
|
137
124
|
if (renderPending !== undefined) {
|
|
138
|
-
place((host) => renderPending(host))
|
|
125
|
+
branch.place((host) => renderPending(host))
|
|
139
126
|
activeKind = 'pending'
|
|
140
127
|
} else {
|
|
141
|
-
detach()
|
|
128
|
+
branch.detach()
|
|
142
129
|
activeKind = undefined
|
|
143
130
|
}
|
|
144
131
|
}
|
|
@@ -161,29 +148,36 @@ export function awaitBlock(
|
|
|
161
148
|
marker for later swaps. The adopted content is everything the build claimed between
|
|
162
149
|
the open and close markers; bracketing it makes the adopted branch a marker-bounded
|
|
163
150
|
range identical to a freshly-`place`d one, so the FIRST swap detaches it with
|
|
164
|
-
`removeRange` like every later swap (no node-array special case).
|
|
151
|
+
`removeRange` like every later swap (no node-array special case). `adoptStrand` carries
|
|
152
|
+
the shared strand-dispose guard; the `afterBuild` closure below is await's bespoke
|
|
153
|
+
bracketing — it runs only after a clean build. */
|
|
165
154
|
const adopt = (open: Node | null, build: (parent: Node) => void): void => {
|
|
166
155
|
const cursor = hydration as NonNullable<typeof hydration>
|
|
167
156
|
const firstAdopted = open?.nextSibling ?? null
|
|
168
157
|
cursor.next.set(parent, firstAdopted)
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
158
|
+
branch.adoptStrand(build, () => {
|
|
159
|
+
/* A guaranteed control-flow marker — claimExpected throws on a desync (caught by
|
|
160
|
+
firstHydrate's adopt try/catch → rebuildCold) instead of silently claiming null
|
|
161
|
+
and over-clearing the parent. */
|
|
162
|
+
const close = claimExpected(cursor, parent, `/abide:await:${id} close marker`)
|
|
163
|
+
cursor.next.set(parent, close.nextSibling ?? null)
|
|
164
|
+
/* Bracket the adopted nodes: `[` before the first claimed node (or before `close`
|
|
165
|
+
for an empty branch), `]` then the anchor just before `close`. */
|
|
166
|
+
const start = document.createComment(RANGE_OPEN)
|
|
167
|
+
parent.insertBefore(start, firstAdopted ?? close)
|
|
168
|
+
const end = document.createComment(RANGE_CLOSE)
|
|
169
|
+
parent.insertBefore(end, close)
|
|
170
|
+
const anchorNode = document.createTextNode('')
|
|
171
|
+
parent.insertBefore(anchorNode, close)
|
|
172
|
+
branch.anchor = anchorNode
|
|
173
|
+
return { start, end }
|
|
174
|
+
})
|
|
181
175
|
}
|
|
182
176
|
|
|
183
177
|
/* Discard the SSR boundary and (re)build the block from the live promise, fresh
|
|
184
178
|
(hydration off) — the recovery path when adoption can't use the server markup. */
|
|
185
179
|
const rebuildCold = (open: Node | null): void => {
|
|
186
|
-
detach()
|
|
180
|
+
branch.detach()
|
|
187
181
|
/* Insert at the node AFTER the discarded boundary (its return) — NOT the captured
|
|
188
182
|
`before`, which for a skeleton-anchored block is the open boundary itself and is
|
|
189
183
|
removed here, so reusing it throws `NotFoundError` in a strict DOM. */
|
|
@@ -193,15 +187,10 @@ export function awaitBlock(
|
|
|
193
187
|
`/abide:await:${id}`,
|
|
194
188
|
hydration as NonNullable<typeof hydration>,
|
|
195
189
|
)
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
try {
|
|
201
|
-
render(promiseThunk())
|
|
202
|
-
} finally {
|
|
203
|
-
RENDER.hydration = previous
|
|
204
|
-
}
|
|
190
|
+
const anchorNode = document.createTextNode('')
|
|
191
|
+
branch.anchor = anchorNode
|
|
192
|
+
parent.insertBefore(anchorNode, after)
|
|
193
|
+
withoutHydration(() => render(promiseThunk()))
|
|
205
194
|
}
|
|
206
195
|
|
|
207
196
|
/* The first run when hydrating: adopt by precedence (resume / warm-sync), else
|
|
@@ -211,7 +200,9 @@ export function awaitBlock(
|
|
|
211
200
|
warm cache (or re-fetches) instead of crashing hydration. */
|
|
212
201
|
const firstHydrate = (): void => {
|
|
213
202
|
const cursor = hydration as NonNullable<typeof hydration>
|
|
214
|
-
|
|
203
|
+
/* The await block's open marker is compiler-guaranteed — claimExpected throws a
|
|
204
|
+
legible desync here rather than propagating a null that over-clears the parent. */
|
|
205
|
+
const open = claimExpected(cursor, parent, `abide:await:${id} open marker`)
|
|
215
206
|
/* RESUME holds the ref-json-encoded entry STRING; decode here, where the codec
|
|
216
207
|
lives. A decode failure (malformed/absent payload) reads as "no resume" — fall
|
|
217
208
|
through to the live promise rather than crash hydration. */
|
|
@@ -280,18 +271,13 @@ export function awaitBlock(
|
|
|
280
271
|
}
|
|
281
272
|
/* Insert at the node after the discarded boundary (see `rebuildCold`). */
|
|
282
273
|
const after = discardBoundary(parent, open, `/abide:await:${id}`, cursor)
|
|
283
|
-
|
|
284
|
-
|
|
274
|
+
const anchorNode = document.createTextNode('')
|
|
275
|
+
branch.anchor = anchorNode
|
|
276
|
+
parent.insertBefore(anchorNode, after)
|
|
285
277
|
/* The boundary's server nodes are gone, so the pending branch builds FRESH — clear
|
|
286
|
-
the claim cursor (
|
|
278
|
+
the claim cursor (see withoutHydration) so its `cloneStatic`/text don't try to
|
|
287
279
|
claim discarded nodes and silently render nothing. */
|
|
288
|
-
|
|
289
|
-
RENDER.hydration = undefined
|
|
290
|
-
try {
|
|
291
|
-
render(result)
|
|
292
|
-
} finally {
|
|
293
|
-
RENDER.hydration = previous
|
|
294
|
-
}
|
|
280
|
+
withoutHydration(() => render(result))
|
|
295
281
|
}
|
|
296
282
|
|
|
297
283
|
effect(() => {
|
|
@@ -304,8 +290,9 @@ export function awaitBlock(
|
|
|
304
290
|
firstHydrate()
|
|
305
291
|
return
|
|
306
292
|
}
|
|
307
|
-
|
|
308
|
-
|
|
293
|
+
const anchorNode = document.createTextNode('')
|
|
294
|
+
branch.anchor = anchorNode
|
|
295
|
+
parent.insertBefore(anchorNode, before)
|
|
309
296
|
}
|
|
310
297
|
/* Read the promise every subsequent run so an invalidate re-runs the block. ONLY this
|
|
311
298
|
read is tracked (the branch builds untracked via `scope`), so the block re-runs only
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/*
|
|
2
|
+
The parent half of a component `bind:prop={target}`: annotates the prop's value
|
|
3
|
+
thunk with a `set` write-back channel, so the same `() => value` entry every prop
|
|
4
|
+
compiles to ALSO carries a setter. A read-only consumer calls the thunk (`$props[key]?.()`)
|
|
5
|
+
and gets the value exactly as for a plain prop — the `set` simply rides along, invisible
|
|
6
|
+
to code that doesn't look for it (`restProps`/`mergeProps`/`spreadProps` pass the thunk
|
|
7
|
+
through untouched). The child upgrades the prop to a writable cell (`bindableProp`) only
|
|
8
|
+
when it writes or forwards it, and reaches this setter through `.set`.
|
|
9
|
+
|
|
10
|
+
`read`/`write` are the caller's lowered bind accessors — an lvalue target reads as
|
|
11
|
+
itself and writes by assignment, a `{ get, set }` accessor reads via `get()` and writes
|
|
12
|
+
via `set(next)` — so a component bind accepts the same targets an element bind does.
|
|
13
|
+
*/
|
|
14
|
+
// @documentation plumbing
|
|
15
|
+
export function bindProp<T>(
|
|
16
|
+
read: () => T,
|
|
17
|
+
write: (next: T) => void,
|
|
18
|
+
): (() => T) & { set: (next: T) => void } {
|
|
19
|
+
const thunk = read as (() => T) & { set: (next: T) => void }
|
|
20
|
+
thunk.set = write
|
|
21
|
+
return thunk
|
|
22
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { linked } from '../linked.ts'
|
|
2
|
+
import type { State } from '../runtime/types/State.ts'
|
|
3
|
+
import type { UiProps } from '../runtime/types/UiProps.ts'
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
The child half of a two-way prop: the writable cell a component gets for a prop it
|
|
7
|
+
WRITES or forwards to another `bind:` target. A prop the child only reads stays a
|
|
8
|
+
read-only derive (unchanged, cheap); this upgrade is emitted only for the props the
|
|
9
|
+
compiler sees written, so the common case pays nothing.
|
|
10
|
+
|
|
11
|
+
Whether the prop is actually two-way is decided at construction from what the parent
|
|
12
|
+
passed:
|
|
13
|
+
• Bound (`bind:prop` on the parent) — the value thunk carries a `set` (see `bindProp`),
|
|
14
|
+
so the cell is a pass-through accessor: reads pull the parent's value (tracking its
|
|
15
|
+
reactive source), writes go straight upstream. The parent's target is the single
|
|
16
|
+
source of truth, so no local copy is kept.
|
|
17
|
+
• Unbound (plain `prop={value}`, or the prop was never passed) — no setter exists, so
|
|
18
|
+
the cell degrades to a local `linked` cell seeded from the parent value: it reseeds
|
|
19
|
+
when the parent value changes and holds local writes in between. The component still
|
|
20
|
+
works standalone; its writes just don't flow anywhere.
|
|
21
|
+
|
|
22
|
+
`fallback` supplies the destructure's `= default` when the prop is absent/undefined,
|
|
23
|
+
matching the read-only derive's `?? default`.
|
|
24
|
+
*/
|
|
25
|
+
// @documentation plumbing
|
|
26
|
+
export function bindableProp<T>(props: UiProps, key: string, fallback?: () => T): State<T> {
|
|
27
|
+
const entry = (props as Record<string, unknown>)[key]
|
|
28
|
+
const read = (): T => {
|
|
29
|
+
const value = typeof entry === 'function' ? (entry as () => T)() : (entry as T)
|
|
30
|
+
return (value === undefined ? fallback?.() : value) as T
|
|
31
|
+
}
|
|
32
|
+
const setter =
|
|
33
|
+
typeof entry === 'function' ? (entry as { set?: (next: T) => void }).set : undefined
|
|
34
|
+
if (setter !== undefined) {
|
|
35
|
+
/* Bound: a pure pass-through to the parent's target — no local store. */
|
|
36
|
+
return {
|
|
37
|
+
get value(): T {
|
|
38
|
+
return read()
|
|
39
|
+
},
|
|
40
|
+
set value(next: T) {
|
|
41
|
+
setter(next)
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/* Unbound: a local reseeding cell — writes echo locally, reseed on parent change. */
|
|
46
|
+
return linked(read) as State<T>
|
|
47
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { isAsyncCell } from '../../shared/isAsyncCell.ts'
|
|
2
|
+
import type { AsyncComputed } from '../../shared/types/AsyncComputed.ts'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
Whether a control-flow subject (`{#if}`/`{#switch}`) is a still-loading async cell — no
|
|
6
|
+
value yet, no error (`AsyncComputed.pending()`). The compiler pairs it with the value read
|
|
7
|
+
for a bare async subject: while `cellPending(cell)` is true the block renders NO branch, so
|
|
8
|
+
"still loading" never collapses into the falsy branch (`{#if getProfile()}` does not flash
|
|
9
|
+
its `{:else}` before the promise settles). A settled falsy value (or a held value being
|
|
10
|
+
refreshed) is NOT pending, so it still routes to its branch. Non-async subjects (a plain
|
|
11
|
+
value, a sync `computed`) are never pending — the block reads them directly.
|
|
12
|
+
|
|
13
|
+
Reading `pending()` subscribes the block's effect to the cell's in-flight/has-value facets,
|
|
14
|
+
so the block re-renders when the value arrives. An errored cell is NOT pending: the paired
|
|
15
|
+
value read (`$$readCell`) then throws its `AsyncCellError`, routing the error to the nearest
|
|
16
|
+
`{#try}` exactly as a bare read does.
|
|
17
|
+
*/
|
|
18
|
+
// @documentation plumbing
|
|
19
|
+
export function cellPending(cell: unknown): boolean {
|
|
20
|
+
if (isAsyncCell(cell)) {
|
|
21
|
+
return (cell as AsyncComputed<unknown>).pending()
|
|
22
|
+
}
|
|
23
|
+
return false
|
|
24
|
+
}
|
|
@@ -10,14 +10,32 @@ export function discardBoundary(
|
|
|
10
10
|
closeData: string,
|
|
11
11
|
hydration: NonNullable<(typeof RENDER)['hydration']>,
|
|
12
12
|
): Node | null {
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
/* Nothing to discard (no open marker) — park the cursor and return, as before. */
|
|
14
|
+
if (open === null) {
|
|
15
|
+
hydration.next.set(parent, null)
|
|
16
|
+
return null
|
|
17
|
+
}
|
|
18
|
+
/* Locate the close marker WITHOUT mutating first. Removing as we walk would, on a
|
|
19
|
+
marker/id desync (the await/try block-id counter drifting between server and
|
|
20
|
+
client), delete every remaining sibling before discovering there is no match —
|
|
21
|
+
silently wiping unrelated later content. Throw AT the divergence like `outlet`
|
|
22
|
+
instead, rather than over-clear to end-of-parent. */
|
|
23
|
+
let close: Node | null = open
|
|
24
|
+
while (close !== null && (close as { data?: string }).data !== closeData) {
|
|
25
|
+
close = close.nextSibling
|
|
26
|
+
}
|
|
27
|
+
if (close === null) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`[abide] hydration desync: boundary open marker has no matching close "${closeData}" — the server DOM is truncated or the block id drifted.`,
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
const after = close.nextSibling
|
|
33
|
+
/* Remove open..close inclusive now that the range is known-bounded. */
|
|
34
|
+
let node: Node | null = open
|
|
15
35
|
while (node !== null) {
|
|
16
|
-
const next = node.nextSibling
|
|
17
|
-
const isClose = (node as { data?: string }).data === closeData
|
|
36
|
+
const next: Node | null = node.nextSibling
|
|
18
37
|
parent.removeChild(node)
|
|
19
|
-
if (
|
|
20
|
-
after = next
|
|
38
|
+
if (node === close) {
|
|
21
39
|
break
|
|
22
40
|
}
|
|
23
41
|
node = next
|
|
@@ -5,7 +5,8 @@ import { clearBetween } from './clearBetween.ts'
|
|
|
5
5
|
The standard teardown for a marker-bounded range (component, layout/page boundary,
|
|
6
6
|
slot): dispose its lexical scope (which stops the content's reactivity first, then its
|
|
7
7
|
nested children and capabilities), and clear the nodes between the markers — leaving the
|
|
8
|
-
markers in place so a
|
|
8
|
+
markers in place so a later rebuild (navigation, control-flow re-fill) refills the range.
|
|
9
|
+
Shared by every range mount
|
|
9
10
|
(`fillRange`, `mountRange`, `fillBoundary`) so the one disposer contract lives in a
|
|
10
11
|
single place.
|
|
11
12
|
*/
|
package/src/lib/ui/dom/each.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { effect } from '../effect.ts'
|
|
2
|
+
import { CURRENT_PATH } from '../runtime/CURRENT_PATH.ts'
|
|
2
3
|
import { claimChild } from '../runtime/claimChild.ts'
|
|
3
4
|
import { claimExpected } from '../runtime/claimExpected.ts'
|
|
4
5
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
5
6
|
import { scope } from '../runtime/scope.ts'
|
|
6
7
|
import { scopeGroup } from '../runtime/scopeGroup.ts'
|
|
7
8
|
import type { State } from '../runtime/types/State.ts'
|
|
9
|
+
import { withoutHydration } from '../runtime/withoutHydration.ts'
|
|
10
|
+
import { withPathFrom } from '../runtime/withPathFrom.ts'
|
|
8
11
|
import { state } from '../state.ts'
|
|
9
12
|
import { buildDetachedRange } from './buildDetachedRange.ts'
|
|
10
13
|
import { moveRange } from './moveRange.ts'
|
|
@@ -40,8 +43,17 @@ export function each<T>(
|
|
|
40
43
|
read them; a direct caller reads `item.value` / `index.value`. */
|
|
41
44
|
render: (parent: Node, item: State<T>, index: State<number>) => void,
|
|
42
45
|
before: Node | null = null,
|
|
46
|
+
/* Whether the each has an explicit `by` key. A KEYED row takes its stable key as its
|
|
47
|
+
render-path segment (survives reorders/reloads); a KEYLESS row (no `by`, so `keyOf`
|
|
48
|
+
returns the raw item identity — unstable) takes its POSITION instead, which is at least
|
|
49
|
+
deterministic SSR↔client for a single render. Absent → keyless (a direct caller). */
|
|
50
|
+
keyed = false,
|
|
43
51
|
): void {
|
|
44
52
|
const rows = new Map<string, EachRow>()
|
|
53
|
+
/* This each's render-path ancestry, captured at construction; every row builds under
|
|
54
|
+
`basePath/segment` so a component/cell in a row gets a stable id, re-established on a
|
|
55
|
+
post-mount reconcile (when the ambient path is gone). */
|
|
56
|
+
const basePath = CURRENT_PATH.current
|
|
45
57
|
/* Monotonic reconcile-pass id. Each reconcile stamps every surviving row with the
|
|
46
58
|
current pass, so prune is a `row.gen !== pass` int compare instead of building a
|
|
47
59
|
`Set` of present keys every reconcile. */
|
|
@@ -65,11 +77,19 @@ export function each<T>(
|
|
|
65
77
|
reconcile below) instead of rebuilding it. */
|
|
66
78
|
const cell = state(item) as State<unknown>
|
|
67
79
|
const indexCell = state(position)
|
|
80
|
+
/* Keyed rows key their path on the stable key; keyless on the position. */
|
|
81
|
+
const segment = keyed ? keyOf(item) : position
|
|
68
82
|
const hydration = RENDER.hydration
|
|
69
83
|
if (hydration !== undefined) {
|
|
70
84
|
const start = claimExpected(hydration, parent, 'each row start marker')
|
|
71
85
|
hydration.next.set(parent, start.nextSibling)
|
|
72
|
-
const dispose = group.track(
|
|
86
|
+
const dispose = group.track(
|
|
87
|
+
scope(() =>
|
|
88
|
+
withPathFrom(basePath, segment, () =>
|
|
89
|
+
render(parent, cell as State<T>, indexCell),
|
|
90
|
+
),
|
|
91
|
+
),
|
|
92
|
+
)
|
|
73
93
|
const end = claimExpected(hydration, parent, 'each row end marker')
|
|
74
94
|
hydration.next.set(parent, end.nextSibling)
|
|
75
95
|
return { start, end, dispose, cell, indexCell }
|
|
@@ -78,7 +98,7 @@ export function each<T>(
|
|
|
78
98
|
foreign namespace (so svg/math row children are namespaced, not built as HTML),
|
|
79
99
|
held in `pending` until placement inserts it. */
|
|
80
100
|
const { start, end, fragment, dispose } = buildDetachedRange(parent, (host) =>
|
|
81
|
-
render(host, cell as State<T>, indexCell),
|
|
101
|
+
withPathFrom(basePath, segment, () => render(host, cell as State<T>, indexCell)),
|
|
82
102
|
)
|
|
83
103
|
return { start, end, dispose: group.track(dispose), cell, indexCell, pending: fragment }
|
|
84
104
|
}
|
|
@@ -106,7 +126,9 @@ export function each<T>(
|
|
|
106
126
|
const hydration = RENDER.hydration
|
|
107
127
|
if (hydration !== undefined) {
|
|
108
128
|
let position = 0
|
|
109
|
-
|
|
129
|
+
/* An undefined source renders an empty list, not a throw — a `{#for x in promise}`
|
|
130
|
+
whose lifted source peeks undefined while pending (ADR-0032 D3). */
|
|
131
|
+
for (const item of items() ?? []) {
|
|
110
132
|
const key = keyOf(item)
|
|
111
133
|
const row = buildRow(item, position) // claims the SSR row where it sits
|
|
112
134
|
if (rows.has(key)) {
|
|
@@ -144,12 +166,11 @@ export function each<T>(
|
|
|
144
166
|
write that reconciles *mid-hydrate* (RENDER.hydration still active — e.g. a
|
|
145
167
|
page setting shared state during the hydrate pass) would otherwise make
|
|
146
168
|
buildRow and its inner row render claim SSR nodes that don't exist for a
|
|
147
|
-
freshly keyed row.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const list = Array.isArray(source) ? source : [...source]
|
|
169
|
+
freshly keyed row. withoutHydration restores the outer cursor after, so the
|
|
170
|
+
enclosing hydrate pass is untouched (mirrors awaitBlock/tryBlock). */
|
|
171
|
+
withoutHydration(() => {
|
|
172
|
+
/* Undefined source → empty list (see the hydration loop, ADR-0032 D3). */
|
|
173
|
+
const list = source == null ? [] : Array.isArray(source) ? source : [...source]
|
|
153
174
|
const keys = list.map(keyOf)
|
|
154
175
|
generation += 1
|
|
155
176
|
const pass = generation
|
|
@@ -183,6 +204,16 @@ export function each<T>(
|
|
|
183
204
|
for (let index = list.length - 1; index >= 0; index -= 1) {
|
|
184
205
|
let row = resolved[index]
|
|
185
206
|
if (row === undefined) {
|
|
207
|
+
/* Duplicate key within this list: a row for this key was already built
|
|
208
|
+
earlier in this pass. Building another and rows.set-ing it would
|
|
209
|
+
overwrite the map entry for the first, orphaning it — the prune loop
|
|
210
|
+
only reaches rows still in the map, so it would leak on screen forever.
|
|
211
|
+
Collapse to one row (like the hydration path above), skipping this
|
|
212
|
+
index rather than inserting an unreachable duplicate. */
|
|
213
|
+
const built = rows.get(keys[index] as string)
|
|
214
|
+
if (built !== undefined && built.gen === pass) {
|
|
215
|
+
continue
|
|
216
|
+
}
|
|
186
217
|
row = buildRow(list[index] as T, index)
|
|
187
218
|
row.gen = pass
|
|
188
219
|
rows.set(keys[index] as string, row)
|
|
@@ -197,8 +228,6 @@ export function each<T>(
|
|
|
197
228
|
placeBefore(row, cursor)
|
|
198
229
|
cursor = row.start
|
|
199
230
|
}
|
|
200
|
-
}
|
|
201
|
-
RENDER.hydration = previousHydration
|
|
202
|
-
}
|
|
231
|
+
})
|
|
203
232
|
})
|
|
204
233
|
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { effect } from '../effect.ts'
|
|
2
|
+
import { CURRENT_BOUNDARY } from '../runtime/CURRENT_BOUNDARY.ts'
|
|
3
|
+
import { CURRENT_PATH } from '../runtime/CURRENT_PATH.ts'
|
|
2
4
|
import { claimChild } from '../runtime/claimChild.ts'
|
|
3
5
|
import { generationGuard } from '../runtime/generationGuard.ts'
|
|
4
6
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
5
7
|
import { scopeGroup } from '../runtime/scopeGroup.ts'
|
|
6
8
|
import type { State } from '../runtime/types/State.ts'
|
|
9
|
+
import { withPathFrom } from '../runtime/withPathFrom.ts'
|
|
7
10
|
import { state } from '../state.ts'
|
|
8
11
|
import { buildDetachedRange } from './buildDetachedRange.ts'
|
|
9
12
|
import { removeRange } from './removeRange.ts'
|
|
@@ -32,11 +35,23 @@ export function eachAsync<T>(
|
|
|
32
35
|
`each`; the streaming runtime rebuilds the row on a re-yield rather than patching, and
|
|
33
36
|
the position is the stream arrival ordinal (a stream only appends, never reorders). */
|
|
34
37
|
render: (parent: Node, item: State<T>, index: State<number>) => void,
|
|
35
|
-
/* Absent → an iterator rejection
|
|
38
|
+
/* Absent → an iterator rejection routes to the enclosing {#try} boundary captured at build
|
|
39
|
+
(if any), else surfaces, instead of rendering a catch branch. */
|
|
36
40
|
renderCatch: ((parent: Node, error: unknown) => void) | undefined,
|
|
37
41
|
before: Node | null = null,
|
|
42
|
+
/* Explicit `by` key → the row keys its render-path segment on the stable key; else on the
|
|
43
|
+
stream arrival ordinal (a stream only appends, so the ordinal is stable). */
|
|
44
|
+
keyed = false,
|
|
38
45
|
): void {
|
|
39
46
|
const rows = new Map<string, EachRow>()
|
|
47
|
+
/* This each's render-path ancestry, captured at build; each row builds under
|
|
48
|
+
`basePath/segment` so a component/cell in a streamed row gets a stable id even though the
|
|
49
|
+
row arrives after the ambient path is gone. */
|
|
50
|
+
const basePath = CURRENT_PATH.current
|
|
51
|
+
/* The enclosing {#try} boundary ambient at BUILD (see `awaitBlock`). A catch-less rejection
|
|
52
|
+
arrives LATER from the async drain, after CURRENT_BOUNDARY has been restored, so capture
|
|
53
|
+
it into this closure now rather than read it at settle time. */
|
|
54
|
+
const capturedBoundary = CURRENT_BOUNDARY.current
|
|
40
55
|
/* Each row's (and the error branch's) scope, registered with the owner so they
|
|
41
56
|
dispose on owner teardown; the block's own teardown only stops the stream. */
|
|
42
57
|
const group = scopeGroup()
|
|
@@ -87,6 +102,17 @@ export function eachAsync<T>(
|
|
|
87
102
|
iterator?.return?.(undefined)?.catch(() => undefined) // close the superseded run's iterator before re-streaming
|
|
88
103
|
iterator = undefined
|
|
89
104
|
clearError() // a fresh run drops a prior error branch
|
|
105
|
+
/* A reseed streams a FRESH source (e.g. `room(newId)`), so the prior run's rows are stale.
|
|
106
|
+
Drop them now rather than relying on the drain's completion prune below — a never-
|
|
107
|
+
completing source (a socket) never reaches that prune, so a key the new stream doesn't
|
|
108
|
+
re-yield would otherwise leak in the DOM and the `rows` map forever (and mix the old
|
|
109
|
+
source's rows in with the new). Keys the new stream does re-yield rebuild fresh (the
|
|
110
|
+
block rebuilds on every re-yield regardless), so nothing reusable is lost. */
|
|
111
|
+
for (const [, row] of rows) {
|
|
112
|
+
row.dispose()
|
|
113
|
+
removeRange(row.start, row.end)
|
|
114
|
+
}
|
|
115
|
+
rows.clear()
|
|
90
116
|
const iterable = items() // read (subscribe) synchronously
|
|
91
117
|
const present = new Set<string>()
|
|
92
118
|
let arrivals = 0 // stream arrival ordinal → each row's index
|
|
@@ -110,9 +136,14 @@ export function eachAsync<T>(
|
|
|
110
136
|
const stale = rows.get(key)
|
|
111
137
|
const cell = state(result.value) as State<unknown>
|
|
112
138
|
const indexCell = state(arrivals)
|
|
139
|
+
const segment = keyed ? key : arrivals
|
|
113
140
|
arrivals += 1
|
|
114
141
|
rows.set(key, {
|
|
115
|
-
...insertRange((host) =>
|
|
142
|
+
...insertRange((host) =>
|
|
143
|
+
withPathFrom(basePath, segment, () =>
|
|
144
|
+
render(host, cell as State<T>, indexCell),
|
|
145
|
+
),
|
|
146
|
+
),
|
|
116
147
|
cell,
|
|
117
148
|
indexCell,
|
|
118
149
|
})
|
|
@@ -133,8 +164,13 @@ export function eachAsync<T>(
|
|
|
133
164
|
if (!guard.live(generationAtStart)) {
|
|
134
165
|
return
|
|
135
166
|
}
|
|
136
|
-
/* No catch branch →
|
|
167
|
+
/* No catch branch → route to the enclosing {#try} boundary captured at build if one
|
|
168
|
+
was ambient; otherwise surface the rejection (mirrors `<template await>`). */
|
|
137
169
|
if (renderCatch === undefined) {
|
|
170
|
+
if (capturedBoundary !== undefined) {
|
|
171
|
+
capturedBoundary.handle(error)
|
|
172
|
+
return
|
|
173
|
+
}
|
|
138
174
|
throw error
|
|
139
175
|
}
|
|
140
176
|
/* Keep the streamed rows; render the catch branch after them, at the anchor. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { RENDER } from '../runtime/RENDER.ts'
|
|
2
1
|
import { scope } from '../runtime/scope.ts'
|
|
2
|
+
import { withoutHydration } from '../runtime/withoutHydration.ts'
|
|
3
3
|
import { enterNamespace } from './enterNamespace.ts'
|
|
4
4
|
|
|
5
5
|
/*
|
|
@@ -29,15 +29,14 @@ export function fillBefore(end: Node, content: (into: Node) => void): () => void
|
|
|
29
29
|
return () => {}
|
|
30
30
|
}
|
|
31
31
|
const fragment = document.createDocumentFragment()
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
/* `fillBefore` is exclusively the CREATE path, so build with the claim cursor
|
|
33
|
+
cleared (see withoutHydration): a rebuild that runs mid-hydrate must not claim
|
|
34
|
+
SSR nodes that don't exist for this fresh content. */
|
|
35
|
+
return withoutHydration(() => {
|
|
35
36
|
/* Build under the insertion parent's foreign namespace (if any), so foreign
|
|
36
37
|
elements built into the fragment are namespaced off `end`'s live parent. */
|
|
37
38
|
const dispose = enterNamespace(end.parentNode ?? end, () => scope(() => content(fragment)))
|
|
38
39
|
;(end.parentNode ?? end).insertBefore(fragment, end)
|
|
39
40
|
return dispose
|
|
40
|
-
}
|
|
41
|
-
RENDER.hydration = previousHydration
|
|
42
|
-
}
|
|
41
|
+
})
|
|
43
42
|
}
|
|
@@ -24,15 +24,14 @@ export function fillBoundary(
|
|
|
24
24
|
close: Comment,
|
|
25
25
|
build: (host: Node, props?: UiProps) => void,
|
|
26
26
|
props: UiProps | undefined,
|
|
27
|
-
label: string | undefined,
|
|
28
27
|
): { dispose: () => void } {
|
|
29
28
|
const hydration = RENDER.hydration
|
|
30
29
|
if (hydration === undefined) {
|
|
31
|
-
return fillRange(open, close, build, props
|
|
30
|
+
return fillRange(open, close, build, props)
|
|
32
31
|
}
|
|
33
32
|
/* Hydrate: adopt the server content between the markers in place. */
|
|
34
33
|
const parent = open.parentNode as Node
|
|
35
34
|
hydration.next.set(parent, open.nextSibling)
|
|
36
|
-
const scoped = withScope(
|
|
35
|
+
const scoped = withScope(() => scope(() => build(parent, props)))
|
|
37
36
|
return { dispose: disposeRange(scoped, open, close) }
|
|
38
37
|
}
|