Implement progressive query admission, session boundaries and event-driven Studio observations

This commit is contained in:
Timothy J. Aveni
2026-10-10 03:49:05 -07:00
parent 2ae7f00acb
commit c2c01a9878
17 changed files with 775 additions and 23 deletions
+27 -2
View File
@@ -20,6 +20,9 @@ export interface ReactModulePreparation {
/** Public input type before workspace query lowering adds private projections. */
input?: ValueType;
children: ReactChildDeclaration[];
/** Binds roots/arguments only. The ordinary checked Query Tree owns every
* acquisition path; the React declaration does not repeat its descendants. */
trees?: Pick<QueryCompositionStep, "name" | "query" | "root" | "arguments">[];
}
export interface ReactPreparationArtifact {
/** Frontend queries belong to installed preparation, not the authored client API. */
@@ -51,7 +54,7 @@ export function presentationDeclarations(
};
for (const [name, value] of Object.entries(modules)) {
const declaration = object(value, `module ${name}`);
if (Object.keys(declaration).some((key) => !["inputs", "children"].includes(key)))
if (Object.keys(declaration).some((key) => !["inputs", "children", "trees"].includes(key)))
throw Error(`${name}: unknown preparation field`);
const module = pkg.modules?.find((entry) => entry.displayName === name);
if (!module) throw Error(`React declaration: unknown module ${name}`);
@@ -129,7 +132,29 @@ export function presentationDeclarations(
...(role ? { role } : {}),
};
});
artifact.modules.push({ module: module.id, composition, children: parsed });
const trees = declaration.trees ?? [];
if (!Array.isArray(trees) || trees.length > 16) throw Error(`${name}: trees must be a bounded array`);
const treeNames = new Set<string>();
const preparedTrees = trees.map((value) => {
const tree = object(value, `${name} tree`) as unknown as NonNullable<ReactModulePreparation["trees"]>[number];
if (
Object.keys(tree).some((key) => !["name", "query", "root", "arguments"].includes(key)) ||
!/^[A-Za-z_][A-Za-z0-9_]*$/.test(tree.name) ||
treeNames.has(tree.name)
)
throw Error(`${name}: tree requires a unique name and checked root/arguments`);
treeNames.add(tree.name);
const query = pkg.queries?.find((entry) => entry.displayName === tree.query);
if (!query || query.placement?.kind !== "replica")
throw Error(`${name}.${tree.name}: requires a replica Query Tree`);
return { ...tree, query: query.id };
});
artifact.modules.push({
module: module.id,
composition,
children: parsed,
...(preparedTrees.length ? { trees: preparedTrees } : {}),
});
}
for (const module of pkg.modules ?? []) {
const contract = interfaces.find((entry) => entry.revisionId === module.interfaceRevisionId);
+12 -1
View File
@@ -1,7 +1,7 @@
import type { WorkspaceRevision, ValueType } from "../capability-model/types.js";
import { moduleContract } from "../capability-model/modules.js";
import { canonicalJson } from "../capability-model/evolution.js";
import { queryBindingType } from "../query/composition.js";
import { queryBindingType, compileQueryComposition } from "../query/composition.js";
import { moduleTypeContext } from "../execution-world/module-contracts.js";
import { preparationArtifact, type ReactPreparationArtifact } from "./declarations.js";
import { expandPresentationQueries } from "./expansion.js";
@@ -25,6 +25,17 @@ export async function completeWorkspace(workspace: WorkspaceRevision): Promise<v
if (!composition || composition.output.kind !== "record")
throw Error(`Missing checked inputs for ${parent.module.displayName}`);
parent.preparation.input = structuredClone(composition.output);
const treeQueries = new Map((parent.pkg.checkedQueries ?? []).map((query) => [query.declaration.id, query]));
for (const tree of parent.preparation.trees ?? []) {
if (!treeQueries.get(tree.query)?.tree)
throw Error(`${parent.module.displayName}.${tree.name}: expected a checked Query Tree`);
// Reuse Camino's ordinary nominal/argument binder. Only input parameters
// are available; a tree cannot wait on a whole presentation result.
compileQueryComposition(
{ id: `react:tree:${parent.module.id}:${tree.name}`, input: composition.input, steps: [tree], maxQueries: 1 },
treeQueries,
);
}
const outputs = new Map(Object.entries(composition.output.fields));
const dependencies: string[] = [];
const children = parent.preparation.children.map((child) => {