Keep checked execution groups and public catalog preparation cooperative
This commit is contained in:
@@ -6,7 +6,7 @@ import type {
|
|||||||
PersistentAttachment,
|
PersistentAttachment,
|
||||||
} from "../capability-model/types.js";
|
} from "../capability-model/types.js";
|
||||||
import { type ExecutionInstallation, type ExecutionGroupIdentity } from "./installation.js";
|
import { type ExecutionInstallation, type ExecutionGroupIdentity } from "./installation.js";
|
||||||
import { type ExecutionPlan, hexDigest, ExecutionPlanError } from "./plan.js";
|
import { type ExecutionPlan, hexDigest, hexDigestAsync, ExecutionPlanError } from "./plan.js";
|
||||||
|
|
||||||
export interface ExecutionEnvironment {
|
export interface ExecutionEnvironment {
|
||||||
/** Explicit host placement. Merely including an optional module does not select it. */
|
/** Explicit host placement. Merely including an optional module does not select it. */
|
||||||
@@ -23,12 +23,12 @@ export interface InstalledExecutionGroup extends ExecutionGroupIdentity {
|
|||||||
}
|
}
|
||||||
/** Conservative outbound closure. Runtime graph values, reverse callers, source
|
/** Conservative outbound closure. Runtime graph values, reverse callers, source
|
||||||
* provenance and the workspace epoch are deliberately absent from this identity. */
|
* provenance and the workspace epoch are deliberately absent from this identity. */
|
||||||
export function compileExecutionGroups(
|
export async function compileExecutionGroups(
|
||||||
plan: ExecutionPlan,
|
plan: ExecutionPlan,
|
||||||
installation: ExecutionInstallation,
|
installation: ExecutionInstallation,
|
||||||
environments: Readonly<Record<string, ExecutionEnvironment>>,
|
environments: Readonly<Record<string, ExecutionEnvironment>>,
|
||||||
): InstalledExecutionGroup[] {
|
): Promise<InstalledExecutionGroup[]> {
|
||||||
if (installation.plan !== plan.digest || hexDigest(plan.definition) !== plan.digest)
|
if (installation.plan !== plan.digest || (await hexDigestAsync(plan.definition)) !== plan.digest)
|
||||||
throw new ExecutionPlanError("GROUP_PLAN_MISMATCH", plan.digest, "Expected one checked installation");
|
throw new ExecutionPlanError("GROUP_PLAN_MISMATCH", plan.digest, "Expected one checked installation");
|
||||||
const workspace = plan.definition.workspace;
|
const workspace = plan.definition.workspace;
|
||||||
const packages = new Map(workspace.packageImports.map((p) => [p.revisionId as string, p]));
|
const packages = new Map(workspace.packageImports.map((p) => [p.revisionId as string, p]));
|
||||||
@@ -82,178 +82,175 @@ export function compileExecutionGroups(
|
|||||||
};
|
};
|
||||||
const bindingContract = (value: DependencyBinding) =>
|
const bindingContract = (value: DependencyBinding) =>
|
||||||
value.kind === "query" ? { ...value, packageRevisionId: logical(value.packageRevisionId) } : value;
|
value.kind === "query" ? { ...value, packageRevisionId: logical(value.packageRevisionId) } : value;
|
||||||
return installation.packages
|
const groups: InstalledExecutionGroup[] = [];
|
||||||
.filter((p) => !!implementations.get(p.revision))
|
for (const root of installation.packages.filter((p) => !!implementations.get(p.revision))) {
|
||||||
.map((root) => {
|
const consumed = new Map<string, unknown>(),
|
||||||
const consumed = new Map<string, unknown>(),
|
visited = new Set<string>(),
|
||||||
visited = new Set<string>(),
|
visitedQueries = new Set<string>(),
|
||||||
visitedQueries = new Set<string>(),
|
consumedPackages = new Set<string>();
|
||||||
consumedPackages = new Set<string>();
|
const remember = (key: string, value: unknown) => consumed.set(key, value);
|
||||||
const remember = (key: string, value: unknown) => consumed.set(key, value);
|
const storage = (id: string) => {
|
||||||
const storage = (id: string) => {
|
const value = attachments.get(id);
|
||||||
const value = attachments.get(id);
|
if (!value) throw new ExecutionPlanError("GROUP_STORAGE_MISSING", id, "Uninstalled consumed storage");
|
||||||
if (!value) throw new ExecutionPlanError("GROUP_STORAGE_MISSING", id, "Uninstalled consumed storage");
|
remember(`storage:${id}`, attachmentContract(value));
|
||||||
remember(`storage:${id}`, attachmentContract(value));
|
};
|
||||||
};
|
// Deletion may visit related objects and their owned state. Conservatively
|
||||||
// Deletion may visit related objects and their owned state. Conservatively
|
// include the connected schema component, including possible future edges.
|
||||||
// include the connected schema component, including possible future edges.
|
const lifecycle = (atom: string) => {
|
||||||
const lifecycle = (atom: string) => {
|
const visited = new Set<string>(),
|
||||||
const visited = new Set<string>(),
|
pending = [atom];
|
||||||
pending = [atom];
|
const atoms = (constraint: Extract<PersistentAttachment, { kind: "edge" }>["endpoints"][number]["constraint"]) =>
|
||||||
const atoms = (
|
constraint.kind === "atom"
|
||||||
constraint: Extract<PersistentAttachment, { kind: "edge" }>["endpoints"][number]["constraint"],
|
? [constraint.atomId as string]
|
||||||
) =>
|
|
||||||
constraint.kind === "atom"
|
|
||||||
? [constraint.atomId as string]
|
|
||||||
: workspace.conformances
|
|
||||||
.filter((c) => c.interfaceRevisionId === constraint.interfaceRevisionId)
|
|
||||||
.map((c) => c.atomId as string);
|
|
||||||
while (pending.length) {
|
|
||||||
const current = pending.pop()!;
|
|
||||||
if (visited.has(current)) continue;
|
|
||||||
visited.add(current);
|
|
||||||
for (const owner of workspace.conformances.filter((c) => c.atomId === current))
|
|
||||||
for (const attachment of owner.privateAttachments) storage(attachment.id);
|
|
||||||
for (const attachment of attachments.values())
|
|
||||||
if (attachment.kind === "edge" && attachment.endpoints.some((e) => atoms(e.constraint).includes(current))) {
|
|
||||||
storage(attachment.id);
|
|
||||||
pending.push(...attachment.endpoints.flatMap((e) => atoms(e.constraint)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const targets = (binding: DependencyBinding, atom: string): string[] => {
|
|
||||||
if (binding.kind === "interface" && binding.receiver === "input")
|
|
||||||
return workspace.conformances
|
|
||||||
.filter((c) => c.interfaceRevisionId === binding.interfaceRevisionId)
|
|
||||||
.map((c) => c.atomId);
|
|
||||||
if (!("via" in binding) || !binding.via) return [atom];
|
|
||||||
storage(binding.via.edgeTypeId);
|
|
||||||
const edge = attachments.get(binding.via.edgeTypeId);
|
|
||||||
if (!edge || edge.kind !== "edge")
|
|
||||||
throw new ExecutionPlanError("GROUP_EDGE_MISSING", atom, "Missing traversal edge");
|
|
||||||
const endpoint = edge.endpoints.find((e) => e.projectionId !== binding.via!.projectionId)!;
|
|
||||||
const constraint = endpoint.constraint;
|
|
||||||
return constraint.kind === "atom"
|
|
||||||
? [constraint.atomId]
|
|
||||||
: workspace.conformances
|
: workspace.conformances
|
||||||
.filter((c) => c.interfaceRevisionId === constraint.interfaceRevisionId)
|
.filter((c) => c.interfaceRevisionId === constraint.interfaceRevisionId)
|
||||||
.map((c) => c.atomId);
|
.map((c) => c.atomId as string);
|
||||||
};
|
while (pending.length) {
|
||||||
const query = (revision: string, id: string) => {
|
const current = pending.pop()!;
|
||||||
const key = JSON.stringify([revision, id]);
|
if (visited.has(current)) continue;
|
||||||
if (visitedQueries.has(key)) return;
|
visited.add(current);
|
||||||
visitedQueries.add(key);
|
for (const owner of workspace.conformances.filter((c) => c.atomId === current))
|
||||||
const value = plan.definition.queries.find((q) => q.id === key);
|
for (const attachment of owner.privateAttachments) storage(attachment.id);
|
||||||
if (!value) throw new ExecutionPlanError("GROUP_QUERY_MISSING", key, "Missing consumed query");
|
for (const attachment of attachments.values())
|
||||||
remember(`query:${JSON.stringify([logical(revision), id])}`, value.query.checked.definitionDigest);
|
if (attachment.kind === "edge" && attachment.endpoints.some((e) => atoms(e.constraint).includes(current))) {
|
||||||
for (const operation of value.operations) visit(operation);
|
storage(attachment.id);
|
||||||
};
|
pending.push(...attachment.endpoints.flatMap((e) => atoms(e.constraint)));
|
||||||
const dependencies = (entry: PackageExport, bindings: readonly BoundDependency[], atom: string) => {
|
}
|
||||||
for (const bound of bindings) {
|
}
|
||||||
const target = bound.binding,
|
};
|
||||||
port = entry.dependencyPorts.find((p) => p.id === bound.portId)!;
|
const targets = (binding: DependencyBinding, atom: string): string[] => {
|
||||||
if ("via" in target && target.via) storage(target.via.edgeTypeId);
|
if (binding.kind === "interface" && binding.receiver === "input")
|
||||||
if (target.kind === "reflection") {
|
return workspace.conformances
|
||||||
for (const selector of target.selectors)
|
.filter((c) => c.interfaceRevisionId === binding.interfaceRevisionId)
|
||||||
remember("reflection:" + selector, portableReflectionValue(workspace, selector));
|
.map((c) => c.atomId);
|
||||||
} else if (target.kind === "state") storage(target.slotId);
|
if (!("via" in binding) || !binding.via) return [atom];
|
||||||
else if (target.kind === "edge") storage(target.edgeTypeId);
|
storage(binding.via.edgeTypeId);
|
||||||
else if (target.kind === "query") query(target.packageRevisionId, target.queryId);
|
const edge = attachments.get(binding.via.edgeTypeId);
|
||||||
else if (target.kind === "interface" && port.requirement.kind === "interface") {
|
if (!edge || edge.kind !== "edge")
|
||||||
const iface = workspace.interfaceImports.find((i) => i.revisionId === target.interfaceRevisionId)!;
|
throw new ExecutionPlanError("GROUP_EDGE_MISSING", atom, "Missing traversal edge");
|
||||||
for (const targetAtom of targets(target, atom))
|
const endpoint = edge.endpoints.find((e) => e.projectionId !== binding.via!.projectionId)!;
|
||||||
for (const op of port.requirement.operations ??
|
const constraint = endpoint.constraint;
|
||||||
iface.members.flatMap((m) => m.operations.map((o) => o.id)))
|
return constraint.kind === "atom"
|
||||||
visit(JSON.stringify([targetAtom, target.interfaceRevisionId, op]));
|
? [constraint.atomId]
|
||||||
} else if (target.kind === "creation") {
|
: workspace.conformances
|
||||||
for (const seed of target.owners) {
|
.filter((c) => c.interfaceRevisionId === constraint.interfaceRevisionId)
|
||||||
const owner = workspace.conformances.find(
|
.map((c) => c.atomId);
|
||||||
(c) => c.atomId === target.atomId && c.interfaceRevisionId === seed.interfaceRevisionId,
|
};
|
||||||
)!;
|
const query = (revision: string, id: string) => {
|
||||||
remember(`birth:${owner.id}`, {
|
const key = JSON.stringify([revision, id]);
|
||||||
id: owner.id,
|
if (visitedQueries.has(key)) return;
|
||||||
atom: owner.atomId,
|
visitedQueries.add(key);
|
||||||
interface: owner.interfaceRevisionId,
|
const value = plan.definition.queries.find((q) => q.id === key);
|
||||||
initialization: owner.initialization ?? [],
|
if (!value) throw new ExecutionPlanError("GROUP_QUERY_MISSING", key, "Missing consumed query");
|
||||||
});
|
remember(`query:${JSON.stringify([logical(revision), id])}`, value.query.checked.definitionDigest);
|
||||||
for (const attachment of owner.privateAttachments) storage(attachment.id);
|
for (const operation of value.operations) visit(operation);
|
||||||
}
|
};
|
||||||
} else
|
const dependencies = (entry: PackageExport, bindings: readonly BoundDependency[], atom: string) => {
|
||||||
throw new ExecutionPlanError(
|
for (const bound of bindings) {
|
||||||
"GROUP_DEPENDENCY_UNSUPPORTED",
|
const target = bound.binding,
|
||||||
bound.portId,
|
port = entry.dependencyPorts.find((p) => p.id === bound.portId)!;
|
||||||
"Cannot establish execution reuse for this dependency",
|
if ("via" in target && target.via) storage(target.via.edgeTypeId);
|
||||||
);
|
if (target.kind === "reflection") {
|
||||||
}
|
for (const selector of target.selectors)
|
||||||
};
|
remember("reflection:" + selector, portableReflectionValue(workspace, selector));
|
||||||
const visit = (key: string) => {
|
} else if (target.kind === "state") storage(target.slotId);
|
||||||
if (visited.has(key)) return;
|
else if (target.kind === "edge") storage(target.edgeTypeId);
|
||||||
visited.add(key);
|
else if (target.kind === "query") query(target.packageRevisionId, target.queryId);
|
||||||
const operation = operations.get(key);
|
else if (target.kind === "interface" && port.requirement.kind === "interface") {
|
||||||
if (!operation) throw new ExecutionPlanError("GROUP_OPERATION_MISSING", key, "Missing consumed operation");
|
const iface = workspace.interfaceImports.find((i) => i.revisionId === target.interfaceRevisionId)!;
|
||||||
const binding = operation.binding;
|
for (const targetAtom of targets(target, atom))
|
||||||
if (binding.kind === "package") {
|
for (const op of port.requirement.operations ?? iface.members.flatMap((m) => m.operations.map((o) => o.id)))
|
||||||
const pkg = packages.get(binding.packageRevisionId)!;
|
visit(JSON.stringify([targetAtom, target.interfaceRevisionId, op]));
|
||||||
const implementation = implementations.get(pkg.revisionId);
|
} else if (target.kind === "creation") {
|
||||||
if (!implementation)
|
for (const seed of target.owners) {
|
||||||
throw new ExecutionPlanError(
|
const owner = workspace.conformances.find(
|
||||||
"GROUP_IMPLEMENTATION_MISSING",
|
(c) => c.atomId === target.atomId && c.interfaceRevisionId === seed.interfaceRevisionId,
|
||||||
pkg.revisionId,
|
)!;
|
||||||
"Consumed package has no selected server implementation",
|
remember(`birth:${owner.id}`, {
|
||||||
);
|
id: owner.id,
|
||||||
consumedPackages.add(pkg.packageId);
|
atom: owner.atomId,
|
||||||
remember(`implementation:${pkg.packageId}`, {
|
interface: owner.interfaceRevisionId,
|
||||||
digest: implementation.contentDigest,
|
initialization: owner.initialization ?? [],
|
||||||
environment: environment(pkg.revisionId),
|
});
|
||||||
});
|
for (const attachment of owner.privateAttachments) storage(attachment.id);
|
||||||
const entry = pkg.exports.find((e) => e.id === binding.exportId)!;
|
}
|
||||||
const bindings = binding.dependencies.map((d) => ({ portId: d.portId, binding: bindingContract(d.binding) }));
|
} else
|
||||||
const acceptance = (binding.acceptanceDependencies ?? []).map((d) => ({
|
throw new ExecutionPlanError(
|
||||||
portId: d.portId,
|
"GROUP_DEPENDENCY_UNSUPPORTED",
|
||||||
binding: bindingContract(d.binding),
|
bound.portId,
|
||||||
}));
|
"Cannot establish execution reuse for this dependency",
|
||||||
const contract = {
|
);
|
||||||
input: operation.input,
|
}
|
||||||
output: operation.output,
|
};
|
||||||
execution: operation.execution,
|
const visit = (key: string) => {
|
||||||
scope: operation.scope,
|
if (visited.has(key)) return;
|
||||||
binding: {
|
visited.add(key);
|
||||||
package: pkg.packageId,
|
const operation = operations.get(key);
|
||||||
export: binding.exportId,
|
if (!operation) throw new ExecutionPlanError("GROUP_OPERATION_MISSING", key, "Missing consumed operation");
|
||||||
dependencies: bindings,
|
const binding = operation.binding;
|
||||||
acceptanceDependencies: acceptance,
|
if (binding.kind === "package") {
|
||||||
},
|
const pkg = packages.get(binding.packageRevisionId)!;
|
||||||
};
|
const implementation = implementations.get(pkg.revisionId);
|
||||||
// Deduplicate aliases of the same bound export, rather than keying its owner by reverse callers.
|
if (!implementation)
|
||||||
remember(`binding:${hexDigest(contract)}`, contract);
|
throw new ExecutionPlanError(
|
||||||
dependencies(entry, binding.dependencies, operation.atomId);
|
"GROUP_IMPLEMENTATION_MISSING",
|
||||||
if (entry.acceptanceExportId)
|
pkg.revisionId,
|
||||||
dependencies(
|
"Consumed package has no selected server implementation",
|
||||||
pkg.exports.find((e) => e.id === entry.acceptanceExportId)!,
|
);
|
||||||
binding.acceptanceDependencies ?? [],
|
consumedPackages.add(pkg.packageId);
|
||||||
operation.atomId,
|
remember(`implementation:${pkg.packageId}`, {
|
||||||
);
|
digest: implementation.contentDigest,
|
||||||
} else {
|
environment: environment(pkg.revisionId),
|
||||||
remember(`operation:${key}`, operation);
|
});
|
||||||
if (binding.kind === "state") storage(binding.slotId);
|
const entry = pkg.exports.find((e) => e.id === binding.exportId)!;
|
||||||
if (binding.kind === "edge") storage(binding.edgeTypeId);
|
const bindings = binding.dependencies.map((d) => ({ portId: d.portId, binding: bindingContract(d.binding) }));
|
||||||
if (binding.kind === "lifecycle") lifecycle(operation.atomId);
|
const acceptance = (binding.acceptanceDependencies ?? []).map((d) => ({
|
||||||
}
|
portId: d.portId,
|
||||||
};
|
binding: bindingContract(d.binding),
|
||||||
for (const operation of plan.definition.operations)
|
}));
|
||||||
if (operation.binding.kind === "package" && operation.binding.packageRevisionId === root.revision)
|
const contract = {
|
||||||
visit(operation.key);
|
input: operation.input,
|
||||||
const own = implementations.get(root.revision)!;
|
output: operation.output,
|
||||||
const exports = packages.get(root.revision)!.exports.map(({ displayName: _name, dependencyPorts, ...entry }) => ({
|
execution: operation.execution,
|
||||||
...entry,
|
scope: operation.scope,
|
||||||
dependencyPorts: dependencyPorts.map(({ displayName: _name, ...port }) => port),
|
binding: {
|
||||||
}));
|
package: pkg.packageId,
|
||||||
const entries = [...consumed].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
export: binding.exportId,
|
||||||
return {
|
dependencies: bindings,
|
||||||
logicalId: logical(root.revision),
|
acceptanceDependencies: acceptance,
|
||||||
packageRevision: root.revision,
|
},
|
||||||
executionDigest: `sha256:${hexDigest({ implementation: own.contentDigest, exports, environment: environment(root.revision), versions: plan.definition.versions, consumed: entries })}`,
|
};
|
||||||
consumedPackages: [...consumedPackages].sort(),
|
// Deduplicate aliases of the same bound export, rather than keying its owner by reverse callers.
|
||||||
};
|
remember(`binding:${hexDigest(contract)}`, contract);
|
||||||
|
dependencies(entry, binding.dependencies, operation.atomId);
|
||||||
|
if (entry.acceptanceExportId)
|
||||||
|
dependencies(
|
||||||
|
pkg.exports.find((e) => e.id === entry.acceptanceExportId)!,
|
||||||
|
binding.acceptanceDependencies ?? [],
|
||||||
|
operation.atomId,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
remember(`operation:${key}`, operation);
|
||||||
|
if (binding.kind === "state") storage(binding.slotId);
|
||||||
|
if (binding.kind === "edge") storage(binding.edgeTypeId);
|
||||||
|
if (binding.kind === "lifecycle") lifecycle(operation.atomId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for (const operation of plan.definition.operations)
|
||||||
|
if (operation.binding.kind === "package" && operation.binding.packageRevisionId === root.revision)
|
||||||
|
visit(operation.key);
|
||||||
|
const own = implementations.get(root.revision)!;
|
||||||
|
const exports = packages.get(root.revision)!.exports.map(({ displayName: _name, dependencyPorts, ...entry }) => ({
|
||||||
|
...entry,
|
||||||
|
dependencyPorts: dependencyPorts.map(({ displayName: _name, ...port }) => port),
|
||||||
|
}));
|
||||||
|
const entries = [...consumed].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
||||||
|
groups.push({
|
||||||
|
logicalId: logical(root.revision),
|
||||||
|
packageRevision: root.revision,
|
||||||
|
executionDigest: `sha256:${await hexDigestAsync({ implementation: own.contentDigest, exports, environment: environment(root.revision), versions: plan.definition.versions, consumed: entries })}`,
|
||||||
|
consumedPackages: [...consumedPackages].sort(),
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
return groups;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import path from "node:path";
|
|||||||
import { loadCheckedArtifactWorld } from "./receipts.js";
|
import { loadCheckedArtifactWorld } from "./receipts.js";
|
||||||
import { contentArtifactUrl, loadArtifactWorld, type LoadedArtifactWorld } from "./files.js";
|
import { contentArtifactUrl, loadArtifactWorld, type LoadedArtifactWorld } from "./files.js";
|
||||||
import { selectArtifactImplementation, type ArtifactImplementation, type ComponentArtifact } from "./artifacts.js";
|
import { selectArtifactImplementation, type ArtifactImplementation, type ComponentArtifact } from "./artifacts.js";
|
||||||
import { ExecutionPlanError, hexDigest, type ExecutionPlan } from "./plan.js";
|
import { ExecutionPlanError, hexDigestAsync, type ExecutionPlan } from "./plan.js";
|
||||||
import {
|
import {
|
||||||
publicOperationContract,
|
publicOperationContract,
|
||||||
publicQueryContract,
|
publicQueryContract,
|
||||||
@@ -41,13 +41,13 @@ export interface ClientContractCatalog {
|
|||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
/** Explicit projection prevents accidental spreading of filesystem roots or private bindings. */
|
/** Explicit projection prevents accidental spreading of filesystem roots or private bindings. */
|
||||||
export function clientContractCatalog(
|
export async function clientContractCatalog(
|
||||||
plan: ExecutionPlan,
|
plan: ExecutionPlan,
|
||||||
installation: ExecutionInstallation,
|
installation: ExecutionInstallation,
|
||||||
requestedPackages: readonly string[],
|
requestedPackages: readonly string[],
|
||||||
checkedKernel?: InspectionKernel,
|
checkedKernel?: InspectionKernel,
|
||||||
): ClientContractCatalog {
|
): Promise<ClientContractCatalog> {
|
||||||
if (installation.plan !== plan.digest || hexDigest(plan.definition) !== plan.digest)
|
if (installation.plan !== plan.digest || (await hexDigestAsync(plan.definition)) !== plan.digest)
|
||||||
throw new ExecutionPlanError(
|
throw new ExecutionPlanError(
|
||||||
"CATALOG_PLAN_MISMATCH",
|
"CATALOG_PLAN_MISMATCH",
|
||||||
plan.digest,
|
plan.digest,
|
||||||
@@ -112,6 +112,7 @@ export function createClientContractCatalog(
|
|||||||
const revisions = plan.definition.workspace.packageImports.map((pkg) => pkg.revisionId);
|
const revisions = plan.definition.workspace.packageImports.map((pkg) => pkg.revisionId);
|
||||||
const installed = new Set<string>(revisions);
|
const installed = new Set<string>(revisions);
|
||||||
let catalog: ClientContractCatalog | undefined;
|
let catalog: ClientContractCatalog | undefined;
|
||||||
|
let preparing: Promise<void> | undefined;
|
||||||
let release: (() => void) | undefined;
|
let release: (() => void) | undefined;
|
||||||
let closed = false;
|
let closed = false;
|
||||||
const freeze = (value: unknown, seen = new WeakSet<object>()) => {
|
const freeze = (value: unknown, seen = new WeakSet<object>()) => {
|
||||||
@@ -121,7 +122,7 @@ export function createClientContractCatalog(
|
|||||||
Object.freeze(value);
|
Object.freeze(value);
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
select(requestedPackages: readonly string[]): ClientContractCatalog {
|
async select(requestedPackages: readonly string[]): Promise<ClientContractCatalog> {
|
||||||
if (closed) throw Error("CANDIDATE_METADATA_CLOSED");
|
if (closed) throw Error("CANDIDATE_METADATA_CLOSED");
|
||||||
for (const revision of requestedPackages)
|
for (const revision of requestedPackages)
|
||||||
if (!installed.has(revision))
|
if (!installed.has(revision))
|
||||||
@@ -129,22 +130,29 @@ export function createClientContractCatalog(
|
|||||||
if (!catalog) {
|
if (!catalog) {
|
||||||
// Like other candidate admission charges, this is an allocation estimate,
|
// Like other candidate admission charges, this is an allocation estimate,
|
||||||
// not a V8 heap limit. The process/cgroup ceiling bounds transient work.
|
// not a V8 heap limit. The process/cgroup ceiling bounds transient work.
|
||||||
const reservation = metadata.reserve(32 * 1024 * 1024);
|
preparing ??= (async () => {
|
||||||
try {
|
const reservation = metadata.reserve(32 * 1024 * 1024);
|
||||||
const compiled = clientContractCatalog(plan, installation, revisions, checkedKernel);
|
try {
|
||||||
const bytes = Buffer.byteLength(JSON.stringify(compiled));
|
const compiled = await clientContractCatalog(plan, installation, revisions, checkedKernel);
|
||||||
if (bytes > 8 * 1024 * 1024) throw Error("CANDIDATE_CATALOG_LIMIT");
|
if (closed) throw Error("CANDIDATE_METADATA_CLOSED");
|
||||||
freeze(compiled);
|
const bytes = Buffer.byteLength(JSON.stringify(compiled));
|
||||||
// No await between reservations: replacing the transient upper bound
|
if (bytes > 8 * 1024 * 1024) throw Error("CANDIDATE_CATALOG_LIMIT");
|
||||||
// with a smaller retained estimate cannot race another admission.
|
freeze(compiled);
|
||||||
reservation();
|
// No await between reservations: replacing the transient upper bound
|
||||||
release = metadata.reserve(Math.max(64 * 1024, bytes * 4));
|
// with a smaller retained estimate cannot race another admission.
|
||||||
catalog = compiled;
|
reservation();
|
||||||
} catch (error) {
|
release = metadata.reserve(Math.max(64 * 1024, bytes * 4));
|
||||||
reservation();
|
catalog = compiled;
|
||||||
throw error;
|
} catch (error) {
|
||||||
}
|
reservation();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
})().finally(() => {
|
||||||
|
preparing = undefined;
|
||||||
|
});
|
||||||
|
await preparing;
|
||||||
}
|
}
|
||||||
|
if (closed || !catalog) throw Error("CANDIDATE_METADATA_CLOSED");
|
||||||
const requested = new Set(requestedPackages);
|
const requested = new Set(requestedPackages);
|
||||||
return {
|
return {
|
||||||
inspections: catalog.inspections,
|
inspections: catalog.inspections,
|
||||||
|
|||||||
@@ -1,22 +1,17 @@
|
|||||||
import { compileInspectionTemplates, type InspectionKernel } from "./inspection.js";
|
import { compileInspectionTemplates, type InspectionKernel } from "./inspection.js";
|
||||||
import { hexDigest, type ExecutionPlan } from "./plan.js";
|
import { hexDigestAsync, type ExecutionPlan } from "./plan.js";
|
||||||
import { clientContractCatalog, type ExecutionInstallation } from "./installation.js";
|
import type { ClientContractCatalog } from "./installation.js";
|
||||||
import { publicOperationContract, publicQueryContract } from "./client-contracts.js";
|
import { publicOperationContract, publicQueryContract } from "./client-contracts.js";
|
||||||
|
|
||||||
/** Explicit public projection of checked inputs. Never serialize conformance
|
/** Explicit public projection of checked inputs. Never serialize conformance
|
||||||
* storage or package ports, or recompile a historical Candidate at runtime. */
|
* storage or package ports, or recompile a historical Candidate at runtime.
|
||||||
export function publicDefinitionSnapshot(
|
* The caller supplies that Candidate's checked, cached public catalog. */
|
||||||
|
export async function publicDefinitionSnapshot(
|
||||||
plan: ExecutionPlan,
|
plan: ExecutionPlan,
|
||||||
installation: ExecutionInstallation,
|
|
||||||
kernel: InspectionKernel,
|
kernel: InspectionKernel,
|
||||||
|
catalog: ClientContractCatalog,
|
||||||
) {
|
) {
|
||||||
const workspace = plan.definition.workspace;
|
const workspace = plan.definition.workspace;
|
||||||
const catalog = clientContractCatalog(
|
|
||||||
plan,
|
|
||||||
installation,
|
|
||||||
installation.packages.map((pkg) => pkg.revision),
|
|
||||||
kernel,
|
|
||||||
);
|
|
||||||
const rows = {
|
const rows = {
|
||||||
atoms: workspace.atoms.map((atom) => ({ id: atom.id, name: atom.displayName, documentation: atom.documentation })),
|
atoms: workspace.atoms.map((atom) => ({ id: atom.id, name: atom.displayName, documentation: atom.documentation })),
|
||||||
interfaces: workspace.interfaceImports.map((iface) => ({
|
interfaces: workspace.interfaceImports.map((iface) => ({
|
||||||
@@ -95,5 +90,5 @@ export function publicDefinitionSnapshot(
|
|||||||
{ id: string; [key: string]: unknown }[]
|
{ id: string; [key: string]: unknown }[]
|
||||||
>;
|
>;
|
||||||
for (const entries of Object.values(publicRows)) entries.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
for (const entries of Object.values(publicRows)) entries.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
||||||
return { codec: "camino-definitions-v1" as const, id: hexDigest(publicRows), rows: publicRows };
|
return { codec: "camino-definitions-v1" as const, id: await hexDigestAsync(publicRows), rows: publicRows };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ const prepare = async (workspace: WorkspaceRevision) => {
|
|||||||
test("execution reuse ignores world/source/display changes and unrelated packages", async () => {
|
test("execution reuse ignores world/source/display changes and unrelated packages", async () => {
|
||||||
const workspace = await loadReaderWorkspace(),
|
const workspace = await loadReaderWorkspace(),
|
||||||
original = await prepare(workspace);
|
original = await prepare(workspace);
|
||||||
const before = original.groups()[0]!.executionDigest;
|
const before = (await original.groups())[0]!.executionDigest;
|
||||||
workspace.sourceRootCommit = "f".repeat(40);
|
workspace.sourceRootCommit = "f".repeat(40);
|
||||||
workspace.packageImports[0]!.displayName = "Renamed";
|
workspace.packageImports[0]!.displayName = "Renamed";
|
||||||
workspace.packageImports[0]!.source.commit = "e".repeat(40);
|
workspace.packageImports[0]!.source.commit = "e".repeat(40);
|
||||||
@@ -59,13 +59,13 @@ test("execution reuse ignores world/source/display changes and unrelated package
|
|||||||
const after = await prepare(workspace);
|
const after = await prepare(workspace);
|
||||||
assert.notEqual(after.plan.digest, original.plan.digest);
|
assert.notEqual(after.plan.digest, original.plan.digest);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
after.groups().find((g) => g.logicalId === workspace.packageImports[0]!.packageId)!.executionDigest,
|
(await after.groups()).find((g) => g.logicalId === workspace.packageImports[0]!.packageId)!.executionDigest,
|
||||||
before,
|
before,
|
||||||
);
|
);
|
||||||
original.environments[workspace.packageImports[0]!.revisionId]!.credentialGeneration = "config:2";
|
original.environments[workspace.packageImports[0]!.revisionId]!.credentialGeneration = "config:2";
|
||||||
assert.notEqual(original.groups()[0]!.executionDigest, before);
|
assert.notEqual((await original.groups())[0]!.executionDigest, before);
|
||||||
original.environments[workspace.packageImports[0]!.revisionId]!.placement = "module";
|
original.environments[workspace.packageImports[0]!.revisionId]!.placement = "module";
|
||||||
assert.notEqual(original.groups()[0]!.executionDigest, before);
|
assert.notEqual((await original.groups())[0]!.executionDigest, before);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("outbound provider changes invalidate consumers while caller changes preserve providers", async () => {
|
test("outbound provider changes invalidate consumers while caller changes preserve providers", async () => {
|
||||||
@@ -108,15 +108,15 @@ test("outbound provider changes invalidate consumers while caller changes preser
|
|||||||
dependencies: [{ portId, binding: { kind: "state", slotId: id.slot("slot:document:content") } }],
|
dependencies: [{ portId, binding: { kind: "state", slotId: id.slot("slot:document:content") } }],
|
||||||
};
|
};
|
||||||
const prepared = await prepare(workspace);
|
const prepared = await prepare(workspace);
|
||||||
const values = () => new Map(prepared.groups().map((g) => [g.logicalId, g.executionDigest]));
|
const values = async () => new Map((await prepared.groups()).map((g) => [g.logicalId, g.executionDigest]));
|
||||||
const before = values();
|
const before = await values();
|
||||||
prepared.installation.packages.find((p) => p.revision === "package:reader-runtime@1")!.service!.contentDigest =
|
prepared.installation.packages.find((p) => p.revision === "package:reader-runtime@1")!.service!.contentDigest =
|
||||||
"c".repeat(64);
|
"c".repeat(64);
|
||||||
const callerChanged = values();
|
const callerChanged = await values();
|
||||||
assert.equal(callerChanged.get("provider"), before.get("provider"));
|
assert.equal(callerChanged.get("provider"), before.get("provider"));
|
||||||
assert.notEqual(callerChanged.get("package:reader-runtime"), before.get("package:reader-runtime"));
|
assert.notEqual(callerChanged.get("package:reader-runtime"), before.get("package:reader-runtime"));
|
||||||
prepared.installation.packages.find((p) => p.revision === revision)!.service!.contentDigest = "d".repeat(64);
|
prepared.installation.packages.find((p) => p.revision === revision)!.service!.contentDigest = "d".repeat(64);
|
||||||
const providerChanged = values();
|
const providerChanged = await values();
|
||||||
assert.notEqual(providerChanged.get("provider"), callerChanged.get("provider"));
|
assert.notEqual(providerChanged.get("provider"), callerChanged.get("provider"));
|
||||||
assert.notEqual(providerChanged.get("package:reader-runtime"), callerChanged.get("package:reader-runtime"));
|
assert.notEqual(providerChanged.get("package:reader-runtime"), callerChanged.get("package:reader-runtime"));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
checkExecutionWorld,
|
checkExecutionWorld,
|
||||||
checkedWorldDigest,
|
checkedWorldDigest,
|
||||||
implementationDigest,
|
implementationDigest,
|
||||||
|
clientContractCatalog,
|
||||||
hexDigest,
|
hexDigest,
|
||||||
parsePackageArtifacts,
|
parsePackageArtifacts,
|
||||||
artifactPath,
|
artifactPath,
|
||||||
@@ -386,7 +387,13 @@ test("public reflection contains nominal contracts and callable descriptors with
|
|||||||
routes: {},
|
routes: {},
|
||||||
};
|
};
|
||||||
const kernel = compileKernelProgram(plan) as import("../src/execution-world/inspection.js").InspectionKernel;
|
const kernel = compileKernelProgram(plan) as import("../src/execution-world/inspection.js").InspectionKernel;
|
||||||
const snapshot = publicDefinitionSnapshot(plan, installation, kernel);
|
const catalog = await clientContractCatalog(
|
||||||
|
plan,
|
||||||
|
installation,
|
||||||
|
installation.packages.map((pkg) => pkg.revision),
|
||||||
|
kernel,
|
||||||
|
);
|
||||||
|
const snapshot = await publicDefinitionSnapshot(plan, kernel, catalog);
|
||||||
assert.ok(snapshot.rows.interfaces.length);
|
assert.ok(snapshot.rows.interfaces.length);
|
||||||
assert.ok(snapshot.rows.queries.length);
|
assert.ok(snapshot.rows.queries.length);
|
||||||
assert.ok(snapshot.rows.conformances.length);
|
assert.ok(snapshot.rows.conformances.length);
|
||||||
@@ -401,13 +408,19 @@ test("public reflection contains nominal contracts and callable descriptors with
|
|||||||
assert.ok(!publicText.includes('"' + privateKey + '"'), privateKey);
|
assert.ok(!publicText.includes('"' + privateKey + '"'), privateKey);
|
||||||
assert.ok(publicText.includes('"descriptor"'));
|
assert.ok(publicText.includes('"descriptor"'));
|
||||||
assert.ok(publicText.includes('"scope":"class"'));
|
assert.ok(publicText.includes('"scope":"class"'));
|
||||||
assert.equal(snapshot.id, publicDefinitionSnapshot(plan, installation, kernel).id);
|
assert.equal(snapshot.id, (await publicDefinitionSnapshot(plan, kernel, catalog)).id);
|
||||||
// Historical checked availability must survive a newer host compiler. If
|
// Historical checked availability must survive a newer host compiler. If
|
||||||
// reflection silently regenerates the Kernel, this evidence disappears.
|
// reflection silently regenerates the Kernel, this evidence disappears.
|
||||||
const historical = structuredClone(kernel);
|
const historical = structuredClone(kernel);
|
||||||
const installed = historical.operations.find((operation) => !operation.unsupported)!;
|
const installed = historical.operations.find((operation) => !operation.unsupported)!;
|
||||||
assert.ok(installed);
|
assert.ok(installed);
|
||||||
installed.unsupported = "CHECKED_HISTORICAL_UNSUPPORTED";
|
installed.unsupported = "CHECKED_HISTORICAL_UNSUPPORTED";
|
||||||
const historicalSnapshot = publicDefinitionSnapshot(plan, installation, historical);
|
const historicalCatalog = await clientContractCatalog(
|
||||||
|
plan,
|
||||||
|
installation,
|
||||||
|
installation.packages.map((pkg) => pkg.revision),
|
||||||
|
historical,
|
||||||
|
);
|
||||||
|
const historicalSnapshot = await publicDefinitionSnapshot(plan, historical, historicalCatalog);
|
||||||
assert.match(JSON.stringify(historicalSnapshot.rows.conformances), /CHECKED_HISTORICAL_UNSUPPORTED/);
|
assert.match(JSON.stringify(historicalSnapshot.rows.conformances), /CHECKED_HISTORICAL_UNSUPPORTED/);
|
||||||
});
|
});
|
||||||
|
|||||||
+12
-10
@@ -249,13 +249,13 @@ test("checked component-only artifacts install without a service and reject miss
|
|||||||
assert.ok(installation.routes[installation.components[0]!.url]);
|
assert.ok(installation.routes[installation.components[0]!.url]);
|
||||||
assert.equal(JSON.stringify(installation.components).includes(root), false);
|
assert.equal(JSON.stringify(installation.components).includes(root), false);
|
||||||
assert.equal(JSON.stringify(installation.components).includes("slot:"), false);
|
assert.equal(JSON.stringify(installation.components).includes("slot:"), false);
|
||||||
const catalog = clientContractCatalog(plan, installation, [pkg.revision]);
|
const catalog = await clientContractCatalog(plan, installation, [pkg.revision]);
|
||||||
assert.equal(catalog.presentations[0]!.component, "component:card");
|
assert.equal(catalog.presentations[0]!.component, "component:card");
|
||||||
assert.equal(catalog.components[0]!.subject, "interface:Data@1");
|
assert.equal(catalog.components[0]!.subject, "interface:Data@1");
|
||||||
assert.equal(JSON.stringify(catalog).includes(root), false);
|
assert.equal(JSON.stringify(catalog).includes(root), false);
|
||||||
assert.equal(JSON.stringify(catalog).includes("bindingSchema"), false);
|
assert.equal(JSON.stringify(catalog).includes("bindingSchema"), false);
|
||||||
assert.throws(() => clientContractCatalog(plan, installation, ["absent"]), /UNINSTALLED_PACKAGE/);
|
await assert.rejects(() => clientContractCatalog(plan, installation, ["absent"]), /UNINSTALLED_PACKAGE/);
|
||||||
assert.throws(
|
await assert.rejects(
|
||||||
() => clientContractCatalog(plan, { ...installation, plan: "bad" }, [pkg.revision]),
|
() => clientContractCatalog(plan, { ...installation, plan: "bad" }, [pkg.revision]),
|
||||||
/CATALOG_PLAN_MISMATCH/,
|
/CATALOG_PLAN_MISMATCH/,
|
||||||
);
|
);
|
||||||
@@ -268,14 +268,16 @@ test("checked component-only artifacts install without a service and reject miss
|
|||||||
compileKernelProgram(plan) as InspectionKernel,
|
compileKernelProgram(plan) as InspectionKernel,
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
assert.throws(() => prepared.select(["absent"]), /UNINSTALLED_PACKAGE/);
|
await assert.rejects(() => prepared.select(["absent"]), /UNINSTALLED_PACKAGE/);
|
||||||
assert.equal(scope.bytes, 0, "Unknown requests do not allocate compiler metadata");
|
assert.equal(scope.bytes, 0, "Unknown requests do not allocate compiler metadata");
|
||||||
assert.deepEqual(prepared.select([pkg.revision]), catalog);
|
const [first, coalesced] = await Promise.all([prepared.select([pkg.revision]), prepared.select([pkg.revision])]);
|
||||||
|
assert.deepEqual(first, catalog);
|
||||||
|
assert.equal(first.operations, coalesced.operations, "Concurrent preparation shares one metadata reservation");
|
||||||
const retained = scope.bytes;
|
const retained = scope.bytes;
|
||||||
assert.ok(retained > 0);
|
assert.ok(retained > 0);
|
||||||
assert.ok(retained < 32 * 1024 * 1024, "Small catalogs retain a proportional metadata charge");
|
assert.ok(retained < 32 * 1024 * 1024, "Small catalogs retain a proportional metadata charge");
|
||||||
const full = prepared.select([pkg.revision]);
|
const full = await prepared.select([pkg.revision]);
|
||||||
const empty = prepared.select([]);
|
const empty = await prepared.select([]);
|
||||||
assert.deepEqual(empty.components, []);
|
assert.deepEqual(empty.components, []);
|
||||||
assert.deepEqual(empty.presentations, []);
|
assert.deepEqual(empty.presentations, []);
|
||||||
assert.deepEqual(empty.moduleBindings, []);
|
assert.deepEqual(empty.moduleBindings, []);
|
||||||
@@ -290,14 +292,14 @@ test("checked component-only artifacts install without a service and reject miss
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
assert.deepEqual(prepared.select([pkg.revision]), full);
|
assert.deepEqual(await prepared.select([pkg.revision]), full);
|
||||||
assert.equal(scope.bytes, retained, "Package projections do not accumulate cache entries");
|
assert.equal(scope.bytes, retained, "Package projections do not accumulate cache entries");
|
||||||
} finally {
|
} finally {
|
||||||
Object.defineProperty(plan, "definition", { configurable: true, value: definition });
|
Object.defineProperty(plan, "definition", { configurable: true, value: definition });
|
||||||
}
|
}
|
||||||
prepared.close();
|
prepared.close();
|
||||||
assert.equal(scope.bytes, 0);
|
assert.equal(scope.bytes, 0);
|
||||||
assert.throws(() => prepared.select([pkg.revision]), /CANDIDATE_METADATA_CLOSED/);
|
await assert.rejects(() => prepared.select([pkg.revision]), /CANDIDATE_METADATA_CLOSED/);
|
||||||
} finally {
|
} finally {
|
||||||
prepared.close();
|
prepared.close();
|
||||||
scope.release();
|
scope.release();
|
||||||
@@ -305,7 +307,7 @@ test("checked component-only artifacts install without a service and reject miss
|
|||||||
const tight = new CandidateMetadataBudget({ bytes: 1000, ownerBytes: 1000, candidates: 1 });
|
const tight = new CandidateMetadataBudget({ bytes: 1000, ownerBytes: 1000, candidates: 1 });
|
||||||
const tightScope = tight.acquire("catalog-test");
|
const tightScope = tight.acquire("catalog-test");
|
||||||
const refused = createClientContractCatalog(plan, installation, tightScope);
|
const refused = createClientContractCatalog(plan, installation, tightScope);
|
||||||
assert.throws(() => refused.select([pkg.revision]), /CANDIDATE_METADATA_LIMIT/);
|
await assert.rejects(() => refused.select([pkg.revision]), /CANDIDATE_METADATA_LIMIT/);
|
||||||
assert.equal(tightScope.bytes, 0);
|
assert.equal(tightScope.bytes, 0);
|
||||||
refused.close();
|
refused.close();
|
||||||
tightScope.release();
|
tightScope.release();
|
||||||
|
|||||||
Reference in New Issue
Block a user