Implement independent presentation readiness boundaries

This commit is contained in:
Timothy J. Aveni
2026-09-27 20:30:07 -07:00
parent b499298299
commit ad72185a6c
8 changed files with 140 additions and 31 deletions
+2 -2
View File
@@ -251,7 +251,7 @@ export function generateComponentClientBindings(plan: ExecutionPlan, packageRevi
.join("\n");
return (
interfaces +
'\nimport {createObjectRenderer,definePackageQuery,definePackageFragment,type ComponentType,type ComponentProps,type PreparedPresentation} from "@quixos/camino-react";\n' +
'\nimport {createObjectRenderer,definePackageQuery,definePackageFragment,type ComponentType,type ComponentProps,type PresentationHandle} from "@quixos/camino-react";\n' +
subjectBindings +
"\n" +
queries +
@@ -288,7 +288,7 @@ export function generateComponentClientBindings(plan: ExecutionPlan, packageRevi
)!;
extra = props(iface.revisionId, member.id);
}
const handle = `PreparedPresentation<${JSON.stringify(child.subject)}, ${extra}>`;
const handle = `PresentationHandle<${JSON.stringify(child.subject)}, ${extra}>`;
return `readonly ${JSON.stringify(child.name)}: ${child.each ? `readonly ${handle}[]` : child.with || child.when ? `${handle} | null` : handle}`;
});
const input = `${tsType(composition.output)} & {readonly children: {${children.join(";")}}}`;
+28 -3
View File
@@ -4,6 +4,8 @@ import {
validate,
specifiedRules,
PossibleFragmentSpreadsRule,
NoUnusedVariablesRule,
GraphQLError,
print,
printSchema,
visit,
@@ -201,10 +203,29 @@ export async function compileQuery(
},
}),
);
const bindingVariables = new Set(
[...fragmentContracts.values()]
.filter((entry) => entry.mode === "reference")
.flatMap((entry) => Object.values(entry.variables)),
);
const errors = validate(
generated.schema,
document,
specifiedRules.filter((rule) => rule !== PossibleFragmentSpreadsRule),
[
...specifiedRules.filter((rule) => rule !== PossibleFragmentSpreadsRule && rule !== NoUnusedVariablesRule),
(context) => ({
OperationDefinition: {
leave(operation) {
const used = new Set(context.getRecursiveVariableUsages(operation).map(({ node }) => node.name.value));
for (const variable of operation.variableDefinitions ?? []) {
const name = variable.variable.name.value;
if (!used.has(name) && !bindingVariables.has(name))
context.reportError(new GraphQLError(`Variable "$${name}" is never used.`, { nodes: variable }));
}
},
},
}),
],
{ maxErrors: 20 },
);
if (errors.length) {
@@ -399,6 +420,7 @@ export async function compileQuery(
for (const selection of set.selections) {
if (++expandedFields > 10000) fail("QUERY_WORK_LIMIT", "Expanded query exceeds 10000 fields", selection);
if (selection.kind === "FragmentSpread") {
if (fragmentContracts.get(selection.name.value)?.mode === "reference") continue;
add(
fragments.get(selection.name.value)!.selectionSet,
conditional || !!selection.directives?.length,
@@ -530,7 +552,9 @@ export async function compileQuery(
},
]
: []),
...selection(fragments.get(node.name.value)!.selectionSet, parent, inheritedConditions),
...(contract?.mode === "reference"
? []
: selection(fragments.get(node.name.value)!.selectionSet, parent, inheritedConditions)),
];
}
if (node.kind !== "Field") return [];
@@ -610,9 +634,10 @@ export async function compileQuery(
const definitionDigest = createHash("sha256")
.update(
canonicalJson({
semantics: 4,
semantics: 5,
declaration,
normalized,
fragmentContracts: Object.fromEntries(fragmentContracts),
// Source provenance belongs to the checked installation. Identical public
// contracts keep their query identity when retained in another repository.
// Closed generic identity remains explicit in revisionId/application.
+16 -2
View File
@@ -11,6 +11,8 @@ export async function fragmentSources(
) {
const markers = new Map<string, NonNullable<QuerySelection["fragmentSpread"]>>();
for (const imported of declaration.importedFragments ?? []) {
if (imported.mode !== undefined && imported.mode !== "reference")
throw new QueryCompileError("FRAGMENT_IMPORT_MODE", "Unknown fragment import mode");
if (!resolve)
throw new QueryCompileError(
"FRAGMENT_RESOLVER_REQUIRED",
@@ -57,7 +59,16 @@ export async function fragmentSources(
node.name.value === exported.name ? exported.name : `${exported.name}__${node.name.value}`,
]),
);
for (const fragment of fragments) {
// A reference import binds the checked export, rather than spreading its
// fields into this operation. Keep a nominal stub for GraphQL root checking;
// the independently checked export supplies the actual projection contract.
const referenced = parse("fragment Binding on Placeholder { _camino { ref } }")
.definitions[0] as FragmentDefinitionNode;
for (const sourceFragment of imported.mode === "reference"
? fragments.filter((entry) => entry.name.value === exported.name)
: fragments) {
const fragment =
imported.mode === "reference" ? { ...sourceFragment, selectionSet: referenced.selectionSet } : sourceFragment;
definitions.push(
visit(fragment, {
FragmentDefinition(node) {
@@ -73,13 +84,16 @@ export async function fragmentSources(
);
}
markers.set(exported.name, {
...(imported.mode ? { mode: imported.mode } : {}),
package: imported.packageRevisionId,
fragment: imported.fragmentId,
digest: checked.definitionDigest,
root: checked.declaration.root.interface,
variables: imported.variables,
});
for (const [name, contract] of Object.entries(checked.fragmentContracts ?? {})) {
for (const [name, contract] of Object.entries(
imported.mode === "reference" ? {} : (checked.fragmentContracts ?? {}),
)) {
markers.set(privateNames.get(name) ?? name, {
...contract,
variables: Object.fromEntries(
+4
View File
@@ -38,6 +38,9 @@ export interface QueryDeclaration {
/** Exported local projection; compilation also creates its direct admission. */
fragment?: { name: string; operation?: string; admission?: boolean };
importedFragments?: {
/** Bind an independently observed fragment without including its fields in
* this projection. Its acquisition is still an explicit replica source. */
mode?: "reference";
packageRevisionId: string;
packageAlias?: string;
fragmentId: string;
@@ -113,6 +116,7 @@ export type QueryArgument =
export interface QuerySelection {
/** A checked occurrence marker, not an output field or storage capability. */
fragmentSpread?: {
mode?: "reference";
package: string;
fragment: string;
digest: string;
-1
View File
@@ -17,7 +17,6 @@ export interface ReactChildDeclaration {
export interface ReactModulePreparation {
module: string;
composition: string;
discoveryComposition?: string;
children: ReactChildDeclaration[];
}
export interface ReactPreparationArtifact {
+20 -11
View File
@@ -147,9 +147,6 @@ export async function expandPresentationQueries(workspace: WorkspaceRevision): P
for (const child of children)
for (const branch of child.branches) await expandModule(identity(branch.packageRevision, branch.id));
const composition = pkg.queryCompositions!.find((entry) => entry.id === preparation.composition)!;
const discovery = { ...composition, id: composition.id + ":discovery" };
preparation.discoveryComposition = discovery.id;
pkg.queryCompositions!.push(discovery);
const steps = structuredClone(composition.steps);
const exported: { step: string; query: CheckedQuery }[] = [];
for (const step of steps) {
@@ -187,7 +184,12 @@ export async function expandPresentationQueries(workspace: WorkspaceRevision): P
(entry) => entry.packageRevisionId === branch.packageRevision && entry.fragmentId === declaration.id,
)
) {
imports.push({ packageRevisionId: branch.packageRevision, fragmentId: declaration.id, variables: {} });
imports.push({
packageRevisionId: branch.packageRevision,
fragmentId: declaration.id,
variables: {},
mode: "reference",
});
}
selection = insert(selection, path, declaration.fragment!.name);
const admission = admissionFragments.get(identity(branch.packageRevision, declaration.id))!;
@@ -203,6 +205,18 @@ export async function expandPresentationQueries(workspace: WorkspaceRevision): P
variables: {},
});
admissionSelection = insert(admissionSelection, path, admission.declaration.fragment!.name);
if (
!admissionImports.some(
(entry) => entry.packageRevisionId === branch.packageRevision && entry.fragmentId === declaration.id,
)
)
admissionImports.push({
packageRevisionId: branch.packageRevision,
fragmentId: declaration.id,
variables: {},
mode: "reference",
});
admissionSelection = insert(admissionSelection, path, declaration.fragment!.name);
}
}
}
@@ -337,13 +351,7 @@ export async function expandPresentationQueries(workspace: WorkspaceRevision): P
{ id: composition.id, input: composition.input, steps, maxQueries: composition.maxQueries },
available,
);
const checkedDiscovery = compileQueryComposition(
{ id: discovery.id, input: discovery.input, steps: discovery.steps, maxQueries: discovery.maxQueries },
available,
);
pkg.queryCompositions = pkg.queryCompositions!.map((entry) =>
entry.id === composition.id ? checked : entry.id === discovery.id ? checkedDiscovery : entry,
);
pkg.queryCompositions = pkg.queryCompositions!.map((entry) => (entry.id === composition.id ? checked : entry));
eager.set(key, exported);
done.add(key);
}
@@ -411,6 +419,7 @@ export async function expandPresentationQueries(workspace: WorkspaceRevision): P
packageRevisionId: input.pkg.revisionId,
fragmentId: input.query.declaration.id,
variables: {},
...(original.declaration.placement?.kind === "local" ? { mode: "reference" as const } : {}),
})),
},
print({ ...document, definitions }),