1
1
import type { ChildProcess } from 'child_process'
2
2
import { Worker as JestWorker } from 'next/dist/compiled/jest-worker'
3
3
import { getNodeOptionsWithoutInspect } from '../server/lib/utils'
4
-
5
- // We need this as we're using `Promise.withResolvers` which is not available in the node typings
6
- import '../lib/polyfill-promise-with-resolvers'
7
-
8
4
type FarmOptions = ConstructorParameters < typeof JestWorker > [ 1 ]
9
5
10
6
const RESTARTED = Symbol ( 'restarted' )
@@ -17,58 +13,45 @@ const cleanupWorkers = (worker: JestWorker) => {
17
13
}
18
14
}
19
15
20
- type Options <
21
- T extends object = object ,
22
- Args extends any [ ] = any [ ]
23
- > = FarmOptions & {
24
- timeout ?: number
25
- onRestart ?: ( method : string , args : Args , attempts : number ) => void
26
- exposedMethods : ReadonlyArray < keyof T >
27
- enableWorkerThreads ?: boolean
28
- }
29
-
30
- export class Worker < T extends object = object , Args extends any [ ] = any [ ] > {
31
- private _worker ?: JestWorker
16
+ export class Worker {
17
+ private _worker : JestWorker | undefined
32
18
33
- /**
34
- * Creates a new worker with the correct typings associated with the selected
35
- * methods.
36
- */
37
- public static create < T extends object , Args extends any [ ] = any [ ] > (
19
+ constructor (
38
20
workerPath : string ,
39
- options : Options < T , Args >
40
- ) : Worker < T , Args > & T {
41
- return new Worker ( workerPath , options ) as Worker < T , Args > & T
42
- }
43
-
44
- constructor ( workerPath : string , options : Options < T , Args > ) {
21
+ options : FarmOptions & {
22
+ timeout ?: number
23
+ onRestart ?: ( method : string , args : any [ ] , attempts : number ) => void
24
+ exposedMethods : ReadonlyArray < string >
25
+ enableWorkerThreads ?: boolean
26
+ }
27
+ ) {
45
28
let { timeout, onRestart, ...farmOptions } = options
46
29
47
30
let restartPromise : Promise < typeof RESTARTED >
48
31
let resolveRestartPromise : ( arg : typeof RESTARTED ) => void
49
32
let activeTasks = 0
50
33
34
+ this . _worker = undefined
35
+
51
36
const createWorker = ( ) => {
52
- const worker = new JestWorker ( workerPath , {
37
+ this . _worker = new JestWorker ( workerPath , {
53
38
...farmOptions ,
54
39
forkOptions : {
55
40
...farmOptions . forkOptions ,
56
41
env : {
57
- ...farmOptions . forkOptions ?. env ,
42
+ ...( ( farmOptions . forkOptions ?. env || { } ) as any ) ,
58
43
...process . env ,
59
- // We don't pass down NODE_OPTIONS as it can lead to extra memory
60
- // usage,
44
+ // we don't pass down NODE_OPTIONS as it can
45
+ // extra memory usage
61
46
NODE_OPTIONS : getNodeOptionsWithoutInspect ( )
62
47
. replace ( / - - m a x - o l d - s p a c e - s i z e = [ \d ] { 1 , } / , '' )
63
48
. trim ( ) ,
64
- } ,
65
- stdio : 'inherit' ,
49
+ } as any ,
66
50
} ,
67
- } )
68
-
69
- const { promise, resolve } = Promise . withResolvers < typeof RESTARTED > ( )
70
- restartPromise = promise
71
- resolveRestartPromise = resolve
51
+ } ) as JestWorker
52
+ restartPromise = new Promise (
53
+ ( resolve ) => ( resolveRestartPromise = resolve )
54
+ )
72
55
73
56
/**
74
57
* Jest Worker has two worker types, ChildProcessWorker (uses child_process) and NodeThreadWorker (uses worker_threads)
@@ -80,14 +63,11 @@ export class Worker<T extends object = object, Args extends any[] = any[]> {
80
63
* But this property is not available in NodeThreadWorker, so we need to check if we are using ChildProcessWorker
81
64
*/
82
65
if ( ! farmOptions . enableWorkerThreads ) {
83
- const poolWorkers : { _child ?: ChildProcess } [ ] =
84
- // @ts -expect-error - we're accessing a private property
85
- worker . _workerPool ?. _workers ?? [ ]
86
-
87
- for ( const poolWorker of poolWorkers ) {
88
- if ( ! poolWorker . _child ) continue
89
-
90
- poolWorker . _child . once ( 'exit' , ( code , signal ) => {
66
+ for ( const worker of ( ( this . _worker as any ) . _workerPool ?. _workers ||
67
+ [ ] ) as {
68
+ _child ?: ChildProcess
69
+ } [ ] ) {
70
+ worker . _child ?. on ( 'exit' , ( code , signal ) => {
91
71
// log unexpected exit if .end() wasn't called
92
72
if ( ( code || ( signal && signal !== 'SIGINT' ) ) && this . _worker ) {
93
73
console . error (
@@ -98,22 +78,16 @@ export class Worker<T extends object = object, Args extends any[] = any[]> {
98
78
}
99
79
}
100
80
101
- return worker
81
+ this . _worker . getStdout ( ) . pipe ( process . stdout )
82
+ this . _worker . getStderr ( ) . pipe ( process . stderr )
102
83
}
103
-
104
- // Create the first worker.
105
- this . _worker = createWorker ( )
84
+ createWorker ( )
106
85
107
86
const onHanging = ( ) => {
108
87
const worker = this . _worker
109
88
if ( ! worker ) return
110
-
111
- // Grab the current restart promise, and create a new worker.
112
89
const resolve = resolveRestartPromise
113
- this . _worker = createWorker ( )
114
-
115
- // Once the old worker is ended, resolve the restart promise to signal to
116
- // any active tasks that the worker had to be restarted.
90
+ createWorker ( )
117
91
worker . end ( ) . then ( ( ) => {
118
92
resolve ( RESTARTED )
119
93
} )
@@ -122,62 +96,33 @@ export class Worker<T extends object = object, Args extends any[] = any[]> {
122
96
let hangingTimer : NodeJS . Timeout | false = false
123
97
124
98
const onActivity = ( ) => {
125
- // If there was an active hanging timer, clear it.
126
99
if ( hangingTimer ) clearTimeout ( hangingTimer )
127
-
128
- // If there are no active tasks, we don't need to start a new hanging
129
- // timer.
130
- if ( activeTasks === 0 ) return
131
-
132
- hangingTimer = setTimeout ( onHanging , timeout )
100
+ hangingTimer = activeTasks > 0 && setTimeout ( onHanging , timeout )
133
101
}
134
102
135
- const wrapMethodWithTimeout =
136
- ( methodName : keyof T ) =>
137
- async ( ...args : Args ) => {
138
- activeTasks ++
139
-
140
- try {
141
- let attempts = 0
142
- for ( ; ; ) {
143
- // Mark that we're doing work, we want to ensure that if the worker
144
- // halts for any reason, we restart it.
145
- onActivity ( )
146
-
147
- const result = await Promise . race ( [
148
- // Either we'll get the result from the worker, or we'll get the
149
- // restart promise to fire.
150
- // @ts -expect-error - we're grabbing a dynamic method on the worker
151
- this . _worker [ methodName ] ( ...args ) ,
152
- restartPromise ,
153
- ] )
154
-
155
- // If the result anything besides `RESTARTED`, we can return it, as
156
- // it's the actual result from the worker.
157
- if ( result !== RESTARTED ) {
158
- return result
103
+ for ( const method of farmOptions . exposedMethods ) {
104
+ if ( method . startsWith ( '_' ) ) continue
105
+ ; ( this as any ) [ method ] = timeout
106
+ ? // eslint-disable-next-line no-loop-func
107
+ async ( ...args : any [ ] ) => {
108
+ activeTasks ++
109
+ try {
110
+ let attempts = 0
111
+ for ( ; ; ) {
112
+ onActivity ( )
113
+ const result = await Promise . race ( [
114
+ ( this . _worker as any ) [ method ] ( ...args ) ,
115
+ restartPromise ,
116
+ ] )
117
+ if ( result !== RESTARTED ) return result
118
+ if ( onRestart ) onRestart ( method , args , ++ attempts )
119
+ }
120
+ } finally {
121
+ activeTasks --
122
+ onActivity ( )
159
123
}
160
-
161
- // Otherwise, we'll need to restart the worker, and try again.
162
- if ( onRestart ) onRestart ( methodName . toString ( ) , args , ++ attempts )
163
124
}
164
- } finally {
165
- activeTasks --
166
- onActivity ( )
167
- }
168
- }
169
-
170
- for ( const name of farmOptions . exposedMethods ) {
171
- if ( name . startsWith ( '_' ) ) continue
172
-
173
- // @ts -expect-error - we're grabbing a dynamic method on the worker
174
- let method = this . _worker [ name ] . bind ( this . _worker )
175
- if ( timeout ) {
176
- method = wrapMethodWithTimeout ( name )
177
- }
178
-
179
- // @ts -expect-error - we're dynamically creating methods
180
- this [ name ] = method
125
+ : ( this . _worker as any ) [ method ] . bind ( this . _worker )
181
126
}
182
127
}
183
128
0 commit comments