@@ -28,63 +28,97 @@ class InvalidWorkerError extends Error {
28
28
}
29
29
}
30
30
31
+ // A class that handles creating, maintaining, and communicating with the
32
+ // worker that we spawn to perform linting.
31
33
class JobManager {
32
34
constructor ( ) {
33
35
this . handlersForJobs = new Map ( ) ;
34
36
this . worker = null ;
35
37
this . workerPath = Path . join ( __dirname , 'worker.js' ) ;
36
-
37
- this . createWorker ( ) ;
38
38
}
39
39
40
40
dispose ( ) {
41
41
this . killWorker ( ) ;
42
+ this . worker = null ;
43
+ this . _workerPromise = null ;
44
+ this . handlersForJobs = null ;
42
45
}
43
46
47
+ // Resolves when the worker is spawned and ready to process messages. Rejects
48
+ // if the worker errors during startup.
44
49
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
+
45
58
let nodeBin = Config . get ( 'nodeBin' ) ;
46
- console . debug ( 'JobManager creating worker at:' , nodeBin , this . workerPath ) ;
59
+ console . debug ( 'JobManager creating worker at:' , nodeBin ) ;
47
60
this . killWorker ( ) ;
48
61
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 .
51
64
//
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.
56
68
//
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 ) {
67
74
this . worker = false ;
68
- return false ;
75
+ throw new InvalidWorkerError ( ) ;
69
76
}
70
77
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
+ } ) ;
72
109
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 ) ;
76
114
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
+ }
82
117
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 ;
88
122
}
89
123
90
124
killWorker ( ) {
@@ -136,20 +170,14 @@ class JobManager {
136
170
}
137
171
138
172
async send ( bundle ) {
173
+ if ( ! this . worker ) {
174
+ console . warn ( 'Creating worker' ) ;
175
+ await this . createWorker ( ) ;
176
+ }
177
+
139
178
let key = generateKey ( ) ;
140
179
bundle . key = key ;
141
180
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
- }
153
181
154
182
return new Promise ( ( resolve , reject ) => {
155
183
this . handlersForJobs . set ( key , [ resolve , reject ] ) ;
0 commit comments