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" ;
55import * as path from "path" ;
66import * as fs from "fs/promises" ;
77import { 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 */
5454export 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