Files
rea/src/browser/modules/WebModuleArtifacts.ts
T
morluto b955b825fc fix(artifacts): retain read cleanup ownership through CLI and MCP shutdown
Keep failed stable-read handles and WABT runtime roots under retryable resource scopes. Preserve cancellation causes and primary errors, close the same service instances bound to MCP tools, and consolidate CLI operation/cleanup projection.

Validation: fast typecheck and lint, cached runtime build, docs checks, SDK schema validation, focused service and real-file cleanup tests, CLI cleanup regressions, and MCP lifecycle/shutdown workflows. Provider process fixtures simulate engine output; real WABT and Linux EVM execution are not established by this pass.
2026-10-13 16:08:41 +08:00

115 lines
3.8 KiB
TypeScript

import type {
WebModuleArtifactPort,
WebModuleArtifacts,
} from "../../application/WebModulePorts.js";
import type { WebScriptArtifactPort } from "../../application/WebScriptArtifactPort.js";
import type { ExecutionOptions } from "../../application/AnalysisProvider.js";
import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
import { jsonObjectSchema } from "../../domain/jsonValue.js";
import { err, ok } from "../../domain/result.js";
import type { WebModuleTraceInput } from "../../domain/webModuleTrace.js";
import { LocalWebScriptArtifacts } from "../assets/LocalWebScriptArtifacts.js";
import {
readWebArtifactJson,
webArtifactReadError,
} from "../assets/WebArtifactReads.js";
/** Add an explicitly selected import map to a verified captured-script source. */
export class LocalWebModuleArtifacts implements WebModuleArtifactPort {
readonly scripts: WebScriptArtifactPort;
readonly #resources: ArtifactResourceScope;
constructor(
scripts?: WebScriptArtifactPort,
resources = scripts instanceof LocalWebScriptArtifacts
? scripts.resources
: new ArtifactResourceScope(),
) {
this.#resources = resources;
this.scripts =
scripts ??
new LocalWebScriptArtifacts("trace_web_module_imports", resources);
}
async close(): Promise<void> {
const closeScript =
this.scripts instanceof LocalWebScriptArtifacts &&
this.scripts.resources === this.#resources
? undefined
: this.scripts.close?.bind(this.scripts);
const results = await Promise.allSettled([
Promise.resolve().then(() => this.#resources.close()),
...(closeScript === undefined
? []
: [Promise.resolve().then(closeScript)]),
]);
const failures = results.flatMap((result) =>
result.status === "rejected" ? [result.reason] : [],
);
if (failures.length > 0)
throw new AggregateError(failures, "Web module artifact cleanup failed");
}
/** Read selected source and optional map without website requests. */
async load(input: WebModuleTraceInput, options?: ExecutionOptions) {
try {
return await this.#resources.run(() => this.#load(input, options));
} catch (cause: unknown) {
return err(
webArtifactReadError(
cause,
{
operation: "trace_web_module_imports",
field:
input.import_map === undefined
? ["manifest_path"]
: ["import_map", "path"],
targetPath: input.import_map?.path ?? input.manifest_path,
callerSelected: input.import_map !== undefined,
},
options?.signal,
),
);
}
}
async #load(input: WebModuleTraceInput, options?: ExecutionOptions) {
const loaded = await this.scripts.load(input, options);
if (!loaded.ok) return loaded;
if (input.import_map === undefined)
return ok({ ...loaded.value, importMap: null });
const selected = input.import_map;
try {
const data = await readWebArtifactJson(
selected.path,
4 * 1024 * 1024,
options?.signal,
this.#resources,
);
const importMap: WebModuleArtifacts["importMap"] = {
file: {
path: selected.path,
sha256: data.sha256,
bytes: data.bytes.length,
},
baseUrl: selected.base_url,
value: jsonObjectSchema.parse(data.value),
};
return ok({ ...loaded.value, importMap });
} catch (cause: unknown) {
return err(
webArtifactReadError(
cause,
{
operation: "trace_web_module_imports",
field: ["import_map", "path"],
targetPath: selected.path,
callerSelected: true,
},
options?.signal,
),
);
}
}
}