Skip to content

Commit 5d69687

Browse files
committed
refactor layer analysis to use Worker threads instead of spawning processes
1 parent 535c7db commit 5d69687

3 files changed

Lines changed: 237 additions & 144 deletions

File tree

package.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,7 @@
2222
"type": "module",
2323
"files": [
2424
"dist",
25-
"bin",
26-
"src/layerResolverCli.ts",
27-
"src/layerResolverCore.ts"
25+
"bin"
2826
],
2927
"bin": {
3028
"effect-devtools": "./bin/effect-devtools"

src/layerAnalysis.ts

Lines changed: 71 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Layer Analysis Service
2-
// Spawns layer-resolver-cli as child process and streams results back
2+
// Uses a Worker thread to run layer analysis without blocking the UI
33

4-
import { Effect, Stream, Chunk } from "effect";
4+
import { Effect } from "effect";
55
import * as path from "path";
66
import * as fs from "fs/promises";
77
import { StoreActionsService } from "./storeActionsService";
@@ -49,7 +49,7 @@ export interface AnalysisResult {
4949

5050
/**
5151
* Run layer analysis on a TypeScript project
52-
* Spawns the layer-resolver-cli.ts script and streams JSON output
52+
* Uses a Worker thread to avoid blocking the UI
5353
*/
5454
export const runLayerAnalysis = (projectPath: string = process.cwd()) =>
5555
Effect.gen(function* () {
@@ -72,120 +72,83 @@ export const runLayerAnalysis = (projectPath: string = process.cwd()) =>
7272

7373
console.log(`Running layer analysis on ${tsconfigPath}`);
7474

75-
// Get the path to layerResolverCli.ts
76-
// When running from source: __dirname is src/
77-
// When running from npm package: files are at package-root/src/
78-
// We try multiple locations to handle both cases
79-
const cliPath = yield* findCliPath();
80-
81-
// Use Bun.spawn to run the analyzer with the same bun executable
82-
// Use process.execPath to get the path to the current bun executable
83-
// This ensures the analyzer works when installed globally or via npx
84-
const bunPath = process.execPath;
85-
console.log(`Spawning: ${bunPath} run ${cliPath} --json ${tsconfigPath}`);
86-
87-
// Store process reference for cleanup on interruption
88-
let spawnedProc: ReturnType<typeof Bun.spawn> | null = null;
89-
90-
const output = yield* Effect.tryPromise({
91-
try: async () => {
92-
const proc = Bun.spawn(
93-
[bunPath, "run", cliPath, "--json", tsconfigPath],
94-
{
95-
stdout: "pipe",
96-
stderr: "pipe",
97-
cwd: path.dirname(cliPath),
98-
},
75+
// Run analysis in a Worker thread
76+
const result = yield* Effect.async<AnalysisResult, Error>((resume) => {
77+
try {
78+
const worker = new Worker(
79+
new URL("./layerResolverWorker.ts", import.meta.url),
9980
);
100-
spawnedProc = proc;
101-
102-
// Use Bun's simpler API for reading streams
103-
const stdoutPromise = new Response(proc.stdout).text();
104-
const stderrPromise = new Response(proc.stderr).text();
105-
const exitPromise = proc.exited;
106-
107-
const [stdout, stderr, exitCode] = await Promise.all([
108-
stdoutPromise,
109-
stderrPromise,
110-
exitPromise,
111-
]);
112-
113-
if (exitCode === 0) {
114-
return stdout;
115-
} else {
116-
throw new Error(`Process exited with code ${exitCode}: ${stderr}`);
117-
}
118-
},
119-
catch: (error) => new Error(String(error)),
81+
82+
const timeout = setTimeout(() => {
83+
worker.terminate();
84+
resume(
85+
Effect.fail(
86+
new Error("Layer analysis timed out after 60 seconds"),
87+
),
88+
);
89+
}, 60000);
90+
91+
worker.onmessage = (event: MessageEvent<AnalysisResult>) => {
92+
clearTimeout(timeout);
93+
worker.terminate();
94+
resume(Effect.succeed(event.data));
95+
};
96+
97+
worker.onerror = (error: ErrorEvent) => {
98+
clearTimeout(timeout);
99+
worker.terminate();
100+
resume(Effect.fail(new Error(error.message)));
101+
};
102+
103+
console.log(`[LayerAnalysis] Posting analysis request for ${tsconfigPath}`);
104+
worker.postMessage({ tsconfigPath, projectPath });
105+
} catch (error) {
106+
resume(Effect.fail(new Error(String(error))));
107+
}
108+
});
109+
110+
// Handle the result
111+
yield* Effect.gen(function* () {
112+
const actions = yield* StoreActionsService;
113+
114+
if (result.status === "error") {
115+
yield* actions.setLayerAnalysisError(result.errors.join("\n"));
116+
yield* actions.setLayerAnalysisStatus("error");
117+
} else if (result.missing.length === 0) {
118+
yield* actions.setLayerAnalysisStatus("complete");
119+
yield* actions.setLayerAnalysisResults({
120+
missing: [],
121+
resolved: [],
122+
candidates: result.candidates || [],
123+
generatedCode: "",
124+
message: "No missing layer requirements found!",
125+
});
126+
} else {
127+
console.log(
128+
`[LayerAnalysis] Setting results with ${result.candidates?.length || 0} candidate groups`,
129+
);
130+
yield* actions.setLayerAnalysisStatus("complete");
131+
yield* actions.setLayerAnalysisResults({
132+
missing: result.missing,
133+
resolved: result.resolved,
134+
candidates: result.candidates || [],
135+
allLayers: result.allLayers || [],
136+
generatedCode: result.generatedCode,
137+
targetFile: result.targetFile,
138+
targetLine: result.targetLine,
139+
stillMissing: result.stillMissing,
140+
resolutionOrder: result.resolutionOrder,
141+
});
142+
}
120143
}).pipe(
121-
Effect.timeout("60 seconds"),
122-
Effect.onInterrupt(() =>
123-
Effect.sync(() => {
124-
if (spawnedProc) {
125-
spawnedProc.kill();
126-
console.log("[LayerAnalysis] Process killed due to interruption");
127-
}
128-
}),
129-
),
130-
Effect.tapError((error) =>
144+
Effect.catchAll((error) =>
131145
Effect.gen(function* () {
132146
const actions = yield* StoreActionsService;
133147
yield* actions.setLayerAnalysisError(`Analysis failed: ${error}`);
134148
yield* actions.setLayerAnalysisStatus("error");
135149
}),
136150
),
137151
);
138-
139-
yield* Effect.gen(function* () {
140-
const actions = yield* StoreActionsService;
141-
142-
try {
143-
// Parse the JSON output
144-
const result: AnalysisResult = JSON.parse(output);
145-
146-
console.log(`[LayerAnalysis] Parsed result:`, {
147-
status: result.status,
148-
missingCount: result.missing.length,
149-
candidatesCount: result.candidates?.length || 0,
150-
resolvedCount: result.resolved.length,
151-
});
152-
153-
if (result.status === "error") {
154-
yield* actions.setLayerAnalysisError(result.errors.join("\n"));
155-
yield* actions.setLayerAnalysisStatus("error");
156-
} else if (result.missing.length === 0) {
157-
yield* actions.setLayerAnalysisStatus("complete");
158-
yield* actions.setLayerAnalysisResults({
159-
missing: [],
160-
resolved: [],
161-
candidates: result.candidates || [],
162-
generatedCode: "",
163-
message: "No missing layer requirements found!",
164-
});
165-
} else {
166-
console.log(
167-
`[LayerAnalysis] Setting results with ${result.candidates?.length || 0} candidate groups`,
168-
);
169-
yield* actions.setLayerAnalysisStatus("complete");
170-
yield* actions.setLayerAnalysisResults({
171-
missing: result.missing,
172-
resolved: result.resolved,
173-
candidates: result.candidates || [],
174-
allLayers: result.allLayers || [],
175-
generatedCode: result.generatedCode,
176-
targetFile: result.targetFile,
177-
targetLine: result.targetLine,
178-
stillMissing: result.stillMissing,
179-
resolutionOrder: result.resolutionOrder,
180-
});
181-
}
182-
} catch (parseError) {
183-
yield* actions.setLayerAnalysisError(
184-
`Failed to parse analysis output: ${parseError}`,
185-
);
186-
yield* actions.setLayerAnalysisStatus("error");
187-
}
188-
});
189152
});
190153

191154
/**
@@ -344,36 +307,3 @@ const findTsConfig = (startPath: string) =>
344307

345308
return null;
346309
});
347-
348-
/**
349-
* Find the layerResolverCli.ts script
350-
* Searches multiple locations to handle both development and installed package scenarios
351-
*/
352-
const findCliPath = () =>
353-
Effect.gen(function* () {
354-
const candidates = [
355-
// Development: running from src directory
356-
path.resolve(__dirname, "./layerResolverCli.ts"),
357-
// npm package: files included at package-root/src/
358-
path.resolve(__dirname, "../src/layerResolverCli.ts"),
359-
// Compiled binary: look relative to executable
360-
path.resolve(path.dirname(process.execPath), "../src/layerResolverCli.ts"),
361-
path.resolve(path.dirname(process.execPath), "../../src/layerResolverCli.ts"),
362-
];
363-
364-
for (const candidate of candidates) {
365-
const exists = yield* Effect.tryPromise({
366-
try: () => fs.access(candidate).then(() => true),
367-
catch: () => false,
368-
});
369-
370-
if (exists) {
371-
console.log(`Found layerResolverCli.ts at ${candidate}`);
372-
return candidate;
373-
}
374-
}
375-
376-
// Fallback to first candidate and let it fail with a clear error
377-
console.log(`layerResolverCli.ts not found in any of: ${candidates.join(", ")}`);
378-
return candidates[0];
379-
});

0 commit comments

Comments
 (0)