@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
|
@@ -6,7 +6,7 @@ them. The shell must exist before its contents are filled so a back-reference to
|
|
|
6
6
|
ancestor resolves to the already-allocated object — which is what reconnects a cycle.
|
|
7
7
|
Inline primitives need no shell; they decode directly where they sit.
|
|
8
8
|
*/
|
|
9
|
-
import { REF_JSON_TAGS } from './REF_JSON_TAGS.ts'
|
|
9
|
+
import { REF_JSON_NUMBER_TOKENS, REF_JSON_TAGS } from './REF_JSON_TAGS.ts'
|
|
10
10
|
import { setOwnProperty } from './setOwnProperty.ts'
|
|
11
11
|
|
|
12
12
|
export function decodeRefJson(text: string): unknown {
|
|
@@ -99,13 +99,13 @@ function fillShell(slot: unknown, shell: unknown, shells: unknown[]): void {
|
|
|
99
99
|
|
|
100
100
|
// Reverse of encodeRefJson's numberToken.
|
|
101
101
|
function decodeNumberToken(token: string): number {
|
|
102
|
-
if (token ===
|
|
102
|
+
if (token === REF_JSON_NUMBER_TOKENS.NAN) {
|
|
103
103
|
return Number.NaN
|
|
104
104
|
}
|
|
105
|
-
if (token ===
|
|
105
|
+
if (token === REF_JSON_NUMBER_TOKENS.INFINITY) {
|
|
106
106
|
return Number.POSITIVE_INFINITY
|
|
107
107
|
}
|
|
108
|
-
if (token ===
|
|
108
|
+
if (token === REF_JSON_NUMBER_TOKENS.NEG_INFINITY) {
|
|
109
109
|
return Number.NEGATIVE_INFINITY
|
|
110
110
|
}
|
|
111
111
|
return -0
|
|
@@ -18,7 +18,7 @@ propagation), and the success path types as Promise<Return> cleanly.
|
|
|
18
18
|
Streaming Content-Types (SSE / JSONL / NDJSON) throw a clear error
|
|
19
19
|
rather than silently doing the wrong thing: response.text() would hang
|
|
20
20
|
forever on a never-ending body and response.json() would fail mid-parse.
|
|
21
|
-
A streaming rpc's bare call already returns a
|
|
21
|
+
A streaming rpc's bare call already returns a NamedAsyncIterable (the type makes
|
|
22
22
|
`await fn(args)` a compile error), so this is a backstop for the paths that
|
|
23
23
|
still decode a raw Response — `cache()` and the one-shot stream reader. The
|
|
24
24
|
error points callers at the right tools: `state(fn(args))` for a reactive
|
|
@@ -39,7 +39,7 @@ export async function decodeResponse(response: Response): Promise<unknown> {
|
|
|
39
39
|
const kind = contentBodyKind(contentType)
|
|
40
40
|
if (kind === 'streaming') {
|
|
41
41
|
throw new Error(
|
|
42
|
-
`[abide] response at ${response.url} is a stream (${contentType}) — a streaming rpc's bare call already returns a
|
|
42
|
+
`[abide] response at ${response.url} is a stream (${contentType}) — a streaming rpc's bare call already returns a NamedAsyncIterable: use state(fn(args)) for a reactive view or \`for await (… of fn(args))\` for iteration, not await/cache()`,
|
|
43
43
|
)
|
|
44
44
|
}
|
|
45
45
|
/* json/text go through the shared mapping warmValueFromSnapshot also uses, so a warm
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/*
|
|
2
|
+
The single decode point for the "is this ref-json?" policy: abide's own client speaks
|
|
3
|
+
ref-json (encodeRefJson — restores the cycles/shared refs/structured types plain JSON
|
|
4
|
+
can't carry); a foreign client (curl, an OpenAPI SDK, a raw ws client) speaks ordinary
|
|
5
|
+
JSON. Both the rpc body face and the socket-frame face funnel through here so the
|
|
6
|
+
codec fork lives in exactly one place instead of being re-derived per face.
|
|
7
|
+
|
|
8
|
+
The discriminator is a genuine per-face input difference, kept thin at the call site:
|
|
9
|
+
|
|
10
|
+
- `isRefJson === true` — the rpc client stamped REF_JSON_HEADER, so decode as ref-json.
|
|
11
|
+
- `isRefJson === false` — no header (a non-abide rpc body), so plain JSON.parse.
|
|
12
|
+
- `isRefJson === undefined` — no header is available at all (a ws frame carries none),
|
|
13
|
+
so sniff: try ref-json, fall back to plain JSON. The ref-json `[rootValue, slots]`
|
|
14
|
+
envelope is ambiguous with a legitimate 2-element array, but a frame is always an
|
|
15
|
+
object, so a foreign plain-JSON frame makes decodeRefJson throw and we fall back.
|
|
16
|
+
|
|
17
|
+
Throws on a malformed payload (JSON.parse's own SyntaxError) — the caller decides
|
|
18
|
+
whether that's a 400 (rpc) or a dropped frame (ws).
|
|
19
|
+
*/
|
|
20
|
+
import { decodeRefJson } from './decodeRefJson.ts'
|
|
21
|
+
|
|
22
|
+
export function decodeWireBody(text: string, isRefJson: boolean | undefined): unknown {
|
|
23
|
+
if (isRefJson === true) {
|
|
24
|
+
return decodeRefJson(text)
|
|
25
|
+
}
|
|
26
|
+
if (isRefJson === false) {
|
|
27
|
+
return JSON.parse(text)
|
|
28
|
+
}
|
|
29
|
+
// No discriminator (a ws frame has no header): try ref-json, fall back to plain JSON.
|
|
30
|
+
try {
|
|
31
|
+
return decodeRefJson(text)
|
|
32
|
+
} catch {
|
|
33
|
+
return JSON.parse(text)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { HTTP_METHODS } from './HTTP_METHODS.ts'
|
|
1
2
|
import type { HttpMethod } from './types/HttpMethod.ts'
|
|
2
3
|
|
|
3
4
|
/*
|
|
@@ -9,8 +10,14 @@ undefined when no rpc export matches — the caller skips the file rather
|
|
|
9
10
|
than guessing. Used by the rpc.d.ts codegen to type url() against
|
|
10
11
|
query-carrying rpcs; matching the same convention the bundler rewrites
|
|
11
12
|
keeps the two from drifting.
|
|
13
|
+
|
|
14
|
+
The FAIL-OPEN fallback for writeRpcDts's method resolution (ADR-0025 D2/D3): when a
|
|
15
|
+
warm server program is present the method is read off the export's helper SYMBOL
|
|
16
|
+
(alias/re-export-aware), and this regex — keyed on the literal helper name — is used
|
|
17
|
+
only when no program built or the query didn't resolve. Byte-identical to the pre-ADR
|
|
18
|
+
path when no program is present.
|
|
12
19
|
*/
|
|
13
|
-
const RPC_EXPORT =
|
|
20
|
+
const RPC_EXPORT = new RegExp(`export\\s+const\\s+\\w+\\s*=\\s*(${HTTP_METHODS.join('|')})\\s*[<(]`)
|
|
14
21
|
|
|
15
22
|
export function detectRpcMethod(source: string): HttpMethod | undefined {
|
|
16
23
|
return (source.match(RPC_EXPORT)?.[1] as HttpMethod | undefined) ?? undefined
|
package/src/lib/shared/done.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { tailProbeSlot } from './tailProbeSlot.ts'
|
|
2
|
-
import type {
|
|
2
|
+
import type { NamedAsyncIterable } from './types/NamedAsyncIterable.ts'
|
|
3
3
|
|
|
4
4
|
/*
|
|
5
5
|
Reactive terminal-state reader for a stream: true once the source has closed (its
|
|
@@ -9,6 +9,12 @@ tail entry status === 'done'). Stream-only — a cache entry's "done" is just
|
|
|
9
9
|
registered (server render, or tail never imported) reads as not-done.
|
|
10
10
|
*/
|
|
11
11
|
// @documentation probes
|
|
12
|
-
export function done(subscribable:
|
|
12
|
+
export function done(subscribable: NamedAsyncIterable<unknown>): boolean {
|
|
13
|
+
/* Null-tolerant: a promise/iterable subexpression peek-lifts to `undefined` while
|
|
14
|
+
pending (ADR-0032), so `done(getFeed())` in a template hands us `undefined` on the
|
|
15
|
+
first pass — treat a missing source as not-done rather than throwing. */
|
|
16
|
+
if (subscribable == null) {
|
|
17
|
+
return false
|
|
18
|
+
}
|
|
13
19
|
return tailProbeSlot.probe?.(subscribable.name)?.done ?? false
|
|
14
20
|
}
|
|
@@ -11,7 +11,7 @@ symbols can't be serialised and encode as undefined, matching JSON.stringify. Ou
|
|
|
11
11
|
shape is `[rootValue, slots]`. Decode with decodeRefJson. Not streaming — the whole
|
|
12
12
|
graph is walked up front.
|
|
13
13
|
*/
|
|
14
|
-
import { REF_JSON_TAGS } from './REF_JSON_TAGS.ts'
|
|
14
|
+
import { REF_JSON_NUMBER_TOKENS, REF_JSON_TAGS } from './REF_JSON_TAGS.ts'
|
|
15
15
|
import { setOwnProperty } from './setOwnProperty.ts'
|
|
16
16
|
|
|
17
17
|
export function encodeRefJson(value: unknown): string {
|
|
@@ -99,10 +99,10 @@ export function encodeRefJson(value: unknown): string {
|
|
|
99
99
|
// Stable token for the numbers JSON can't carry.
|
|
100
100
|
function numberToken(value: number): string {
|
|
101
101
|
if (Object.is(value, -0)) {
|
|
102
|
-
return
|
|
102
|
+
return REF_JSON_NUMBER_TOKENS.NEG_ZERO
|
|
103
103
|
}
|
|
104
104
|
if (Number.isNaN(value)) {
|
|
105
|
-
return
|
|
105
|
+
return REF_JSON_NUMBER_TOKENS.NAN
|
|
106
106
|
}
|
|
107
|
-
return value > 0 ?
|
|
107
|
+
return value > 0 ? REF_JSON_NUMBER_TOKENS.INFINITY : REF_JSON_NUMBER_TOKENS.NEG_INFINITY
|
|
108
108
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/*
|
|
2
|
+
The single honest-JSON ENCODE point for the REST response faces (`json()` and the
|
|
3
|
+
per-frame `sse()` body). JSON.stringify driven by wireJsonReplacer so a structured
|
|
4
|
+
runtime value crosses as honest JSON — a `Set` → array, a `Map` → `[K,V]` entries, a
|
|
5
|
+
`bigint` → digit string — instead of the silent `{}` a bare Set stringifies to or the
|
|
6
|
+
throw a bare bigint triggers. Value-directed (not type-directed), so every consumer
|
|
7
|
+
reads the same honest JSON the projected schema (jsonSchemaForType) describes: abide's
|
|
8
|
+
own client and a foreign EventSource/curl alike.
|
|
9
|
+
|
|
10
|
+
This is the REST counterpart to the ws path's encodeRefJson: it preserves the
|
|
11
|
+
structured values losslessly the way ref-json does, but without the `[rootValue, slots]`
|
|
12
|
+
envelope a foreign REST consumer (a naive EventSource + JSON.parse) can't read.
|
|
13
|
+
*/
|
|
14
|
+
import { wireJsonReplacer } from './wireJsonReplacer.ts'
|
|
15
|
+
|
|
16
|
+
export function encodeWireBody(value: unknown): string {
|
|
17
|
+
return JSON.stringify(value, wireJsonReplacer)
|
|
18
|
+
}
|
|
@@ -2,9 +2,10 @@ import type { BuildOutput } from 'bun'
|
|
|
2
2
|
import { abideLog } from './abideLog.ts'
|
|
3
3
|
|
|
4
4
|
/*
|
|
5
|
-
On a failed Bun.build(), logs each diagnostic and exits non-zero.
|
|
6
|
-
build
|
|
7
|
-
|
|
5
|
+
On a failed Bun.build(), logs each diagnostic and exits non-zero. The one-shot
|
|
6
|
+
build entrypoints reach it through `buildArtifact` (build-or-die); the
|
|
7
|
+
incremental client build (build.ts) calls it directly on its conditional-exit
|
|
8
|
+
path. One reporter so build failure can't drift between them.
|
|
8
9
|
*/
|
|
9
10
|
export function exitOnBuildFailure(result: BuildOutput): void {
|
|
10
11
|
if (result.success) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { StandardSchemaV1 } from '
|
|
1
|
+
import type { StandardSchemaV1 } from './types/StandardSchemaV1.ts'
|
|
2
2
|
|
|
3
3
|
/*
|
|
4
4
|
Flattens Standard Schema issues into a top-level-field → first-message map, the
|
|
@@ -7,6 +7,10 @@ bare `PropertyKey` or a `{ key }` wrapper (the spec allows both), so normalize
|
|
|
7
7
|
before reading. First message wins per field (a form shows one per input); the
|
|
8
8
|
raw `issues` keep every message and the full path. Issues with no string field
|
|
9
9
|
(root-level refinements) are omitted — they live only in `issues`.
|
|
10
|
+
|
|
11
|
+
Isomorphic (only the type-only StandardSchemaV1 import): the server 422 path
|
|
12
|
+
(validationError.ts) and the client pre-flight (validationHttpError.ts, ADR-0026)
|
|
13
|
+
both build the identical `fields` map from the same issue list.
|
|
10
14
|
*/
|
|
11
15
|
export function fieldErrorsFromIssues(
|
|
12
16
|
issues: readonly StandardSchemaV1.Issue[],
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { Glob } from 'bun'
|
|
3
|
+
import { abideImportName } from './abideImportName.ts'
|
|
4
|
+
import { abideLog } from './abideLog.ts'
|
|
5
|
+
import { rpcServerForRoot } from './rpcServerForRoot.ts'
|
|
6
|
+
import { scanPages } from './scanPages.ts'
|
|
7
|
+
import { writeHealthDts } from './writeHealthDts.ts'
|
|
8
|
+
import { writePublicAssetsDts } from './writePublicAssetsDts.ts'
|
|
9
|
+
import { writeRoutesDts } from './writeRoutesDts.ts'
|
|
10
|
+
import { writeRpcDts } from './writeRpcDts.ts'
|
|
11
|
+
import { writeTestRpcDts } from './writeTestRpcDts.ts'
|
|
12
|
+
import { writeTestSocketsDts } from './writeTestSocketsDts.ts'
|
|
13
|
+
|
|
14
|
+
/* Globs a project subtree once, returning [] when the dir is absent so an app
|
|
15
|
+
missing a folder still generates the same (mirrors the plugin's scanDir /
|
|
16
|
+
scanPublicOnce). */
|
|
17
|
+
async function scanDir(dir: string, pattern: string, onlyFiles: boolean): Promise<string[]> {
|
|
18
|
+
if (!existsSync(dir)) {
|
|
19
|
+
return []
|
|
20
|
+
}
|
|
21
|
+
return await Array.fromAsync(new Glob(pattern).scan({ cwd: dir, onlyFiles }))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/*
|
|
25
|
+
The single producer of the dev-editor `.d.ts` artifacts under src/.abide
|
|
26
|
+
(routes/rpc/testRpc/testSockets/health/publicAssets). Owned by build() and run
|
|
27
|
+
CONCURRENTLY with Bun.build so its one rpc `ts.Program` build (ADR-0025 D2's
|
|
28
|
+
alias-aware method detection) hides under the bundle. The runtime server (dev
|
|
29
|
+
worker / `abide start`) no longer generates any `.d.ts` at boot — that removed
|
|
30
|
+
the per-boot program build. Every `.d.ts` here is a non-critical editor
|
|
31
|
+
convenience (CI typechecks via shadow programs, not these files), so the whole
|
|
32
|
+
body is guarded: a codegen hiccup LOGS and resolves rather than failing the
|
|
33
|
+
build.
|
|
34
|
+
*/
|
|
35
|
+
export async function generateDeclarations({ cwd }: { cwd: string }): Promise<void> {
|
|
36
|
+
try {
|
|
37
|
+
const rpcDir = `${cwd}/src/server/rpc`
|
|
38
|
+
const socketsDir = `${cwd}/src/server/sockets`
|
|
39
|
+
const pagesDir = `${cwd}/src/ui/pages`
|
|
40
|
+
const publicDir = `${cwd}/src/ui/public`
|
|
41
|
+
const [importName, pagesScan, rpcFiles, socketFiles, publicFiles] = await Promise.all([
|
|
42
|
+
abideImportName(cwd),
|
|
43
|
+
scanPages(pagesDir),
|
|
44
|
+
scanDir(rpcDir, '**/*.ts', false),
|
|
45
|
+
scanDir(socketsDir, '**/*.ts', false),
|
|
46
|
+
scanDir(publicDir, '**/*', true),
|
|
47
|
+
])
|
|
48
|
+
/* One program per build (ADR-0025 D2): resolve each rpc's method off its export helper
|
|
49
|
+
SYMBOL so an aliased/re-exported helper resolves where writeRpcDts's regex misses it.
|
|
50
|
+
Fails open to undefined, in which case writeRpcDts falls back to its own regex. */
|
|
51
|
+
const rpcServerProgram = rpcServerForRoot(new Map(), cwd, rpcDir)
|
|
52
|
+
const hasAppModule = existsSync(`${cwd}/src/app.ts`)
|
|
53
|
+
await Promise.all([
|
|
54
|
+
writeRoutesDts({ cwd, pageFiles: pagesScan.pageFiles, importName }),
|
|
55
|
+
writeRpcDts({
|
|
56
|
+
cwd,
|
|
57
|
+
rpcDir,
|
|
58
|
+
rpcFiles,
|
|
59
|
+
importName,
|
|
60
|
+
methodForModule: rpcServerProgram
|
|
61
|
+
? (modulePath) => rpcServerProgram.methodForModule(modulePath)
|
|
62
|
+
: undefined,
|
|
63
|
+
}),
|
|
64
|
+
writeTestRpcDts({ cwd, rpcFiles, importName }),
|
|
65
|
+
writeTestSocketsDts({ cwd, socketFiles, importName }),
|
|
66
|
+
writeHealthDts({ cwd, hasAppModule, importName }),
|
|
67
|
+
writePublicAssetsDts({ cwd, publicFiles, importName }),
|
|
68
|
+
])
|
|
69
|
+
} catch (error) {
|
|
70
|
+
abideLog.error(error)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { CacheEntry } from './types/CacheEntry.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Whether a cache entry carries a wire request that can be SEEDED into the SSR warm
|
|
5
|
+
snapshot — any method, so an inline (bare smart read) call hydrates warm on the
|
|
6
|
+
client instead of re-firing after render, regardless of verb. A value called
|
|
7
|
+
inline during render is being used as a read; the seed carries the SSR-computed
|
|
8
|
+
body so the client reads it warm rather than issuing a second call. Producer
|
|
9
|
+
entries (no request) and stream cells (a `NamedAsyncIterable` cell holds no wire
|
|
10
|
+
request) fail it — neither can round-trip a url/method/body through the snapshot.
|
|
11
|
+
|
|
12
|
+
This gates SEEDING, not re-firing. Re-firing unprompted (invalidate-policy
|
|
13
|
+
refetch, snapshot replay) stays GET-only (REPLAYABLE_METHODS / isReplayableMethod
|
|
14
|
+
in cache.ts) so a genuine write coalesces-only and stays re-submittable — a
|
|
15
|
+
seeded POST is read warm once, never auto-replayed.
|
|
16
|
+
|
|
17
|
+
The request half of snapshot shippability, factored out so the two gates that
|
|
18
|
+
need it cannot drift: `snapshotShippable` composes it with `settled` for the
|
|
19
|
+
store-filter sites, and `snapshotEntryFromCache` calls it alone — its
|
|
20
|
+
streaming-drain caller hands it still-pending entries it then awaits, so it can't
|
|
21
|
+
require `settled` up front.
|
|
22
|
+
*/
|
|
23
|
+
export function hasSeedableRequest(entry: CacheEntry): boolean {
|
|
24
|
+
return entry.request !== undefined
|
|
25
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { cacheStores } from './cacheStores.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
The one owner of the hydration pass lifecycle — the timing window during which the
|
|
5
|
+
client adopts the server-rendered DOM (the router's hydrate branch and `hydrate`).
|
|
6
|
+
|
|
7
|
+
Why it exists: the server materializes no cache value (materializeRetained/
|
|
8
|
+
cacheEntryFromSnapshot are client-only), so server-side peek is uniformly undefined
|
|
9
|
+
and the SSR render always shows the fallback. A snapshot-seeded warm value surfacing
|
|
10
|
+
DURING hydration would diverge from that server text and corrupt the claimed text
|
|
11
|
+
node (assertClaimedText desync), so `peek` withholds it while `active` is true and
|
|
12
|
+
`wake()` re-runs the withheld scopes once the pass ends.
|
|
13
|
+
|
|
14
|
+
`active` is a plain boolean — false on the server (no hydration) and after boot —
|
|
15
|
+
readable by the withhold check. `enter`/`exit` bracket a pass with save/restore
|
|
16
|
+
nesting (a `depth` counter) so a child hydrate can't clear an outer pass early;
|
|
17
|
+
`active` stays true until the OUTERMOST `exit`, which then fires `wake`.
|
|
18
|
+
|
|
19
|
+
`wake` marks every live cache entry's key: a peek scope taps the exact call key's
|
|
20
|
+
lifecycle channel (peek's trackLifecycle), so marking every live entry's key wakes
|
|
21
|
+
the scopes that withheld — cheap, one-time, and harmless for non-peek lifecycle
|
|
22
|
+
readers (they recompute the same value). Marks by key, not the bare store-wide
|
|
23
|
+
channel, because a peek(fn, args) scope taps the per-key prefix channel, which a
|
|
24
|
+
keyless mark misses.
|
|
25
|
+
*/
|
|
26
|
+
export const hydrationWindow = {
|
|
27
|
+
active: false,
|
|
28
|
+
depth: 0,
|
|
29
|
+
/* Open a pass. Nested enters raise the depth without re-firing anything. */
|
|
30
|
+
enter(): void {
|
|
31
|
+
hydrationWindow.depth += 1
|
|
32
|
+
hydrationWindow.active = true
|
|
33
|
+
},
|
|
34
|
+
/* Close a pass. Only the OUTERMOST unwind (depth back to zero) clears `active`
|
|
35
|
+
and wakes the scopes the pass withheld — a nested child exit must not. */
|
|
36
|
+
exit(): void {
|
|
37
|
+
hydrationWindow.depth -= 1
|
|
38
|
+
if (hydrationWindow.depth === 0) {
|
|
39
|
+
hydrationWindow.active = false
|
|
40
|
+
hydrationWindow.wake()
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
/* Re-run the peeks the pass withheld, now that the retained value is congruent
|
|
44
|
+
to show. Mark every live entry's key across both cache stores. */
|
|
45
|
+
wake(): void {
|
|
46
|
+
const stores = cacheStores()
|
|
47
|
+
for (const store of stores) {
|
|
48
|
+
for (const key of store.entries.keys()) {
|
|
49
|
+
store.markLifecycle(key)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ASYNC_CELL } from './ASYNC_CELL.ts'
|
|
2
|
+
import type { AsyncComputed } from './types/AsyncComputed.ts'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
True for an async cell's facet — the brand the probe family tests to route a cell
|
|
6
|
+
(`AsyncComputed`/`AsyncState`) to its own `peek`/`pending`/`refreshing`/`refresh`
|
|
7
|
+
methods instead of the cache/stream registries.
|
|
8
|
+
*/
|
|
9
|
+
export function isAsyncCell(value: unknown): value is AsyncComputed<unknown> {
|
|
10
|
+
return typeof value === 'object' && value !== null && ASYNC_CELL in value
|
|
11
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Any async iterable — a stream cell auto-tracks by consuming `Symbol.asyncIterator` alone, so
|
|
3
|
+
it needs no `name` (unlike `isSubscribable`, which the probe registry keys on the name). A
|
|
4
|
+
plain `async function*` generator qualifies here where the named check would reject it.
|
|
5
|
+
*/
|
|
6
|
+
export function isAsyncIterable(value: unknown): value is AsyncIterable<unknown> {
|
|
7
|
+
return value != null && typeof (value as any)[Symbol.asyncIterator] === 'function'
|
|
8
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { NamedAsyncIterable } from './types/NamedAsyncIterable.ts'
|
|
2
2
|
|
|
3
3
|
/*
|
|
4
|
-
A
|
|
4
|
+
A NamedAsyncIterable is an AsyncIterable carrying a `name` — distinguishes a stream argument from
|
|
5
5
|
the other probe selector shapes (callables and `{ tags }` objects, neither of
|
|
6
6
|
which carries Symbol.asyncIterator).
|
|
7
7
|
*/
|
|
8
|
-
export function isSubscribable(value: unknown): value is
|
|
8
|
+
export function isSubscribable(value: unknown): value is NamedAsyncIterable<unknown> {
|
|
9
9
|
return (
|
|
10
10
|
typeof value === 'object' &&
|
|
11
11
|
value !== null &&
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/* True for a Promise-like value — anything carrying a callable `then`. Used to tell an
|
|
2
|
+
async cell's unwrap path (a promise to await) from a synchronously-produced value. */
|
|
3
|
+
export function isThenable(value: unknown): value is PromiseLike<unknown> {
|
|
4
|
+
return (
|
|
5
|
+
value !== null &&
|
|
6
|
+
(typeof value === 'object' || typeof value === 'function') &&
|
|
7
|
+
typeof (value as { then?: unknown }).then === 'function'
|
|
8
|
+
)
|
|
9
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import ts from 'typescript'
|
|
2
|
+
|
|
3
|
+
const STRINGISH_FLAGS =
|
|
4
|
+
ts.TypeFlags.String | ts.TypeFlags.TemplateLiteral | ts.TypeFlags.StringMapping
|
|
5
|
+
const NUMERIC_FLAGS = ts.TypeFlags.Number | ts.TypeFlags.NumberLiteral
|
|
6
|
+
const NULLISH_FLAGS = ts.TypeFlags.Undefined | ts.TypeFlags.Null | ts.TypeFlags.Void
|
|
7
|
+
const OPTIONALITY_FLAGS = ts.TypeFlags.Undefined | ts.TypeFlags.Void
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
Projects a `ts.Type` to a JSON Schema (ADR-0030 D2) — the complement of jsonSchemaForSchema, which
|
|
11
|
+
projects a runtime VALIDATOR. This projects a bare typed handler return so an rpc's OpenAPI 200 / MCP
|
|
12
|
+
outputSchema is generated from the handler's return type when no `schemas.output` validator is
|
|
13
|
+
declared. Covers the subset abide's `json()`/`jsonl()` actually emit (primitives, literals, objects,
|
|
14
|
+
arrays, tuples, unions, Date, records). Returns the same `Record<string, unknown>` shape
|
|
15
|
+
jsonSchemaForSchema does, so the projected schema is drop-in for every surface.
|
|
16
|
+
|
|
17
|
+
Fails OPEN: an unsupported / unresolvable type projects to `{}` (permissive "any JSON") and, at the
|
|
18
|
+
top level, a bare `{}` collapses to undefined so the surface omits the schema — exactly today's
|
|
19
|
+
behavior with no `schemas.output`. Never throws, so a projection gap can't break a build. The `seen`
|
|
20
|
+
set guards a recursive type (a tree node referencing itself) from infinite-looping — a revisit
|
|
21
|
+
emits `{}` to break the cycle.
|
|
22
|
+
*/
|
|
23
|
+
export function jsonSchemaForType(
|
|
24
|
+
checker: ts.TypeChecker,
|
|
25
|
+
type: ts.Type,
|
|
26
|
+
): Record<string, unknown> | undefined {
|
|
27
|
+
const schema = project(checker, type, new Set())
|
|
28
|
+
// A bare permissive object carries no shape hint — omit it so the surface behaves as if unschema'd.
|
|
29
|
+
return Object.keys(schema).length === 0 ? undefined : schema
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* The recursive core. Always returns a schema object (never undefined) — `{}` is the permissive
|
|
33
|
+
fail-open value for anything unsupported, so every branch can compose the result inline. */
|
|
34
|
+
function project(
|
|
35
|
+
checker: ts.TypeChecker,
|
|
36
|
+
type: ts.Type,
|
|
37
|
+
seen: Set<ts.Type>,
|
|
38
|
+
): Record<string, unknown> {
|
|
39
|
+
// any/unknown carry no shape — permissive.
|
|
40
|
+
if ((type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) {
|
|
41
|
+
return {}
|
|
42
|
+
}
|
|
43
|
+
if (type.isUnion()) {
|
|
44
|
+
return projectUnion(checker, type, seen)
|
|
45
|
+
}
|
|
46
|
+
// An intersection rarely projects cleanly to a single JSON Schema — punt permissive (ADR-0030 scope).
|
|
47
|
+
if (type.isIntersection()) {
|
|
48
|
+
return {}
|
|
49
|
+
}
|
|
50
|
+
// `boolean` is a `true | false` union caught above; a lone literal / intrinsic lands here.
|
|
51
|
+
if ((type.flags & (ts.TypeFlags.Boolean | ts.TypeFlags.BooleanLiteral)) !== 0) {
|
|
52
|
+
return { type: 'boolean' }
|
|
53
|
+
}
|
|
54
|
+
if ((type.flags & ts.TypeFlags.StringLiteral) !== 0) {
|
|
55
|
+
return { type: 'string', const: (type as ts.StringLiteralType).value }
|
|
56
|
+
}
|
|
57
|
+
// A template-literal / mapped string projects to a plain string (its pattern isn't modeled).
|
|
58
|
+
if ((type.flags & STRINGISH_FLAGS) !== 0) {
|
|
59
|
+
return { type: 'string' }
|
|
60
|
+
}
|
|
61
|
+
if ((type.flags & NUMERIC_FLAGS) !== 0) {
|
|
62
|
+
return { type: 'number' }
|
|
63
|
+
}
|
|
64
|
+
// JSON Schema has no bigint; matches the ADR-0029 wire representation (a bigint rides as a string).
|
|
65
|
+
if ((type.flags & (ts.TypeFlags.BigInt | ts.TypeFlags.BigIntLiteral)) !== 0) {
|
|
66
|
+
return { type: 'string' }
|
|
67
|
+
}
|
|
68
|
+
if ((type.flags & ts.TypeFlags.Null) !== 0) {
|
|
69
|
+
return { type: 'null' }
|
|
70
|
+
}
|
|
71
|
+
if ((type.flags & OPTIONALITY_FLAGS) !== 0) {
|
|
72
|
+
return {}
|
|
73
|
+
}
|
|
74
|
+
// A function/callable body isn't JSON — punt permissive.
|
|
75
|
+
if (type.getCallSignatures().length > 0) {
|
|
76
|
+
return {}
|
|
77
|
+
}
|
|
78
|
+
const symbolName = type.getSymbol()?.name
|
|
79
|
+
if (symbolName === 'Date') {
|
|
80
|
+
return { type: 'string', format: 'date-time' }
|
|
81
|
+
}
|
|
82
|
+
if (symbolName === 'Array' || symbolName === 'ReadonlyArray') {
|
|
83
|
+
return projectArray(checker, type, seen)
|
|
84
|
+
}
|
|
85
|
+
// A `Set`/`Map` rides the wire as a JSON array (ADR-0029 output encode), so the projected schema
|
|
86
|
+
// MUST agree with those bytes — a `Set<T>` is `T[]`, a `Map<K,V>` is `[K,V][]` entries.
|
|
87
|
+
if (symbolName === 'Set' || symbolName === 'ReadonlySet') {
|
|
88
|
+
return projectSet(checker, type, seen)
|
|
89
|
+
}
|
|
90
|
+
if (symbolName === 'Map' || symbolName === 'ReadonlyMap') {
|
|
91
|
+
return projectMap(checker, type, seen)
|
|
92
|
+
}
|
|
93
|
+
if (isTupleType(type)) {
|
|
94
|
+
return projectTuple(checker, type, seen)
|
|
95
|
+
}
|
|
96
|
+
if ((type.flags & ts.TypeFlags.Object) !== 0) {
|
|
97
|
+
return projectObject(checker, type, seen)
|
|
98
|
+
}
|
|
99
|
+
return {}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/*
|
|
103
|
+
A union projects by first stripping the `undefined`/`null`/`void` members that only encode
|
|
104
|
+
optionality (they make a property non-required, not a real branch). What survives: all-boolean →
|
|
105
|
+
`boolean`; all string-literal → `const` (one) or `enum` (many); one survivor → that member's schema;
|
|
106
|
+
otherwise `anyOf` of each. An all-nullish union (a bare `undefined`) has nothing to say → `{}`.
|
|
107
|
+
*/
|
|
108
|
+
function projectUnion(
|
|
109
|
+
checker: ts.TypeChecker,
|
|
110
|
+
type: ts.UnionType,
|
|
111
|
+
seen: Set<ts.Type>,
|
|
112
|
+
): Record<string, unknown> {
|
|
113
|
+
const members = type.types.filter((member) => (member.flags & NULLISH_FLAGS) === 0)
|
|
114
|
+
if (members.length === 0) {
|
|
115
|
+
return {}
|
|
116
|
+
}
|
|
117
|
+
if (members.every((member) => (member.flags & ts.TypeFlags.BooleanLiteral) !== 0)) {
|
|
118
|
+
return { type: 'boolean' }
|
|
119
|
+
}
|
|
120
|
+
if (members.every((member) => (member.flags & ts.TypeFlags.StringLiteral) !== 0)) {
|
|
121
|
+
const values = members.map((member) => (member as ts.StringLiteralType).value)
|
|
122
|
+
return values.length === 1 ? { type: 'string', const: values[0] } : { enum: values }
|
|
123
|
+
}
|
|
124
|
+
const [onlyMember] = members
|
|
125
|
+
if (members.length === 1 && onlyMember !== undefined) {
|
|
126
|
+
return project(checker, onlyMember, seen)
|
|
127
|
+
}
|
|
128
|
+
seen.add(type)
|
|
129
|
+
const anyOf = members.map((member) => project(checker, member, seen))
|
|
130
|
+
seen.delete(type)
|
|
131
|
+
return { anyOf }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* `T[]`/`readonly T[]` → `{ type: 'array', items: project(T) }`. */
|
|
135
|
+
function projectArray(
|
|
136
|
+
checker: ts.TypeChecker,
|
|
137
|
+
type: ts.Type,
|
|
138
|
+
seen: Set<ts.Type>,
|
|
139
|
+
): Record<string, unknown> {
|
|
140
|
+
const element = checker.getTypeArguments(type as ts.TypeReference)[0]
|
|
141
|
+
seen.add(type)
|
|
142
|
+
const items = element === undefined ? {} : project(checker, element, seen)
|
|
143
|
+
seen.delete(type)
|
|
144
|
+
return { type: 'array', items }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* `Set<T>`/`readonly Set<T>` → `{ type: 'array', items: project(T) }` — congruent with the
|
|
148
|
+
`wireJsonReplacer` encode (`[...set]`). */
|
|
149
|
+
function projectSet(
|
|
150
|
+
checker: ts.TypeChecker,
|
|
151
|
+
type: ts.Type,
|
|
152
|
+
seen: Set<ts.Type>,
|
|
153
|
+
): Record<string, unknown> {
|
|
154
|
+
const element = checker.getTypeArguments(type as ts.TypeReference)[0]
|
|
155
|
+
seen.add(type)
|
|
156
|
+
const items = element === undefined ? {} : project(checker, element, seen)
|
|
157
|
+
seen.delete(type)
|
|
158
|
+
return { type: 'array', items }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* `Map<K,V>`/`readonly Map<K,V>` → an array of `[K,V]` entry tuples — congruent with the
|
|
162
|
+
`wireJsonReplacer` encode (`[...map]`). Each entry is a fixed 2-tuple, so the item schema is
|
|
163
|
+
positional `prefixItems` with `items: false` to forbid extras (matching projectTuple). */
|
|
164
|
+
function projectMap(
|
|
165
|
+
checker: ts.TypeChecker,
|
|
166
|
+
type: ts.Type,
|
|
167
|
+
seen: Set<ts.Type>,
|
|
168
|
+
): Record<string, unknown> {
|
|
169
|
+
const args = checker.getTypeArguments(type as ts.TypeReference)
|
|
170
|
+
seen.add(type)
|
|
171
|
+
const keySchema = args[0] === undefined ? {} : project(checker, args[0], seen)
|
|
172
|
+
const valueSchema = args[1] === undefined ? {} : project(checker, args[1], seen)
|
|
173
|
+
seen.delete(type)
|
|
174
|
+
return {
|
|
175
|
+
type: 'array',
|
|
176
|
+
items: { type: 'array', prefixItems: [keySchema, valueSchema], items: false },
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/* A fixed-length tuple → positional `prefixItems`, `items: false` to forbid extras (JSON Schema
|
|
181
|
+
2020-12 / OpenAPI 3.1 tuple encoding). */
|
|
182
|
+
function projectTuple(
|
|
183
|
+
checker: ts.TypeChecker,
|
|
184
|
+
type: ts.Type,
|
|
185
|
+
seen: Set<ts.Type>,
|
|
186
|
+
): Record<string, unknown> {
|
|
187
|
+
seen.add(type)
|
|
188
|
+
const prefixItems = checker
|
|
189
|
+
.getTypeArguments(type as ts.TypeReference)
|
|
190
|
+
.map((element) => project(checker, element, seen))
|
|
191
|
+
seen.delete(type)
|
|
192
|
+
return { type: 'array', prefixItems, items: false }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/*
|
|
196
|
+
An object type → `{ type: 'object', properties, required }`. A property is required unless it is `?`
|
|
197
|
+
optional or its type bears `undefined`. A string index signature (`Record<string, V>` / `{ [k]: V }`)
|
|
198
|
+
becomes `additionalProperties: project(V)`. The `seen` guard short-circuits a self-referential type
|
|
199
|
+
(a recursive tree node) to `{}` so it can't infinite-loop.
|
|
200
|
+
*/
|
|
201
|
+
function projectObject(
|
|
202
|
+
checker: ts.TypeChecker,
|
|
203
|
+
type: ts.Type,
|
|
204
|
+
seen: Set<ts.Type>,
|
|
205
|
+
): Record<string, unknown> {
|
|
206
|
+
if (seen.has(type)) {
|
|
207
|
+
return {}
|
|
208
|
+
}
|
|
209
|
+
seen.add(type)
|
|
210
|
+
const properties: Record<string, unknown> = {}
|
|
211
|
+
const required: string[] = []
|
|
212
|
+
for (const symbol of checker.getPropertiesOfType(type)) {
|
|
213
|
+
const propertyType = typeOfProperty(checker, symbol)
|
|
214
|
+
const name = symbol.getName()
|
|
215
|
+
properties[name] = project(checker, propertyType, seen)
|
|
216
|
+
const optional =
|
|
217
|
+
(symbol.flags & ts.SymbolFlags.Optional) !== 0 || bearsUndefined(propertyType)
|
|
218
|
+
if (!optional) {
|
|
219
|
+
required.push(name)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const indexType = checker.getIndexTypeOfType(type, ts.IndexKind.String)
|
|
223
|
+
seen.delete(type)
|
|
224
|
+
const schema: Record<string, unknown> = { type: 'object' }
|
|
225
|
+
if (Object.keys(properties).length > 0) {
|
|
226
|
+
schema.properties = properties
|
|
227
|
+
}
|
|
228
|
+
if (required.length > 0) {
|
|
229
|
+
schema.required = required
|
|
230
|
+
}
|
|
231
|
+
if (indexType !== undefined) {
|
|
232
|
+
schema.additionalProperties = project(checker, indexType, seen)
|
|
233
|
+
}
|
|
234
|
+
return schema
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/* A property symbol's type — from its declaration when it has one (the common case), else the
|
|
238
|
+
checker's symbol-level type (a synthesized/index property). */
|
|
239
|
+
function typeOfProperty(checker: ts.TypeChecker, symbol: ts.Symbol): ts.Type {
|
|
240
|
+
const declaration = symbol.valueDeclaration ?? symbol.declarations?.[0]
|
|
241
|
+
return declaration === undefined
|
|
242
|
+
? checker.getTypeOfSymbol(symbol)
|
|
243
|
+
: checker.getTypeOfSymbolAtLocation(symbol, declaration)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/* True when a property's type includes `undefined`/`void` — an implicit optional even without `?`. */
|
|
247
|
+
function bearsUndefined(type: ts.Type): boolean {
|
|
248
|
+
if (type.isUnion()) {
|
|
249
|
+
return type.types.some((member) => (member.flags & OPTIONALITY_FLAGS) !== 0)
|
|
250
|
+
}
|
|
251
|
+
return (type.flags & OPTIONALITY_FLAGS) !== 0
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/* A tuple is a TypeReference whose target (an ObjectType) carries the Tuple object flag. */
|
|
255
|
+
function isTupleType(type: ts.Type): boolean {
|
|
256
|
+
const target = (type as ts.TypeReference).target as ts.ObjectType | undefined
|
|
257
|
+
return target !== undefined && (target.objectFlags & ts.ObjectFlags.Tuple) !== 0
|
|
258
|
+
}
|