@amazon/vinyl-build-utils 3.0.1 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,330 +0,0 @@
1
- /*
2
- * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3
- * SPDX-License-Identifier: Apache-2.0
4
- */
5
- import { TaskQueue } from '../util/TaskQueue';
6
- import { logger } from '../util/Logger';
7
- import path from 'node:path';
8
- import fs from 'node:fs';
9
- import crypto from 'node:crypto';
10
- export class BrowserStackWorker {
11
- deps;
12
- /**
13
- * Set to observe state updates.
14
- */
15
- onUpdate = () => { };
16
- _state;
17
- tasks = new TaskQueue();
18
- pollIntervalId = null;
19
- captureTimeoutId = null;
20
- options;
21
- /**
22
- * The worker url with an appended reportApi query param.
23
- */
24
- url;
25
- constructor(deps, workerOptions, options) {
26
- this.deps = deps;
27
- this.tasks.onError = (error) => this.setError(error);
28
- this.options = {
29
- retries: 2,
30
- captureTimeout: 45,
31
- pollInterval: 5,
32
- logsDir: './bStackLogs',
33
- queryParams: null,
34
- ...options,
35
- };
36
- this._state = {
37
- localId: crypto.randomUUID(),
38
- workerOptions: workerOptions,
39
- error: null,
40
- session: null,
41
- started: false,
42
- active: false,
43
- workerId: null,
44
- sessionId: null,
45
- totalSpecs: null,
46
- totalCompleted: 0,
47
- attemptsRemaining: this.options.retries,
48
- };
49
- // Add the reportApi query param:
50
- const finalUrl = new URL(workerOptions.url);
51
- finalUrl.searchParams.set('reportApi', `/report/${this._state.localId}`);
52
- // Add the provided query params from options.
53
- if (options?.queryParams) {
54
- for (const [name, value] of Object.entries(options.queryParams)) {
55
- finalUrl.searchParams.set(name, value);
56
- }
57
- }
58
- this.url = finalUrl.toString();
59
- if (this.options.logsDir)
60
- fs.mkdirSync(this.options.logsDir, { recursive: true });
61
- }
62
- /**
63
- * Returns true if this worker is completed. This will be true if either in an error state or the session status
64
- * indicates completion
65
- */
66
- get completed() {
67
- const state = this._state;
68
- const status = state.session?.status;
69
- return (state.error != null ||
70
- (state.started &&
71
- status != null &&
72
- ['timeout', 'passed', 'failed'].includes(status)));
73
- }
74
- /**
75
- * Returns this worker's state.
76
- */
77
- get state() {
78
- return this._state;
79
- }
80
- get name() {
81
- return this._state.workerOptions.name;
82
- }
83
- get passed() {
84
- return this._state.session?.status === 'passed';
85
- }
86
- set totalCompleted(value) {
87
- this._state.totalCompleted = value;
88
- }
89
- set totalSpecs(value) {
90
- this._state.totalSpecs = value;
91
- }
92
- done(passed, reason) {
93
- const name = this.name;
94
- if (passed)
95
- logger.info(`✅ ${name} passed`);
96
- else
97
- logger.info(`❌ ${name} failed\n${reason}\n`);
98
- const client = this.deps.client;
99
- return this.tasks
100
- .enqueue(async () => {
101
- const state = this._state;
102
- const { sessionId, workerId } = state;
103
- if (!sessionId || !workerId) {
104
- logger.debug('done aborting - sessionId or workerId is missing');
105
- return;
106
- }
107
- logger.debug('done, passed:', passed);
108
- state.session = await client.updateSession(sessionId, {
109
- status: passed ? 'passed' : 'failed',
110
- reason: reason,
111
- });
112
- this.onUpdate();
113
- })
114
- .finally(() => {
115
- this.terminate().catch(() => { });
116
- });
117
- }
118
- async refreshSession() {
119
- const state = this._state;
120
- if (state.sessionId) {
121
- const previousStatus = state.session?.status;
122
- state.session = await this.deps.client.getSession(state.sessionId);
123
- // A worker terminated by BrowserStack (e.g. the run exceeded the
124
- // worker `timeout`) reports a 'timeout' status. It counts as a
125
- // failure like any other, but is otherwise silent — the reporter
126
- // never sends its `done`, so nothing explains the failed count.
127
- // Log it clearly, once, on the transition.
128
- if (state.session.status === 'timeout' &&
129
- previousStatus !== 'timeout') {
130
- const secs = state.workerOptions.timeout;
131
- logger.error(`⏱️ ${this.name} timed out${secs ? ` after ${secs}s` : ''} — worker terminated by BrowserStack before tests ` +
132
- `completed; counts as a failure`);
133
- }
134
- this.onUpdate();
135
- }
136
- }
137
- appendLogs(logs) {
138
- if (this.options.logsDir == null || logs.length === 0)
139
- return;
140
- const logFilePath = path.join(this.options.logsDir, `${this._state.sessionId}.log`);
141
- // Append logs to a file.
142
- fs.appendFile(logFilePath, logs.map((entry) => entry.join(' ')).join('\n') + '\n', (error) => {
143
- if (error)
144
- logger.error('Could not write to console log file:', error);
145
- });
146
- }
147
- /**
148
- * Creates the worker/session.
149
- */
150
- async start() {
151
- logger.debug(`Starting session '${this.name}'...`);
152
- const state = this._state;
153
- state.started = true;
154
- await this.tasks
155
- .enqueue(async () => {
156
- if (state.active)
157
- return;
158
- state.active = true;
159
- this.onUpdate();
160
- this.pollIntervalId = setInterval(() => {
161
- void this.refreshSession();
162
- }, this.options.pollInterval * 1000);
163
- this.captureTimeoutId = setTimeout(this.captureTimeoutHandler, this.options.captureTimeout * 1000);
164
- const client = this.deps.client;
165
- // fallbackBrowser is a local concern, not a BrowserStack capability.
166
- const { fallbackBrowser: _fallbackBrowser, ...workerOptions } = state.workerOptions;
167
- const newWorker = await client.createWorker({
168
- ...workerOptions,
169
- url: this.url,
170
- });
171
- state.workerId = newWorker.id;
172
- const workerDetails = await client.getWorker(newWorker.id);
173
- logger.info(`Session created for '${this.name}': ${workerDetails.browser_url}`);
174
- state.sessionId = workerDetails.sessionId;
175
- await this.refreshSession();
176
- this.onUpdate();
177
- })
178
- .catch((error) => {
179
- logger.error(`Session start failed '${this.name}'`, error);
180
- state.started = false;
181
- this.clearTimers();
182
- this.onUpdate();
183
- });
184
- }
185
- /**
186
- * Deletes the worker.
187
- * The worker may be created again.
188
- */
189
- terminate() {
190
- logger.debug(`Terminate session '${this.name}'`);
191
- return this.tasks.enqueue(async () => {
192
- const state = this._state;
193
- if (!state.active)
194
- return;
195
- state.active = false;
196
- this.clearTimers();
197
- const workerId = state.workerId;
198
- if (workerId) {
199
- state.workerId = null;
200
- try {
201
- await this.deps.client.deleteWorker(workerId);
202
- logger.debug(`Terminated worker: ${workerId}`);
203
- }
204
- catch (error) {
205
- logger.warn('Could not terminate worker:', error);
206
- }
207
- }
208
- logger.debug(`Terminated session '${this.name}'...`);
209
- this.onUpdate();
210
- });
211
- }
212
- clearTimers() {
213
- if (this.pollIntervalId) {
214
- clearInterval(this.pollIntervalId);
215
- this.pollIntervalId = null;
216
- }
217
- if (this.captureTimeoutId) {
218
- clearTimeout(this.captureTimeoutId);
219
- this.captureTimeoutId = null;
220
- }
221
- }
222
- /**
223
- * Deletes the session if it exists.
224
- */
225
- deleteSession() {
226
- const state = this._state;
227
- logger.debug(`Delete session '${this.name}'`);
228
- return this.tasks.enqueue(async () => {
229
- const sessionId = state.sessionId;
230
- if (sessionId) {
231
- state.sessionId = null;
232
- state.session = null;
233
- try {
234
- await this.deps.client.deleteSession(sessionId);
235
- logger.debug(`Deleted session '${this.name}'`);
236
- }
237
- catch (_) {
238
- logger.warn(`Could not delete session '${this.name}'`);
239
- }
240
- this.onUpdate();
241
- }
242
- });
243
- }
244
- /**
245
- * Terminates the worker, deletes the session, starts a new worker.
246
- */
247
- async restart() {
248
- await this.terminate().catch(() => { });
249
- await this.deleteSession().catch(() => { });
250
- await this.start();
251
- }
252
- /**
253
- * Invoked when the capture timeout has been reached. Checks if data has been received from the REST api.
254
- */
255
- captureTimeoutHandler = () => {
256
- const state = this._state;
257
- if (state.totalSpecs == null) {
258
- if (state.attemptsRemaining > 0) {
259
- state.attemptsRemaining--;
260
- logger.warn(`No test results received for ${this.name}, retrying. Attempts remaining: ${state.attemptsRemaining}`);
261
- this.restart().catch((error) => {
262
- this.setError(error);
263
- });
264
- }
265
- else if (!this.tryFallbackBrowser()) {
266
- this.setError(new Error(`Did not capture test progress after ${this.options.retries + 1} attempts for '${this.name}'.`));
267
- }
268
- }
269
- };
270
- /**
271
- * If a fallback browser is configured, swaps the worker to it — keeping the
272
- * rest of the worker options — resets the retry budget, and restarts.
273
- * Returns true if a fallback was available and a restart was kicked off.
274
- */
275
- tryFallbackBrowser() {
276
- const state = this._state;
277
- const { fallbackBrowser } = state.workerOptions;
278
- if (!fallbackBrowser)
279
- return false;
280
- const target = [
281
- fallbackBrowser.device,
282
- fallbackBrowser.os,
283
- fallbackBrowser.os_version,
284
- fallbackBrowser.browser,
285
- fallbackBrowser.browser_version,
286
- ]
287
- .filter(Boolean)
288
- .join(' ');
289
- logger.warn(`Did not capture test progress for '${this.name}'; falling back to '${target}'.`);
290
- // Drop the previous combination's browser fields so a device or
291
- // browser_version the fallback omits can't bleed through, then apply
292
- // the fallback over the rest of the options. fallbackBrowser itself is
293
- // dropped so a repeat failure fails outright rather than looping. The
294
- // name is relabeled to the fallback platform so its logs and the
295
- // BrowserStack session reflect the browser actually running.
296
- const { fallbackBrowser: _fallbackBrowser, browser: _browser, browser_version: _browserVersion, device: _device, ...rest } = state.workerOptions;
297
- state.workerOptions = {
298
- ...rest,
299
- ...fallbackBrowser,
300
- name: `${target} [fallback]`,
301
- };
302
- state.attemptsRemaining = this.options.retries;
303
- this.restart().catch((error) => this.setError(error));
304
- return true;
305
- }
306
- /**
307
- * Sets the worker to a failed state and terminates.
308
- */
309
- setError(error) {
310
- const state = this._state;
311
- if (state.error)
312
- return; // Already in an erred state.
313
- state.error = error;
314
- logger.error(error == null
315
- ? 'An internal error has occurred.'
316
- : (error.message ?? String(error)));
317
- this.tasks
318
- .enqueue(async () => {
319
- if (state.sessionId) {
320
- state.session = await this.deps.client.updateSession(state.sessionId, {
321
- status: 'failed',
322
- reason: error?.message ?? 'Internal error',
323
- });
324
- this.onUpdate();
325
- }
326
- })
327
- .catch(() => { });
328
- this.terminate().catch(() => { });
329
- }
330
- }
@@ -1,103 +0,0 @@
1
- import type { BrowserStackWorkerOptions, ReadonlyBrowserStackWorkerState } from './BrowserStackWorker';
2
- import type { BrowserStackAutomateClient, WorkerOptions } from './browserStackRestApi';
3
- import type { DoneRequest, ProgressRequest, ReportingListener, StartedRequest } from './configureReporterApi';
4
- export interface BrowserStackWorkerControllerDeps {
5
- /**
6
- * The BrowserStack REST API client.
7
- */
8
- readonly client: BrowserStackAutomateClient;
9
- }
10
- export interface BrowserStackWorkerControllerOptions {
11
- /**
12
- * Configuration to provide to each Worker.
13
- */
14
- readonly worker?: BrowserStackWorkerOptions;
15
- /**
16
- * The maximum number of sessions to run in parallel.
17
- * If not set, the maximum number of sessions according to the automate plan will be used.
18
- */
19
- readonly maxSessions?: number;
20
- }
21
- export interface BrowserStackWorkerControllerState {
22
- readonly workers: readonly ReadonlyBrowserStackWorkerState[];
23
- /**
24
- * The number of workers not yet started.
25
- */
26
- pending: number;
27
- /**
28
- * The number of workers currently active.
29
- */
30
- running: number;
31
- /**
32
- * The number of workers completed and passed.
33
- */
34
- passed: number;
35
- /**
36
- * The number of workers completed and failed.
37
- */
38
- failed: number;
39
- /**
40
- * The estimated percentage complete, a number from 0-1.
41
- */
42
- percentComplete: number;
43
- /**
44
- * The total number of workers not yet completed.
45
- */
46
- remaining: number;
47
- /**
48
- * The total number of workers.
49
- */
50
- readonly total: number;
51
- }
52
- export type ReadonlyBrowserStackWorkerControllerState = Readonly<BrowserStackWorkerControllerState>;
53
- export declare class BrowserStackWorkerController implements ReportingListener {
54
- private readonly deps;
55
- private readonly options?;
56
- /**
57
- * Set to observe state updates.
58
- */
59
- onUpdate: () => void;
60
- private readonly _state;
61
- private readonly workers;
62
- /**
63
- * Maps the worker's local id to the worker.
64
- */
65
- private readonly localIdToWorkerMap;
66
- private sessionPollIntervalId;
67
- /**
68
- * Tracks an in-flight startSessions invocation. While set, additional calls
69
- * are deferred until the current one settles to avoid redundant API calls
70
- * and racing state updates.
71
- */
72
- private startSessionsInFlight;
73
- /**
74
- * If set during an in-flight startSessions, indicates another invocation
75
- * was requested and should run after the current one settles.
76
- */
77
- private startSessionsRequested;
78
- constructor(deps: BrowserStackWorkerControllerDeps, browsers: readonly WorkerOptions[], options?: BrowserStackWorkerControllerOptions | undefined);
79
- get state(): ReadonlyBrowserStackWorkerControllerState;
80
- shouldHandleRequest(id: string): boolean;
81
- onStarted(request: StartedRequest): void;
82
- onProgress(request: ProgressRequest): void;
83
- onDone(request: DoneRequest): void;
84
- private refreshState;
85
- /**
86
- * Refreshes the current state and invokes the onUpdate callback.
87
- */
88
- private updateHandler;
89
- /**
90
- * Begins new workers until the maximum parallel sessions is reached.
91
- * Reentrancy-safe: if invoked while a previous invocation is still
92
- * running, schedules a single follow-up to run after it settles.
93
- */
94
- private startSessions;
95
- private clearOpenSessionPollTimeout;
96
- get allPassed(): boolean;
97
- start(): void;
98
- /**
99
- * Terminates all running workers.
100
- */
101
- terminate(): Promise<void>;
102
- }
103
- //# sourceMappingURL=BrowserStackWorkerController.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"BrowserStackWorkerController.d.ts","sourceRoot":"","sources":["../../src/browserstack/BrowserStackWorkerController.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACR,yBAAyB,EACzB,+BAA+B,EAClC,MAAM,sBAAsB,CAAA;AAE7B,OAAO,KAAK,EACR,0BAA0B,EAC1B,aAAa,EAChB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,KAAK,EACR,WAAW,EACX,eAAe,EACf,iBAAiB,EACjB,cAAc,EACjB,MAAM,wBAAwB,CAAA;AAG/B,MAAM,WAAW,gCAAgC;IAC7C;;OAEG;IACH,QAAQ,CAAC,MAAM,EAAE,0BAA0B,CAAA;CAC9C;AAED,MAAM,WAAW,mCAAmC;IAChD;;OAEG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,yBAAyB,CAAA;IAE3C;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAChC;AAED,MAAM,WAAW,iCAAiC;IAC9C,QAAQ,CAAC,OAAO,EAAE,SAAS,+BAA+B,EAAE,CAAA;IAE5D;;OAEG;IACH,OAAO,EAAE,MAAM,CAAA;IAEf;;OAEG;IACH,OAAO,EAAE,MAAM,CAAA;IAEf;;OAEG;IACH,MAAM,EAAE,MAAM,CAAA;IAEd;;OAEG;IACH,MAAM,EAAE,MAAM,CAAA;IAEd;;OAEG;IACH,eAAe,EAAE,MAAM,CAAA;IAEvB;;OAEG;IACH,SAAS,EAAE,MAAM,CAAA;IAEjB;;OAEG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CACzB;AAED,MAAM,MAAM,yCAAyC,GACjD,QAAQ,CAAC,iCAAiC,CAAC,CAAA;AAI/C,qBAAa,4BAA6B,YAAW,iBAAiB;IA8B9D,OAAO,CAAC,QAAQ,CAAC,IAAI;IAErB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;IA/B7B;;OAEG;IACI,QAAQ,EAAE,MAAM,IAAI,CAAW;IAEtC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmC;IAC1D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsB;IAE9C;;OAEG;IACH,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAiC;IAEpE,OAAO,CAAC,qBAAqB,CAAuB;IAEpD;;;;OAIG;IACH,OAAO,CAAC,qBAAqB,CAA6B;IAE1D;;;OAGG;IACH,OAAO,CAAC,sBAAsB,CAAQ;gBAGjB,IAAI,EAAE,gCAAgC,EACvD,QAAQ,EAAE,SAAS,aAAa,EAAE,EACjB,OAAO,CAAC,EAAE,mCAAmC,YAAA;IA2BlE,IAAI,KAAK,IAAI,yCAAyC,CAErD;IAED,mBAAmB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAIxC,SAAS,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI;IAKxC,UAAU,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI;IAM1C,MAAM,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI;IAQlC,OAAO,CAAC,YAAY;IA8BpB;;OAEG;IACH,OAAO,CAAC,aAAa,CAMpB;IAED;;;;OAIG;IACH,OAAO,CAAC,aAAa;IAwErB,OAAO,CAAC,2BAA2B;IAOnC,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,KAAK,IAAI,IAAI;IAKb;;OAEG;IACG,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;CAenC"}
@@ -1,199 +0,0 @@
1
- /*
2
- * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3
- * SPDX-License-Identifier: Apache-2.0
4
- */
5
- import { BrowserStackWorker } from './BrowserStackWorker';
6
- import { logger } from '../util/Logger';
7
- export class BrowserStackWorkerController {
8
- deps;
9
- options;
10
- /**
11
- * Set to observe state updates.
12
- */
13
- onUpdate = () => { };
14
- _state;
15
- workers;
16
- /**
17
- * Maps the worker's local id to the worker.
18
- */
19
- localIdToWorkerMap;
20
- sessionPollIntervalId = null;
21
- /**
22
- * Tracks an in-flight startSessions invocation. While set, additional calls
23
- * are deferred until the current one settles to avoid redundant API calls
24
- * and racing state updates.
25
- */
26
- startSessionsInFlight = null;
27
- /**
28
- * If set during an in-flight startSessions, indicates another invocation
29
- * was requested and should run after the current one settles.
30
- */
31
- startSessionsRequested = false;
32
- constructor(deps, browsers, options) {
33
- this.deps = deps;
34
- this.options = options;
35
- this.workers = browsers.map((browser) => new BrowserStackWorker(deps, browser, options?.worker));
36
- this._state = {
37
- workers: this.workers.map((worker) => worker.state),
38
- pending: this.workers.length,
39
- remaining: this.workers.length,
40
- running: 0,
41
- failed: 0,
42
- passed: 0,
43
- percentComplete: 0,
44
- total: this.workers.length,
45
- };
46
- this.localIdToWorkerMap = new Map(this.workers.map((worker) => {
47
- return [worker.state.localId, worker];
48
- }));
49
- // Observe state on all workers
50
- for (const worker of this.workers) {
51
- worker.onUpdate = this.updateHandler;
52
- }
53
- }
54
- get state() {
55
- return this._state;
56
- }
57
- shouldHandleRequest(id) {
58
- return this.localIdToWorkerMap.has(id);
59
- }
60
- onStarted(request) {
61
- const worker = this.localIdToWorkerMap.get(request.id);
62
- worker.totalSpecs = request.totalSpecs;
63
- }
64
- onProgress(request) {
65
- const worker = this.localIdToWorkerMap.get(request.id);
66
- worker.totalCompleted = request.totalCompleted;
67
- worker.appendLogs(request.logs);
68
- }
69
- onDone(request) {
70
- const worker = this.localIdToWorkerMap.get(request.id);
71
- void worker.done(request.overallStatus === 'passed', request.reason ?? '');
72
- }
73
- refreshState() {
74
- const state = this._state;
75
- let running = 0;
76
- let pending = 0;
77
- let failed = 0;
78
- let passed = 0;
79
- let percentSum = 0;
80
- for (const worker of this.workers) {
81
- const completed = worker.completed;
82
- if (completed) {
83
- if (worker.passed)
84
- passed++;
85
- else
86
- failed++;
87
- percentSum++;
88
- }
89
- else {
90
- if (worker.state.started)
91
- running++;
92
- else
93
- pending++;
94
- percentSum +=
95
- worker.state.totalCompleted / (worker.state.totalSpecs || 1);
96
- }
97
- }
98
- state.running = running;
99
- state.pending = pending;
100
- state.failed = failed;
101
- state.passed = passed;
102
- state.remaining = running + pending;
103
- state.percentComplete = percentSum / (state.total || 1);
104
- }
105
- /**
106
- * Refreshes the current state and invokes the onUpdate callback.
107
- */
108
- updateHandler = () => {
109
- const state = this._state;
110
- const previousRunning = state.running;
111
- this.refreshState();
112
- if (previousRunning !== state.running)
113
- this.startSessions();
114
- this.onUpdate();
115
- };
116
- /**
117
- * Begins new workers until the maximum parallel sessions is reached.
118
- * Reentrancy-safe: if invoked while a previous invocation is still
119
- * running, schedules a single follow-up to run after it settles.
120
- */
121
- startSessions() {
122
- if (this.startSessionsInFlight) {
123
- this.startSessionsRequested = true;
124
- return;
125
- }
126
- const run = (async () => {
127
- const state = this._state;
128
- if (state.pending === 0)
129
- return;
130
- const client = this.deps.client;
131
- const planDetails = await client.getPlan();
132
- const remainingOpenSessions = Math.min(this.options?.maxSessions ?? Number.MAX_SAFE_INTEGER, planDetails.parallel_sessions_max_allowed) -
133
- Math.max(state.running, planDetails.parallel_sessions_running);
134
- logger.debug('Plan parallel sessions:', `${planDetails.parallel_sessions_running} running, ${planDetails.parallel_sessions_max_allowed} allowed`);
135
- // Takes up to [remainingOpenSessions] pending workers, starts them in parallel, and awaits their startup.
136
- const workersToStart = this.workers
137
- .filter((worker) => !worker.state.started)
138
- .slice(0, remainingOpenSessions);
139
- if (workersToStart.length) {
140
- logger.debug(`Starting ${workersToStart.length} sessions...`);
141
- // Start all workers in parallel
142
- await Promise.all(workersToStart.map((worker) => worker.start().catch(() => { })));
143
- }
144
- this.refreshState();
145
- if (this._state.pending > 0) {
146
- // There are pending sessions that were not started, either due
147
- // no open sessions or a worker start failure.
148
- // Check if a session opens up after a timeout.
149
- logger.debug('No open sessions, checking again in 15s.');
150
- this.clearOpenSessionPollTimeout();
151
- this.sessionPollIntervalId = setTimeout(() => this.startSessions(), 15000);
152
- }
153
- })()
154
- .catch((error) => {
155
- logger.warn('startSessions failed', error);
156
- // Schedule a retry rather than abandoning all pending workers;
157
- // a transient API failure should not permanently strand them.
158
- this.clearOpenSessionPollTimeout();
159
- this.sessionPollIntervalId = setTimeout(() => this.startSessions(), 15000);
160
- })
161
- .finally(() => {
162
- this.startSessionsInFlight = null;
163
- if (this.startSessionsRequested) {
164
- this.startSessionsRequested = false;
165
- // A coalesced request supersedes any retry scheduled by
166
- // .catch(); otherwise both the immediate run here and the
167
- // delayed timeout below would fire startSessions().
168
- this.clearOpenSessionPollTimeout();
169
- this.startSessions();
170
- }
171
- });
172
- this.startSessionsInFlight = run;
173
- }
174
- clearOpenSessionPollTimeout() {
175
- if (this.sessionPollIntervalId != null) {
176
- clearTimeout(this.sessionPollIntervalId);
177
- this.sessionPollIntervalId = null;
178
- }
179
- }
180
- get allPassed() {
181
- return this.workers.every((worker) => worker.passed);
182
- }
183
- start() {
184
- logger.debug(`BrowserStackWorkerController start`);
185
- this.startSessions();
186
- }
187
- /**
188
- * Terminates all running workers.
189
- */
190
- async terminate() {
191
- logger.debug(`Terminating workers`);
192
- this.clearOpenSessionPollTimeout();
193
- await Promise.all(this.workers.map((worker) => worker.terminate().catch(() => {
194
- logger.warn(`Could not terminate worker: '${worker.state.workerOptions.name}'`);
195
- }))).finally(() => {
196
- logger.debug('terminate done');
197
- });
198
- }
199
- }