@@ -28,63 +28,97 @@ class InvalidWorkerError extends Error {
2828 }
2929}
3030
31+ // A class that handles creating, maintaining, and communicating with the
32+ // worker that we spawn to perform linting.
3133class JobManager {
3234 constructor ( ) {
3335 this . handlersForJobs = new Map ( ) ;
3436 this . worker = null ;
3537 this . workerPath = Path . join ( __dirname , 'worker.js' ) ;
36-
37- this . createWorker ( ) ;
3838 }
3939
4040 dispose ( ) {
4141 this . killWorker ( ) ;
42+ this . worker = null ;
43+ this . _workerPromise = null ;
44+ this . handlersForJobs = null ;
4245 }
4346
47+ // Resolves when the worker is spawned and ready to process messages. Rejects
48+ // if the worker errors during startup.
4449 createWorker ( ) {
50+ // When reloading an existing project with X tabs open, this method will be
51+ // called X times in a very short span of time. They all need to wait for
52+ // the _same_ worker to spawn instead of each trying to spawn their own.
53+ if ( this . _workerPromise ) {
54+ // The worker is already in the process of being created.
55+ return this . _workerPromise ;
56+ }
57+
4558 let nodeBin = Config . get ( 'nodeBin' ) ;
46- console . debug ( 'JobManager creating worker at:' , nodeBin , this . workerPath ) ;
59+ console . debug ( 'JobManager creating worker at:' , nodeBin ) ;
4760 this . killWorker ( ) ;
4861
49- // We should not try to start the worker without testing the value we have
50- // for `nodeBin` .
62+ // We choose to do a sync test here because this method is much easier to
63+ // reason about without an `await` keyword introducing side effects .
5164 //
52- // When the setting is changed after initialization, we test it
53- // asynchronously before calling `createWorker` again. In those cases,
54- // `testSync` just looks up the result of that test so we don't duplicate
55- // effort.
65+ // In practice, this will result in only one brief call to `execSync` when
66+ // a project window is created/reloaded; subsequent calls with the same
67+ // `nodeBin` argument will reuse the earlier value.
5668 //
57- // But on startup, we don't want to defer creation of this worker while we
58- // perform an async test of `nodeBin`. So in that one scenario, `testSync`
59- // will do an `execSync` on this value to perform a sanity check. Like the
60- // async version, we remember this result, so further calls to `testSync`
61- // with the same value won't block while we run a shell command.
62- //
63- // TODO: See if there's a way to use the async test logic on startup
64- // without putting us in async/promise hell.
65- if ( ! NodePathTester . testSync ( nodeBin ) ) {
66- console . error ( 'Invalid nodeBin!' ) ;
69+ // When `nodeBin` is changed in the middle of a session, we validate the
70+ // new value asynchronously _before_ we reach this method, and `testSync`
71+ // merely looks up the async validation's result.
72+ let isValid = NodePathTester . testSync ( nodeBin ) ;
73+ if ( ! isValid ) {
6774 this . worker = false ;
68- return false ;
75+ throw new InvalidWorkerError ( ) ;
6976 }
7077
71- this . worker = spawn ( nodeBin , [ this . workerPath ] ) ;
78+ let promise = new Promise ( ( resolve , reject ) => {
79+ this . worker = spawn ( nodeBin , [ this . workerPath ] ) ;
80+
81+ // Reject this promise if the worker fails to spawn.
82+ this . worker . on ( 'error' , reject ) ;
83+
84+ this . worker . stdout
85+ . pipe ( ndjson . parse ( ) )
86+ . on ( 'data' , ( obj ) => {
87+ // We could listen for the `spawn` event to know when the worker is
88+ // ready, but that event wasn't added until Node v14.17. Instead,
89+ // we'll just have the worker emit a `ready` message.
90+ if ( obj . type === 'ready' ) {
91+ resolve ( ) ;
92+ } else {
93+ this . receiveMessage ( obj ) ;
94+ }
95+ } ) ;
96+
97+ // Even unanticipated runtime errors will get sent as newline-delimited
98+ // JSON.
99+ this . worker . stderr
100+ . pipe ( ndjson . parse ( ) )
101+ . on ( 'data' , this . receiveError . bind ( this ) ) ;
102+
103+ this . worker . on ( 'close' , ( ) => {
104+ if ( this . worker . killed === false ) {
105+ this . createWorker ( ) ;
106+ }
107+ } ) ;
108+ } ) ;
72109
73- this . worker . stdout
74- . pipe ( ndjson . parse ( ) )
75- . on ( 'data' , this . receiveMessage . bind ( this ) ) ;
110+ this . _workerPromise = promise ;
111+ this . _workerPromise
112+ . then ( ( ) => this . _workerPromise = null )
113+ . catch ( ( ) => this . _workerPromise = null ) ;
76114
77- // Even unanticipated runtime errors will get sent as newline-delimited
78- // JSON.
79- this . worker . stderr
80- . pipe ( ndjson . parse ( ) )
81- . on ( 'data' , this . receiveError . bind ( this ) ) ;
115+ return promise ;
116+ }
82117
83- this . worker . on ( 'close' , ( ) => {
84- if ( this . worker . killed === false ) {
85- this . createWorker ( ) ;
86- }
87- } ) ;
118+ suspend ( ) {
119+ console . warn ( 'Suspending worker' ) ;
120+ this . killWorker ( ) ;
121+ this . worker = null ;
88122 }
89123
90124 killWorker ( ) {
@@ -136,20 +170,14 @@ class JobManager {
136170 }
137171
138172 async send ( bundle ) {
173+ if ( ! this . worker ) {
174+ console . warn ( 'Creating worker' ) ;
175+ await this . createWorker ( ) ;
176+ }
177+
139178 let key = generateKey ( ) ;
140179 bundle . key = key ;
141180 console . debug ( 'JobManager#send:' , bundle ) ;
142- try {
143- this . ensureWorker ( ) ;
144- } catch ( err ) {
145- if ( this . worker === false ) {
146- // `false` means we intentionally refused to create a worker because
147- // `nodeBin` was invalid.
148- throw new InvalidWorkerError ( ) ;
149- } else {
150- throw err ;
151- }
152- }
153181
154182 return new Promise ( ( resolve , reject ) => {
155183 this . handlersForJobs . set ( key , [ resolve , reject ] ) ;
0 commit comments