@aguacerowx/mapsgl 0.0.57 → 0.0.58

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.
package/src/WorkerPool.js CHANGED
@@ -1,340 +1,340 @@
1
- export default class WorkerPool {
2
- /**
3
- * @param {typeof Worker | (() => Worker)} workerCtorOrFactory - A `class extends Worker`, or a zero-arg
4
- * factory. Prefer a factory that contains `new Worker(new URL('./worker.js', import.meta.url))` so Vite
5
- * can bundle workers in dev; a `class extends Worker { constructor() { super(url) } }` breaks that rewrite.
6
- */
7
- constructor(workerCtorOrFactory, poolSize = Math.min(8, Math.max(2, Math.floor(navigator.hardwareConcurrency * 0.75))), options = {}) {
8
- if (typeof workerCtorOrFactory === 'function' && workerCtorOrFactory.prototype instanceof Worker) {
9
- this._newWorker = () => new workerCtorOrFactory();
10
- } else {
11
- this._newWorker = () => workerCtorOrFactory();
12
- }
13
- this.poolSize = poolSize;
14
- this.workers = [];
15
- this.availableWorkers = [];
16
- this.taskQueue = [];
17
- this.isInitialized = false;
18
-
19
- /** Called for every new Worker (including replacements) so satellite INIT_WORKER is never skipped. */
20
- this.workerBootstrapFn = options.workerBootstrap || null;
21
-
22
- this.debug = Boolean(options.debug);
23
- this.enablePerfMarks = Boolean(options.enablePerfMarks);
24
- }
25
-
26
- setWorkerBootstrap(fn) {
27
- this.workerBootstrapFn = typeof fn === 'function' ? fn : null;
28
- }
29
-
30
- _log(...args) { if (this.debug) console.log(...args); }
31
- _warn(...args) { if (this.debug) console.warn(...args); }
32
- _error(...args) { if (this.debug) console.error(...args); }
33
-
34
- _mark(name) {
35
- if (!this.enablePerfMarks || typeof performance === 'undefined' || typeof performance.mark !== 'function') return;
36
- performance.mark(name);
37
- }
38
-
39
- _measure(name, startMark, endMark) {
40
- if (!this.enablePerfMarks || typeof performance === 'undefined' || typeof performance.measure !== 'function') return;
41
- try {
42
- performance.measure(name, startMark, endMark);
43
- if (typeof performance.clearMeasures === 'function') {
44
- performance.clearMeasures(name);
45
- }
46
- } catch (error) {
47
- this._warn(`[Pool] Skipping perf measure "${name}"`, error);
48
- }
49
- }
50
-
51
- _clearTaskMarks(taskId) {
52
- if (!this.enablePerfMarks || typeof performance === 'undefined' || typeof performance.clearMarks !== 'function') return;
53
- const marks = [
54
- `Task-Queued-${taskId}`,
55
- `Task-Sent-${taskId}`,
56
- `Task-Received-${taskId}`,
57
- `Main-Resolve-Start-${taskId}`,
58
- `Main-Resolve-End-${taskId}`
59
- ];
60
- marks.forEach((mark) => {
61
- try {
62
- performance.clearMarks(mark);
63
- } catch (error) {
64
- this._warn(`[Pool] Failed to clear mark "${mark}"`, error);
65
- }
66
- });
67
- }
68
-
69
- async initialize() {
70
- if (this.isInitialized) return;
71
- try {
72
- for (let i = 0; i < this.poolSize; i++) {
73
- this._createWorker(i);
74
- }
75
- this.isInitialized = true;
76
- } catch (error) {
77
- this._error('❌ Failed to initialize worker pool:', error);
78
- throw error;
79
- }
80
- }
81
-
82
- _createWorker(id) {
83
- const worker = this._newWorker();
84
- const workerInstance = { id: id, worker: worker, busy: false, currentTask: null };
85
-
86
- worker.onmessage = (e) => this.handleWorkerMessage(workerInstance, e);
87
- worker.onerror = (error) => {
88
- this._error(`❌ Worker ${id} error:`, error);
89
- this.handleWorkerError(workerInstance, error);
90
- };
91
-
92
- if (this.workerBootstrapFn) {
93
- try {
94
- this.workerBootstrapFn(worker);
95
- } catch (error) {
96
- this._error(`❌ workerBootstrap failed for worker ${id}:`, error);
97
- }
98
- }
99
-
100
- this.workers.push(workerInstance);
101
- this.availableWorkers.push(workerInstance);
102
- return workerInstance;
103
- }
104
-
105
- async broadcast(message) {
106
- if (!this.isInitialized) await this.initialize();
107
- this.workers.forEach(w => w.worker.postMessage(message));
108
- }
109
-
110
- async execute(taskData, options = {}) {
111
- if (!this.isInitialized) await this.initialize();
112
- const { signal, priority = 0, transfer = [] } = options;
113
-
114
- return new Promise((resolve, reject) => {
115
- if (signal?.aborted) return reject(new DOMException('Operation aborted', 'AbortError'));
116
-
117
- const now = performance.now();
118
- const taskId = `${taskData.type}_${Math.floor(now)}_${Math.random().toString(36).substr(2, 5)}`;
119
-
120
- this._mark(`Task-Queued-${taskId}`);
121
-
122
- const task = {
123
- id: taskId,
124
- taskType: taskData.type,
125
- data: taskData, transfer, resolve, reject, signal, priority,
126
- timeout: options.timeout || 30000,
127
- retries: options.retries || 0, maxRetries: options.maxRetries || 2,
128
- createdAt: now, startedAt: 0, enqueuedAt: now, isAborted: false
129
- };
130
-
131
- const abortHandler = () => {
132
- task.isAborted = true;
133
- const taskIndexInQueue = this.taskQueue.findIndex(t => t.id === task.id);
134
- if (taskIndexInQueue !== -1) {
135
- this.taskQueue.splice(taskIndexInQueue, 1);
136
- task.reject(new DOMException('Operation aborted', 'AbortError'));
137
- }
138
- };
139
-
140
- task.abortHandler = abortHandler;
141
- signal?.addEventListener('abort', abortHandler, { once: true });
142
-
143
- this.taskQueue.push(task);
144
- this.processQueue();
145
- });
146
- }
147
-
148
- processQueue() {
149
- for (let i = this.taskQueue.length - 1; i >= 0; i--) {
150
- if (this.taskQueue[i].signal?.aborted) {
151
- this.taskQueue[i].reject(new DOMException('Operation aborted', 'AbortError'));
152
- this.taskQueue.splice(i, 1);
153
- }
154
- }
155
-
156
- this.taskQueue.sort((a, b) => {
157
- if (b.priority !== a.priority) return b.priority - a.priority;
158
- return b.enqueuedAt - a.enqueuedAt;
159
- });
160
-
161
- while (this.taskQueue.length > 0 && this.availableWorkers.length > 0) {
162
- const task = this.taskQueue.shift();
163
- const workerInstance = this.availableWorkers.shift();
164
- this.assignTaskToWorker(workerInstance, task);
165
- }
166
- }
167
-
168
- assignTaskToWorker(workerInstance, task) {
169
- if (task.signal?.aborted) {
170
- task.reject(new DOMException('Operation aborted', 'AbortError'));
171
- this.availableWorkers.push(workerInstance);
172
- this.processQueue();
173
- return;
174
- }
175
-
176
- workerInstance.busy = true;
177
- workerInstance.currentTask = task;
178
- task.startedAt = performance.now();
179
-
180
- this._mark(`Task-Sent-${task.id}`);
181
- this._measure(`QueueTime:${task.taskType}`, `Task-Queued-${task.id}`, `Task-Sent-${task.id}`);
182
-
183
- task.timeoutId = setTimeout(() => {
184
- this._error(`[Pool] ⏰ Task ${task.id} TIMED OUT on Worker ${workerInstance.id}`);
185
- this.handleTaskTimeout(workerInstance, task);
186
- }, task.timeout);
187
-
188
- try {
189
- const messageWithId = { ...task.data, __taskId: task.id };
190
- workerInstance.worker.postMessage(messageWithId, task.transfer || []);
191
- task.data = null;
192
- task.transfer = null;
193
- } catch (error) {
194
- this._error(`❌ Failed to send task to worker ${workerInstance.id}:`, error);
195
- this.handleWorkerError(workerInstance, error);
196
- }
197
- }
198
-
199
- handleWorkerMessage(workerInstance, event) {
200
- if (event?.data?.type === 'WGPU_INIT_FAILED') {
201
- const error = new Error(event.data.error || 'WGPU initialization failed');
202
- const task = workerInstance.currentTask;
203
- if (task) {
204
- this.handleTaskFailure(workerInstance, task, error);
205
- }
206
- this._replaceWorker(workerInstance);
207
- return;
208
- }
209
-
210
- const responseTaskId = event.data.__taskId;
211
- const task = (workerInstance.currentTask && workerInstance.currentTask.id === responseTaskId)
212
- ? workerInstance.currentTask : null;
213
-
214
- if (!task) {
215
- this._warn(`[Pool] ⚠️ Stale response for task ${responseTaskId}`);
216
- return;
217
- }
218
-
219
- this._mark(`Task-Received-${task.id}`);
220
- this._measure(`WorkerProcess:${task.taskType}`, `Task-Sent-${task.id}`, `Task-Received-${task.id}`);
221
-
222
- if (task.timeoutId) clearTimeout(task.timeoutId);
223
- task.signal?.removeEventListener('abort', task.abortHandler);
224
- let shouldReplaceWorker = false;
225
-
226
- if (!task.isAborted) {
227
- this._mark(`Main-Resolve-Start-${task.id}`);
228
-
229
- if (event.data.type === 'PROCESSING_ERROR' || event.data.type === 'VIEWPORT_PROCESSING_ERROR') {
230
- task.reject(new Error(event.data.error));
231
- if (event.data.wgpuRuntimeFailure) {
232
- shouldReplaceWorker = true;
233
- }
234
- } else if (event.data.type === 'WORKER_ERROR') {
235
- task.reject(new Error(event.data.error || 'Worker error'));
236
- } else {
237
- task.resolve(event.data);
238
- }
239
-
240
- this._mark(`Main-Resolve-End-${task.id}`);
241
- this._measure(`PoolResolveTrigger:${task.taskType}`, `Main-Resolve-Start-${task.id}`, `Main-Resolve-End-${task.id}`);
242
- }
243
-
244
- this._clearTaskMarks(task.id);
245
- if (shouldReplaceWorker) {
246
- this._replaceWorker(workerInstance);
247
- return;
248
- }
249
- this.releaseWorker(workerInstance);
250
- }
251
-
252
- handleWorkerError(workerInstance, error) {
253
- const task = workerInstance.currentTask;
254
- if (task) {
255
- this.handleTaskFailure(workerInstance, task, error);
256
- }
257
- this._replaceWorker(workerInstance);
258
- }
259
-
260
- handleTaskTimeout(workerInstance, task) {
261
- this._warn(`[Pool] 💀 Terminating stuck Worker ${workerInstance.id}`);
262
- workerInstance.worker.terminate();
263
- this.handleTaskFailure(workerInstance, task, new Error('Task timeout'));
264
- this._replaceWorker(workerInstance);
265
- }
266
-
267
- handleTaskFailure(workerInstance, task, error) {
268
- if (task.timeoutId) clearTimeout(task.timeoutId);
269
- task.signal?.removeEventListener('abort', task.abortHandler);
270
-
271
- const canRetry = task.data !== null;
272
-
273
- if (canRetry && task.retries < task.maxRetries && error.name !== 'AbortError' && !task.isAborted) {
274
- task.retries++;
275
- delete task.timeoutId;
276
- task.enqueuedAt = performance.now();
277
- this.taskQueue.push(task);
278
- this.releaseWorker(workerInstance);
279
- } else {
280
- if (!task.isAborted) {
281
- task.reject(error);
282
- }
283
-
284
- this._clearTaskMarks(task.id);
285
- if (this.workers.includes(workerInstance)) {
286
- this.releaseWorker(workerInstance);
287
- }
288
- }
289
- }
290
-
291
- releaseWorker(workerInstance) {
292
- workerInstance.busy = false;
293
- workerInstance.currentTask = null;
294
- this.availableWorkers.push(workerInstance);
295
- this.processQueue();
296
- }
297
-
298
- _replaceWorker(oldWorkerInstance) {
299
- try {
300
- oldWorkerInstance?.worker?.terminate?.();
301
- } catch (error) {
302
- this._warn(`[Pool] Failed to terminate worker ${oldWorkerInstance?.id}`, error);
303
- }
304
-
305
- const idx = this.workers.findIndex(w => w.id === oldWorkerInstance.id);
306
- if (idx !== -1) this.workers.splice(idx, 1);
307
-
308
- const availIdx = this.availableWorkers.findIndex(w => w.id === oldWorkerInstance.id);
309
- if (availIdx !== -1) this.availableWorkers.splice(availIdx, 1);
310
-
311
- this._createWorker(oldWorkerInstance.id);
312
- this.processQueue();
313
- }
314
-
315
- terminate() {
316
- for (const t of this.taskQueue) {
317
- if (!t.isAborted) t.reject(new Error('Pool terminated'));
318
- }
319
- this.taskQueue = [];
320
-
321
- for (const w of this.workers) {
322
- const task = w.currentTask;
323
- if (task && !task.isAborted) {
324
- if (task.timeoutId) clearTimeout(task.timeoutId);
325
- task.signal?.removeEventListener('abort', task.abortHandler);
326
- task.reject(new Error('Pool terminated'));
327
- this._clearTaskMarks(task.id);
328
- }
329
- try {
330
- w.worker.terminate();
331
- } catch {
332
- /* ignore */
333
- }
334
- }
335
-
336
- this.workers = [];
337
- this.availableWorkers = [];
338
- this.isInitialized = false;
339
- }
340
- }
1
+ export default class WorkerPool {
2
+ /**
3
+ * @param {typeof Worker | (() => Worker)} workerCtorOrFactory - A `class extends Worker`, or a zero-arg
4
+ * factory. Prefer a factory that contains `new Worker(new URL('./worker.js', import.meta.url))` so Vite
5
+ * can bundle workers in dev; a `class extends Worker { constructor() { super(url) } }` breaks that rewrite.
6
+ */
7
+ constructor(workerCtorOrFactory, poolSize = Math.min(8, Math.max(2, Math.floor(navigator.hardwareConcurrency * 0.75))), options = {}) {
8
+ if (typeof workerCtorOrFactory === 'function' && workerCtorOrFactory.prototype instanceof Worker) {
9
+ this._newWorker = () => new workerCtorOrFactory();
10
+ } else {
11
+ this._newWorker = () => workerCtorOrFactory();
12
+ }
13
+ this.poolSize = poolSize;
14
+ this.workers = [];
15
+ this.availableWorkers = [];
16
+ this.taskQueue = [];
17
+ this.isInitialized = false;
18
+
19
+ /** Called for every new Worker (including replacements) so satellite INIT_WORKER is never skipped. */
20
+ this.workerBootstrapFn = options.workerBootstrap || null;
21
+
22
+ this.debug = Boolean(options.debug);
23
+ this.enablePerfMarks = Boolean(options.enablePerfMarks);
24
+ }
25
+
26
+ setWorkerBootstrap(fn) {
27
+ this.workerBootstrapFn = typeof fn === 'function' ? fn : null;
28
+ }
29
+
30
+ _log(...args) { if (this.debug) console.log(...args); }
31
+ _warn(...args) { if (this.debug) console.warn(...args); }
32
+ _error(...args) { if (this.debug) console.error(...args); }
33
+
34
+ _mark(name) {
35
+ if (!this.enablePerfMarks || typeof performance === 'undefined' || typeof performance.mark !== 'function') return;
36
+ performance.mark(name);
37
+ }
38
+
39
+ _measure(name, startMark, endMark) {
40
+ if (!this.enablePerfMarks || typeof performance === 'undefined' || typeof performance.measure !== 'function') return;
41
+ try {
42
+ performance.measure(name, startMark, endMark);
43
+ if (typeof performance.clearMeasures === 'function') {
44
+ performance.clearMeasures(name);
45
+ }
46
+ } catch (error) {
47
+ this._warn(`[Pool] Skipping perf measure "${name}"`, error);
48
+ }
49
+ }
50
+
51
+ _clearTaskMarks(taskId) {
52
+ if (!this.enablePerfMarks || typeof performance === 'undefined' || typeof performance.clearMarks !== 'function') return;
53
+ const marks = [
54
+ `Task-Queued-${taskId}`,
55
+ `Task-Sent-${taskId}`,
56
+ `Task-Received-${taskId}`,
57
+ `Main-Resolve-Start-${taskId}`,
58
+ `Main-Resolve-End-${taskId}`
59
+ ];
60
+ marks.forEach((mark) => {
61
+ try {
62
+ performance.clearMarks(mark);
63
+ } catch (error) {
64
+ this._warn(`[Pool] Failed to clear mark "${mark}"`, error);
65
+ }
66
+ });
67
+ }
68
+
69
+ async initialize() {
70
+ if (this.isInitialized) return;
71
+ try {
72
+ for (let i = 0; i < this.poolSize; i++) {
73
+ this._createWorker(i);
74
+ }
75
+ this.isInitialized = true;
76
+ } catch (error) {
77
+ this._error('❌ Failed to initialize worker pool:', error);
78
+ throw error;
79
+ }
80
+ }
81
+
82
+ _createWorker(id) {
83
+ const worker = this._newWorker();
84
+ const workerInstance = { id: id, worker: worker, busy: false, currentTask: null };
85
+
86
+ worker.onmessage = (e) => this.handleWorkerMessage(workerInstance, e);
87
+ worker.onerror = (error) => {
88
+ this._error(`❌ Worker ${id} error:`, error);
89
+ this.handleWorkerError(workerInstance, error);
90
+ };
91
+
92
+ if (this.workerBootstrapFn) {
93
+ try {
94
+ this.workerBootstrapFn(worker);
95
+ } catch (error) {
96
+ this._error(`❌ workerBootstrap failed for worker ${id}:`, error);
97
+ }
98
+ }
99
+
100
+ this.workers.push(workerInstance);
101
+ this.availableWorkers.push(workerInstance);
102
+ return workerInstance;
103
+ }
104
+
105
+ async broadcast(message) {
106
+ if (!this.isInitialized) await this.initialize();
107
+ this.workers.forEach(w => w.worker.postMessage(message));
108
+ }
109
+
110
+ async execute(taskData, options = {}) {
111
+ if (!this.isInitialized) await this.initialize();
112
+ const { signal, priority = 0, transfer = [] } = options;
113
+
114
+ return new Promise((resolve, reject) => {
115
+ if (signal?.aborted) return reject(new DOMException('Operation aborted', 'AbortError'));
116
+
117
+ const now = performance.now();
118
+ const taskId = `${taskData.type}_${Math.floor(now)}_${Math.random().toString(36).substr(2, 5)}`;
119
+
120
+ this._mark(`Task-Queued-${taskId}`);
121
+
122
+ const task = {
123
+ id: taskId,
124
+ taskType: taskData.type,
125
+ data: taskData, transfer, resolve, reject, signal, priority,
126
+ timeout: options.timeout || 30000,
127
+ retries: options.retries || 0, maxRetries: options.maxRetries || 2,
128
+ createdAt: now, startedAt: 0, enqueuedAt: now, isAborted: false
129
+ };
130
+
131
+ const abortHandler = () => {
132
+ task.isAborted = true;
133
+ const taskIndexInQueue = this.taskQueue.findIndex(t => t.id === task.id);
134
+ if (taskIndexInQueue !== -1) {
135
+ this.taskQueue.splice(taskIndexInQueue, 1);
136
+ task.reject(new DOMException('Operation aborted', 'AbortError'));
137
+ }
138
+ };
139
+
140
+ task.abortHandler = abortHandler;
141
+ signal?.addEventListener('abort', abortHandler, { once: true });
142
+
143
+ this.taskQueue.push(task);
144
+ this.processQueue();
145
+ });
146
+ }
147
+
148
+ processQueue() {
149
+ for (let i = this.taskQueue.length - 1; i >= 0; i--) {
150
+ if (this.taskQueue[i].signal?.aborted) {
151
+ this.taskQueue[i].reject(new DOMException('Operation aborted', 'AbortError'));
152
+ this.taskQueue.splice(i, 1);
153
+ }
154
+ }
155
+
156
+ this.taskQueue.sort((a, b) => {
157
+ if (b.priority !== a.priority) return b.priority - a.priority;
158
+ return b.enqueuedAt - a.enqueuedAt;
159
+ });
160
+
161
+ while (this.taskQueue.length > 0 && this.availableWorkers.length > 0) {
162
+ const task = this.taskQueue.shift();
163
+ const workerInstance = this.availableWorkers.shift();
164
+ this.assignTaskToWorker(workerInstance, task);
165
+ }
166
+ }
167
+
168
+ assignTaskToWorker(workerInstance, task) {
169
+ if (task.signal?.aborted) {
170
+ task.reject(new DOMException('Operation aborted', 'AbortError'));
171
+ this.availableWorkers.push(workerInstance);
172
+ this.processQueue();
173
+ return;
174
+ }
175
+
176
+ workerInstance.busy = true;
177
+ workerInstance.currentTask = task;
178
+ task.startedAt = performance.now();
179
+
180
+ this._mark(`Task-Sent-${task.id}`);
181
+ this._measure(`QueueTime:${task.taskType}`, `Task-Queued-${task.id}`, `Task-Sent-${task.id}`);
182
+
183
+ task.timeoutId = setTimeout(() => {
184
+ this._error(`[Pool] ⏰ Task ${task.id} TIMED OUT on Worker ${workerInstance.id}`);
185
+ this.handleTaskTimeout(workerInstance, task);
186
+ }, task.timeout);
187
+
188
+ try {
189
+ const messageWithId = { ...task.data, __taskId: task.id };
190
+ workerInstance.worker.postMessage(messageWithId, task.transfer || []);
191
+ task.data = null;
192
+ task.transfer = null;
193
+ } catch (error) {
194
+ this._error(`❌ Failed to send task to worker ${workerInstance.id}:`, error);
195
+ this.handleWorkerError(workerInstance, error);
196
+ }
197
+ }
198
+
199
+ handleWorkerMessage(workerInstance, event) {
200
+ if (event?.data?.type === 'WGPU_INIT_FAILED') {
201
+ const error = new Error(event.data.error || 'WGPU initialization failed');
202
+ const task = workerInstance.currentTask;
203
+ if (task) {
204
+ this.handleTaskFailure(workerInstance, task, error);
205
+ }
206
+ this._replaceWorker(workerInstance);
207
+ return;
208
+ }
209
+
210
+ const responseTaskId = event.data.__taskId;
211
+ const task = (workerInstance.currentTask && workerInstance.currentTask.id === responseTaskId)
212
+ ? workerInstance.currentTask : null;
213
+
214
+ if (!task) {
215
+ this._warn(`[Pool] ⚠️ Stale response for task ${responseTaskId}`);
216
+ return;
217
+ }
218
+
219
+ this._mark(`Task-Received-${task.id}`);
220
+ this._measure(`WorkerProcess:${task.taskType}`, `Task-Sent-${task.id}`, `Task-Received-${task.id}`);
221
+
222
+ if (task.timeoutId) clearTimeout(task.timeoutId);
223
+ task.signal?.removeEventListener('abort', task.abortHandler);
224
+ let shouldReplaceWorker = false;
225
+
226
+ if (!task.isAborted) {
227
+ this._mark(`Main-Resolve-Start-${task.id}`);
228
+
229
+ if (event.data.type === 'PROCESSING_ERROR' || event.data.type === 'VIEWPORT_PROCESSING_ERROR') {
230
+ task.reject(new Error(event.data.error));
231
+ if (event.data.wgpuRuntimeFailure) {
232
+ shouldReplaceWorker = true;
233
+ }
234
+ } else if (event.data.type === 'WORKER_ERROR') {
235
+ task.reject(new Error(event.data.error || 'Worker error'));
236
+ } else {
237
+ task.resolve(event.data);
238
+ }
239
+
240
+ this._mark(`Main-Resolve-End-${task.id}`);
241
+ this._measure(`PoolResolveTrigger:${task.taskType}`, `Main-Resolve-Start-${task.id}`, `Main-Resolve-End-${task.id}`);
242
+ }
243
+
244
+ this._clearTaskMarks(task.id);
245
+ if (shouldReplaceWorker) {
246
+ this._replaceWorker(workerInstance);
247
+ return;
248
+ }
249
+ this.releaseWorker(workerInstance);
250
+ }
251
+
252
+ handleWorkerError(workerInstance, error) {
253
+ const task = workerInstance.currentTask;
254
+ if (task) {
255
+ this.handleTaskFailure(workerInstance, task, error);
256
+ }
257
+ this._replaceWorker(workerInstance);
258
+ }
259
+
260
+ handleTaskTimeout(workerInstance, task) {
261
+ this._warn(`[Pool] 💀 Terminating stuck Worker ${workerInstance.id}`);
262
+ workerInstance.worker.terminate();
263
+ this.handleTaskFailure(workerInstance, task, new Error('Task timeout'));
264
+ this._replaceWorker(workerInstance);
265
+ }
266
+
267
+ handleTaskFailure(workerInstance, task, error) {
268
+ if (task.timeoutId) clearTimeout(task.timeoutId);
269
+ task.signal?.removeEventListener('abort', task.abortHandler);
270
+
271
+ const canRetry = task.data !== null;
272
+
273
+ if (canRetry && task.retries < task.maxRetries && error.name !== 'AbortError' && !task.isAborted) {
274
+ task.retries++;
275
+ delete task.timeoutId;
276
+ task.enqueuedAt = performance.now();
277
+ this.taskQueue.push(task);
278
+ this.releaseWorker(workerInstance);
279
+ } else {
280
+ if (!task.isAborted) {
281
+ task.reject(error);
282
+ }
283
+
284
+ this._clearTaskMarks(task.id);
285
+ if (this.workers.includes(workerInstance)) {
286
+ this.releaseWorker(workerInstance);
287
+ }
288
+ }
289
+ }
290
+
291
+ releaseWorker(workerInstance) {
292
+ workerInstance.busy = false;
293
+ workerInstance.currentTask = null;
294
+ this.availableWorkers.push(workerInstance);
295
+ this.processQueue();
296
+ }
297
+
298
+ _replaceWorker(oldWorkerInstance) {
299
+ try {
300
+ oldWorkerInstance?.worker?.terminate?.();
301
+ } catch (error) {
302
+ this._warn(`[Pool] Failed to terminate worker ${oldWorkerInstance?.id}`, error);
303
+ }
304
+
305
+ const idx = this.workers.findIndex(w => w.id === oldWorkerInstance.id);
306
+ if (idx !== -1) this.workers.splice(idx, 1);
307
+
308
+ const availIdx = this.availableWorkers.findIndex(w => w.id === oldWorkerInstance.id);
309
+ if (availIdx !== -1) this.availableWorkers.splice(availIdx, 1);
310
+
311
+ this._createWorker(oldWorkerInstance.id);
312
+ this.processQueue();
313
+ }
314
+
315
+ terminate() {
316
+ for (const t of this.taskQueue) {
317
+ if (!t.isAborted) t.reject(new Error('Pool terminated'));
318
+ }
319
+ this.taskQueue = [];
320
+
321
+ for (const w of this.workers) {
322
+ const task = w.currentTask;
323
+ if (task && !task.isAborted) {
324
+ if (task.timeoutId) clearTimeout(task.timeoutId);
325
+ task.signal?.removeEventListener('abort', task.abortHandler);
326
+ task.reject(new Error('Pool terminated'));
327
+ this._clearTaskMarks(task.id);
328
+ }
329
+ try {
330
+ w.worker.terminate();
331
+ } catch {
332
+ /* ignore */
333
+ }
334
+ }
335
+
336
+ this.workers = [];
337
+ this.availableWorkers = [];
338
+ this.isInitialized = false;
339
+ }
340
+ }