mirror of
https://github.com/morluto/rea
synced 2026-10-27 06:33:32 -04:00
5fdb0636b4
analyze_javascript_application ran several long phases with no event-loop turn, so MCP ping and cancellation waited: each file's parse, analysis and semantic projection; building every application node and relationship before the application graph's first sealing step; and whole-graph schema parses inside both immutable graph factories. - Each file runs as steps (parse, static analysis, semantic analysis, then each projection pass), driven by the existing time-sliced checkpoint. - Application graph input construction yields between phases, per module link and per file of findings. - The pipeline transfers its privately built graph inputs to new sealing steps that validate the graph's own fields synchronously and elements, content checks and the digest in steps. The public copying factories keep their synchronous capture. Results are unchanged: graph IDs and the complete result digest match main on five inputs including Obsidian's 23 MB obsidian.asar. On the issue's 1,200-function fixture the longest event-loop block falls from about 290 ms to 90 ms, and ping after abort from up to 286 ms to at most 56 ms. On obsidian.asar it falls from 10.9 s to 5.4 s; the rest is one analyzer stage of a single very large file. Refs #1462 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: N0zoM1z0 <161784452+N0zoM1z0@users.noreply.github.com>
151 lines
5.4 KiB
TypeScript
151 lines
5.4 KiB
TypeScript
import { setImmediate } from "node:timers";
|
|
import { writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { describe, expect, it } from "vitest";
|
|
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
|
|
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
|
|
import { analyzeJavaScriptApplication } from "./JavaScriptApplicationService.js";
|
|
|
|
describe("JavaScript analysis cancellation before publication", () => {
|
|
it("interrupts semantic graph commitment before result validation", async () => {
|
|
const root = await createTestTempDirectory("rea-js-cancel-semantic-");
|
|
await writeFile(
|
|
join(root, "main.js"),
|
|
Array.from(
|
|
{ length: 500 },
|
|
(_, index) =>
|
|
`export function inspect${index}(value) { return JSON.stringify(value); }`,
|
|
).join("\n"),
|
|
);
|
|
const controller = new AbortController();
|
|
const phases: string[] = [];
|
|
const result = await analyzeJavaScriptApplication(
|
|
{ input_path: root, format: "directory" },
|
|
{
|
|
signal: controller.signal,
|
|
progress: {
|
|
async report(event) {
|
|
phases.push(event.phase);
|
|
if (event.phase === "seal_javascript_semantic_graph")
|
|
setImmediate(() => controller.abort());
|
|
},
|
|
},
|
|
},
|
|
);
|
|
if (result.ok)
|
|
throw new Error("Cancelled commitment must not publish Evidence");
|
|
expect(projectAnalysisError(result.error)).toMatchObject({
|
|
code: "cancelled",
|
|
details: { reason: "cancelled" },
|
|
});
|
|
expect(phases).toContain("seal_javascript_semantic_graph");
|
|
expect(phases).not.toContain("validate_javascript_application_result");
|
|
});
|
|
|
|
it("handles control turns and cancellation inside one large source file (#1462)", async () => {
|
|
const root = await createTestTempDirectory("rea-js-cancel-in-file-");
|
|
await writeFile(
|
|
join(root, "index.js"),
|
|
Array.from(
|
|
{ length: 1200 },
|
|
(_, index) =>
|
|
`export function f${index}(){return {name:'f${index}',value:${index}};}`,
|
|
).join("\n"),
|
|
);
|
|
const controller = new AbortController();
|
|
const phases: string[] = [];
|
|
let turns = 0;
|
|
// Count event-loop turns once the file starts; parsing, analysis and
|
|
// projection of this one file must leave room for them.
|
|
const tick = (): void => {
|
|
turns += 1;
|
|
if (turns === 3) controller.abort();
|
|
else if (!controller.signal.aborted) setImmediate(tick);
|
|
};
|
|
const result = await analyzeJavaScriptApplication(
|
|
{ input_path: root, format: "directory" },
|
|
{
|
|
signal: controller.signal,
|
|
progress: {
|
|
async report(event) {
|
|
phases.push(event.phase);
|
|
if (event.phase === "parse_javascript_source") setImmediate(tick);
|
|
},
|
|
},
|
|
},
|
|
);
|
|
if (result.ok) throw new Error("Cancelled analysis must not publish");
|
|
expect(projectAnalysisError(result.error)).toMatchObject({
|
|
code: "cancelled",
|
|
});
|
|
expect(phases).toContain("parse_javascript_source");
|
|
expect(phases).not.toContain("build_javascript_application_graph");
|
|
});
|
|
|
|
it.each([
|
|
"parse_javascript_source",
|
|
"build_javascript_application_graph",
|
|
"seal_javascript_application_graph",
|
|
"build_javascript_semantic_graph",
|
|
"seal_javascript_semantic_graph",
|
|
"validate_javascript_application_result",
|
|
"create_javascript_application_evidence",
|
|
"seal_javascript_application_result",
|
|
"validate_javascript_application_evidence",
|
|
"hash_javascript_application_evidence",
|
|
])("rejects cancellation scheduled during %s", async (phase) => {
|
|
const root = await createTestTempDirectory("rea-js-cancel-phase-");
|
|
await writeFile(
|
|
join(root, "main.js"),
|
|
"export function inspect(value) { return JSON.stringify(value); }\n",
|
|
);
|
|
const controller = new AbortController();
|
|
let requested = false;
|
|
const terminal: string[] = [];
|
|
const result = await analyzeJavaScriptApplication(
|
|
{ input_path: root, format: "directory" },
|
|
{
|
|
signal: controller.signal,
|
|
progress: {
|
|
async report(event) {
|
|
if (event.terminal) terminal.push(event.phase);
|
|
if (event.phase === phase) {
|
|
requested = true;
|
|
setImmediate(() => controller.abort());
|
|
}
|
|
},
|
|
},
|
|
},
|
|
);
|
|
expect(requested).toBe(true);
|
|
if (result.ok)
|
|
throw new Error("Cancelled analysis must not publish Evidence");
|
|
expect(projectAnalysisError(result.error)).toMatchObject({
|
|
code: "cancelled",
|
|
details: { reason: "cancelled" },
|
|
});
|
|
expect(terminal).toEqual([]);
|
|
});
|
|
|
|
it("honors cancellation observed during the terminal publication race", async () => {
|
|
const root = await createTestTempDirectory("rea-js-cancel-terminal-");
|
|
await writeFile(join(root, "main.js"), "export const value = 1;\n");
|
|
const controller = new AbortController();
|
|
const result = await analyzeJavaScriptApplication(
|
|
{ input_path: root, format: "directory" },
|
|
{
|
|
signal: controller.signal,
|
|
progress: {
|
|
async report(event) {
|
|
if (event.terminal) controller.abort();
|
|
},
|
|
},
|
|
},
|
|
);
|
|
if (result.ok) throw new Error("Expected cancellation before admission");
|
|
expect(projectAnalysisError(result.error)).toMatchObject({
|
|
details: { reason: "cancelled" },
|
|
});
|
|
});
|
|
});
|