claw-dashboard 1.8.4 → 2.0.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.
Files changed (135) hide show
  1. package/.c8rc.json +38 -0
  2. package/.dockerignore +68 -0
  3. package/.github/dependabot.yml +45 -0
  4. package/.github/pull_request_template.md +39 -0
  5. package/.github/workflows/ci.yml +147 -0
  6. package/.github/workflows/release.yml +40 -0
  7. package/.github/workflows/security.yml +84 -0
  8. package/.husky/pre-commit +1 -0
  9. package/.planning/codebase/ARCHITECTURE.md +206 -0
  10. package/.planning/codebase/INTEGRATIONS.md +150 -0
  11. package/.planning/codebase/STACK.md +122 -0
  12. package/.planning/codebase/STRUCTURE.md +201 -0
  13. package/CHANGELOG.md +293 -0
  14. package/CONTRIBUTING.md +378 -0
  15. package/Dockerfile +54 -0
  16. package/FEATURES.md +195 -21
  17. package/README.md +83 -6
  18. package/TODO.md +28 -0
  19. package/build-cjs.js +127 -0
  20. package/cjs-shim.js +13 -0
  21. package/dist/clawdash +1729 -0
  22. package/dist/clawdash.meta.json +2236 -0
  23. package/dist/widgets.cjs +6511 -0
  24. package/docker-compose.yml +67 -0
  25. package/docs/API.md +1050 -0
  26. package/docs/PLUGINS.md +1504 -0
  27. package/esbuild.config.js +158 -0
  28. package/eslint.config.js +56 -0
  29. package/examples/plugins/README.md +122 -0
  30. package/examples/plugins/api-status/index.js +294 -0
  31. package/examples/plugins/api-status/plugin.json +19 -0
  32. package/examples/plugins/hello-world/index.js +104 -0
  33. package/examples/plugins/hello-world/plugin.json +15 -0
  34. package/examples/plugins/system-metrics-chart/index.js +339 -0
  35. package/examples/plugins/system-metrics-chart/plugin.json +18 -0
  36. package/examples/plugins/weather-widget/index.js +136 -0
  37. package/examples/plugins/weather-widget/plugin.json +19 -0
  38. package/index.cjs +23005 -0
  39. package/index.js +5331 -512
  40. package/jest.config.js +11 -0
  41. package/man/clawdash.1 +276 -0
  42. package/package.json +54 -5
  43. package/schemas/plugin-manifest.json +128 -0
  44. package/scripts/release.js +595 -0
  45. package/src/alerts.js +693 -0
  46. package/src/auto-save.js +584 -0
  47. package/src/cache.js +390 -0
  48. package/src/checksum.js +146 -0
  49. package/src/cli/args.js +110 -0
  50. package/src/cli/export-schedule.js +423 -0
  51. package/src/cli/export-snapshot.js +138 -0
  52. package/src/cli/help.js +69 -0
  53. package/src/cli/import-snapshot.js +230 -0
  54. package/src/cli/index.js +14 -0
  55. package/src/cli/list-templates.js +69 -0
  56. package/src/cli/validate-config.js +76 -0
  57. package/src/cli/validate-plugin.js +187 -0
  58. package/src/cli/version.js +15 -0
  59. package/src/config-validator.js +586 -0
  60. package/src/config-watcher.js +401 -0
  61. package/src/config.js +684 -0
  62. package/src/container-detector.js +499 -0
  63. package/src/database.js +734 -0
  64. package/src/differential-render.js +327 -0
  65. package/src/errors.js +169 -0
  66. package/src/export-scheduler.js +581 -0
  67. package/src/gateway-manager.js +685 -0
  68. package/src/hints.js +371 -0
  69. package/src/loading-states.js +509 -0
  70. package/src/logger.js +185 -0
  71. package/src/memory-pressure.js +467 -0
  72. package/src/performance-monitor.js +374 -0
  73. package/src/plugin-errors.js +586 -0
  74. package/src/plugin-manifest-validator.js +219 -0
  75. package/src/plugin-reload.js +481 -0
  76. package/src/plugin-scaffold.js +1941 -0
  77. package/src/plugin-validator.js +284 -0
  78. package/src/retry.js +218 -0
  79. package/src/security.js +860 -0
  80. package/src/snapshot.js +372 -0
  81. package/src/splash.js +115 -0
  82. package/src/theme-selector.js +411 -0
  83. package/src/themes.js +874 -0
  84. package/src/transitions.js +534 -0
  85. package/src/types.d.ts +174 -0
  86. package/src/validation.js +926 -0
  87. package/src/web-server.js +885 -0
  88. package/src/widgets/builtin-widgets.js +1057 -0
  89. package/src/widgets/config-processor.js +401 -0
  90. package/src/widgets/dependency-resolver.js +596 -0
  91. package/src/widgets/index.js +79 -0
  92. package/src/widgets/plugin-api.js +845 -0
  93. package/src/widgets/widget-error-boundary.js +773 -0
  94. package/src/widgets/widget-error-isolation.js +551 -0
  95. package/src/widgets/widget-loader.js +1437 -0
  96. package/src/workers/system-worker.js +130 -0
  97. package/src/workers/worker-pool.js +633 -0
  98. package/tests/alerts.test.js +437 -0
  99. package/tests/auto-save.test.js +529 -0
  100. package/tests/cache.test.js +317 -0
  101. package/tests/cli.test.js +351 -0
  102. package/tests/command-palette.test.js +320 -0
  103. package/tests/config-processor.test.js +452 -0
  104. package/tests/config-validator.test.js +710 -0
  105. package/tests/config-watcher.test.js +594 -0
  106. package/tests/config.test.js +755 -0
  107. package/tests/database.test.js +438 -0
  108. package/tests/errors.test.js +189 -0
  109. package/tests/example-plugins.test.js +624 -0
  110. package/tests/integration.test.js +1321 -0
  111. package/tests/loading-states.test.js +300 -0
  112. package/tests/manifest-validation-on-load.test.js +671 -0
  113. package/tests/performance-monitor.test.js +190 -0
  114. package/tests/plugin-api-rate-limit.test.js +302 -0
  115. package/tests/plugin-errors.test.js +311 -0
  116. package/tests/plugin-lifecycle-e2e.test.js +1036 -0
  117. package/tests/plugin-reload.test.js +764 -0
  118. package/tests/rate-limiter.test.js +539 -0
  119. package/tests/retry.test.js +308 -0
  120. package/tests/security.test.js +411 -0
  121. package/tests/settings-modal.test.js +226 -0
  122. package/tests/theme-selector.test.js +57 -0
  123. package/tests/utils.js +242 -0
  124. package/tests/utils.test.js +317 -0
  125. package/tests/validate-plugin-cli.test.js +359 -0
  126. package/tests/validation.test.js +837 -0
  127. package/tests/web-server.test.js +646 -0
  128. package/tests/widget-arrange-mode.test.js +183 -0
  129. package/tests/widget-config-hot-reload.test.js +465 -0
  130. package/tests/widget-dependency.test.js +591 -0
  131. package/tests/widget-error-boundary.test.js +749 -0
  132. package/tests/widget-error-isolation.test.js +469 -0
  133. package/tests/widget-integration.test.js +1147 -0
  134. package/tests/widget-refresh-intervals.test.js +284 -0
  135. package/tests/worker-pool.test.js +522 -0
@@ -0,0 +1,633 @@
1
+ /**
2
+ * Worker Pool Manager for system information gathering
3
+ * Manages worker thread lifecycle and provides a simple interface
4
+ * for offloading heavy systeminformation calls
5
+ */
6
+
7
+ import { Worker } from 'worker_threads';
8
+ import { fileURLToPath } from 'url';
9
+ import { dirname, join } from 'path';
10
+ import logger from '../logger.js';
11
+ import config from '../config.js';
12
+ import { WorkerPoolOverloadError } from '../errors.js';
13
+
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = dirname(__filename);
16
+
17
+ // Degradation levels
18
+ const DEGRADATION_LEVELS = {
19
+ NONE: 'none',
20
+ WARNING: 'warning',
21
+ CRITICAL: 'critical',
22
+ };
23
+
24
+ // Circuit breaker states
25
+ const CIRCUIT_STATES = {
26
+ CLOSED: 'closed',
27
+ OPEN: 'open',
28
+ HALF_OPEN: 'half_open',
29
+ };
30
+
31
+ /**
32
+ * Worker Pool Manager class
33
+ * Manages a pool of worker threads for parallel system information gathering
34
+ * with graceful degradation under overload conditions
35
+ */
36
+ export const DegradationLevel = {
37
+ NONE: 'none',
38
+ WARNING: 'warning',
39
+ CRITICAL: 'critical',
40
+ };
41
+
42
+ class WorkerPool {
43
+ constructor(options = {}) {
44
+ this.workerPath = options.workerPath || join(__dirname, 'system-worker.js');
45
+ this.maxWorkers = options.maxWorkers || config.WORKERS?.MAX_WORKERS || 2;
46
+ this.taskTimeout = options.taskTimeout || config.WORKERS?.TASK_TIMEOUT || 10000;
47
+ this.enableWorkers = options.enableWorkers ?? config.WORKERS?.ENABLED ?? true;
48
+
49
+ this.workers = [];
50
+ this.taskQueue = [];
51
+ this.taskId = 0;
52
+ this.pendingTasks = new Map();
53
+ this.isShutdown = false;
54
+
55
+ // Graceful degradation settings
56
+ const degradationConfig = options.degradationConfig || config.WORKER_DEGRADATION || {};
57
+ this.degradationConfig = {
58
+ queue: degradationConfig.QUEUE || { WARNING_SIZE: 10, CRITICAL_SIZE: 25, MAX_SIZE: 50 },
59
+ utilization: degradationConfig.UTILIZATION || { WARNING_PCT: 75, CRITICAL_PCT: 90 },
60
+ strategies: degradationConfig.STRATEGIES || {
61
+ ADAPTIVE_TIMEOUT: { ENABLED: true, WARNING_MULTIPLIER: 1.5, CRITICAL_MULTIPLIER: 2.0 },
62
+ SHED_LOAD: { ENABLED: true, SHED_NON_CRITICAL: true },
63
+ CIRCUIT_BREAKER: { ENABLED: true, FAILURE_THRESHOLD: 5, RESET_TIMEOUT_MS: 30000 },
64
+ },
65
+ recovery: degradationConfig.RECOVERY || { COOLDOWN_MS: 5000, MIN_NORMAL_OPERATIONS: 5 },
66
+ };
67
+
68
+ // Degradation state
69
+ this.degradationLevel = DEGRADATION_LEVELS.NONE;
70
+ this.degradationSince = null;
71
+ this.consecutiveFailures = 0;
72
+ this.circuitBreakerState = CIRCUIT_STATES.CLOSED;
73
+ this.circuitBreakerOpenedAt = null;
74
+ this.successfulOperations = 0;
75
+ this.overloadEvents = 0;
76
+ this.lastOverloadTime = null;
77
+ this.totalRejected = 0;
78
+ this.totalShed = 0;
79
+
80
+ // Check if worker threads are supported (Node.js 12+)
81
+ this.workersSupported = this.checkWorkerSupport();
82
+
83
+ if (this.enableWorkers && this.workersSupported) {
84
+ this.initializeWorkers();
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Check if worker threads are supported
90
+ * @returns {boolean} True if worker threads are available
91
+ */
92
+ checkWorkerSupport() {
93
+ try {
94
+ // Worker threads are available in Node.js 12+
95
+ const [major] = process.versions.node.split('.').map(Number);
96
+ if (major < 12) {
97
+ logger.info('Worker threads not available (Node.js < 12)');
98
+ return false;
99
+ }
100
+
101
+ // Worker threads are available - we'll verify when creating workers
102
+ // This is a preliminary check
103
+ return true;
104
+ } catch (error) {
105
+ logger.info('Worker threads not supported in this environment:', error.message);
106
+ return false;
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Initialize worker threads
112
+ */
113
+ initializeWorkers() {
114
+ try {
115
+ for (let i = 0; i < this.maxWorkers; i++) {
116
+ this.createWorker(i);
117
+ }
118
+ logger.info(`Initialized ${this.maxWorkers} system information worker threads`);
119
+ } catch (error) {
120
+ logger.error('Failed to initialize workers:', error.message);
121
+ this.workersSupported = false;
122
+ this.workers = [];
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Create a new worker thread
128
+ * @param {number} id - Worker ID
129
+ * @returns {Worker} Created worker
130
+ */
131
+ createWorker(id) {
132
+ const worker = new Worker(this.workerPath, {
133
+ execArgv: ['--experimental-vm-modules'],
134
+ workerData: { __filename: this.workerPath }
135
+ });
136
+
137
+ worker.id = id;
138
+ worker.isReady = false;
139
+ worker.isBusy = false;
140
+
141
+ worker.on('message', (message) => {
142
+ this.handleWorkerMessage(worker, message);
143
+ });
144
+
145
+ worker.on('error', (error) => {
146
+ logger.error(`Worker ${id} error:`, error.message);
147
+ this.restartWorker(id);
148
+ });
149
+
150
+ worker.on('exit', (code) => {
151
+ if (code !== 0) {
152
+ logger.warn(`Worker ${id} exited with code ${code}`);
153
+ }
154
+ this.removeWorker(id);
155
+ });
156
+
157
+ this.workers.push(worker);
158
+ return worker;
159
+ }
160
+
161
+ /**
162
+ * Handle messages from worker threads
163
+ * @param {Worker} worker - Worker that sent the message
164
+ * @param {Object} message - Message from worker
165
+ */
166
+ handleWorkerMessage(worker, message) {
167
+ // Handle ready signal
168
+ if (message.type === 'ready') {
169
+ worker.isReady = true;
170
+ this.processQueue();
171
+ return;
172
+ }
173
+
174
+ // Handle task completion
175
+ if (message.id !== undefined) {
176
+ const task = this.pendingTasks.get(message.id);
177
+ if (task) {
178
+ this.pendingTasks.delete(message.id);
179
+ worker.isBusy = false;
180
+
181
+ if (task.timeout) {
182
+ clearTimeout(task.timeout);
183
+ }
184
+
185
+ if (message.success) {
186
+ this.recordSuccess();
187
+ task.resolve(message.data);
188
+ } else {
189
+ this.recordFailure();
190
+ const error = new Error(message.error || 'Worker task failed');
191
+ if (message.stack) {
192
+ error.stack = message.stack;
193
+ }
194
+ task.reject(error);
195
+ }
196
+
197
+ this.processQueue();
198
+ }
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Restart a worker thread
204
+ * @param {number} id - Worker ID to restart
205
+ */
206
+ restartWorker(id) {
207
+ const existingWorker = this.workers.find(w => w.id === id);
208
+ if (existingWorker) {
209
+ existingWorker.terminate().catch(() => {});
210
+ }
211
+
212
+ // Create replacement worker (unref'd to prevent timer from blocking shutdown)
213
+ setTimeout(() => {
214
+ if (!this.isShutdown) {
215
+ this.createWorker(id);
216
+ }
217
+ }, 100).unref();
218
+ }
219
+
220
+ /**
221
+ * Remove a worker from the pool
222
+ * @param {number} id - Worker ID to remove
223
+ */
224
+ removeWorker(id) {
225
+ const index = this.workers.findIndex(w => w.id === id);
226
+ if (index !== -1) {
227
+ this.workers.splice(index, 1);
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Check current system load and detect overload conditions
233
+ * @returns {Object} Load status with degradation level
234
+ */
235
+ checkOverload() {
236
+ const queueSize = this.taskQueue.length;
237
+ const busyWorkers = this.workers.filter(w => w.isBusy).length;
238
+ const utilizationPercent = this.workers.length > 0
239
+ ? Math.round((busyWorkers / this.workers.length) * 100)
240
+ : 0;
241
+
242
+ // Check circuit breaker
243
+ if (this.circuitBreakerState === CIRCUIT_STATES.OPEN) {
244
+ const timeOpen = Date.now() - this.circuitBreakerOpenedAt;
245
+ if (timeOpen >= this.degradationConfig.strategies.CIRCUIT_BREAKER.RESET_TIMEOUT_MS) {
246
+ // Try half-open state
247
+ this.circuitBreakerState = CIRCUIT_STATES.HALF_OPEN;
248
+ logger.info('Circuit breaker entering half-open state');
249
+ } else {
250
+ return {
251
+ level: DEGRADATION_LEVELS.CRITICAL,
252
+ queueSize,
253
+ utilizationPercent,
254
+ circuitOpen: true,
255
+ reason: 'circuit_breaker',
256
+ };
257
+ }
258
+ }
259
+
260
+ // Check queue size thresholds
261
+ const { WARNING_SIZE, CRITICAL_SIZE, MAX_SIZE } = this.degradationConfig.queue;
262
+
263
+ if (queueSize >= MAX_SIZE) {
264
+ return {
265
+ level: DEGRADATION_LEVELS.CRITICAL,
266
+ queueSize,
267
+ utilizationPercent,
268
+ reason: 'max_queue_size',
269
+ };
270
+ }
271
+
272
+ if (queueSize >= CRITICAL_SIZE || utilizationPercent >= this.degradationConfig.utilization.CRITICAL_PCT) {
273
+ return {
274
+ level: DEGRADATION_LEVELS.CRITICAL,
275
+ queueSize,
276
+ utilizationPercent,
277
+ reason: queueSize >= CRITICAL_SIZE ? 'queue_size' : 'utilization',
278
+ };
279
+ }
280
+
281
+ if (queueSize >= WARNING_SIZE || utilizationPercent >= this.degradationConfig.utilization.WARNING_PCT) {
282
+ return {
283
+ level: DEGRADATION_LEVELS.WARNING,
284
+ queueSize,
285
+ utilizationPercent,
286
+ reason: queueSize >= WARNING_SIZE ? 'queue_size' : 'utilization',
287
+ };
288
+ }
289
+
290
+ return {
291
+ level: DEGRADATION_LEVELS.NONE,
292
+ queueSize,
293
+ utilizationPercent,
294
+ reason: null,
295
+ };
296
+ }
297
+
298
+ /**
299
+ * Get adaptive timeout based on current degradation level
300
+ * @returns {number} Adjusted timeout in milliseconds
301
+ */
302
+ getAdaptiveTimeout() {
303
+ const baseTimeout = this.taskTimeout;
304
+
305
+ if (!this.degradationConfig.strategies.ADAPTIVE_TIMEOUT.ENABLED) {
306
+ return baseTimeout;
307
+ }
308
+
309
+ switch (this.degradationLevel) {
310
+ case DEGRADATION_LEVELS.CRITICAL:
311
+ return baseTimeout * this.degradationConfig.strategies.ADAPTIVE_TIMEOUT.CRITICAL_MULTIPLIER;
312
+ case DEGRADATION_LEVELS.WARNING:
313
+ return baseTimeout * this.degradationConfig.strategies.ADAPTIVE_TIMEOUT.WARNING_MULTIPLIER;
314
+ default:
315
+ return baseTimeout;
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Update degradation level based on current load
321
+ * @param {Object} loadStatus - Result from checkOverload()
322
+ */
323
+ updateDegradationLevel(loadStatus) {
324
+ const previousLevel = this.degradationLevel;
325
+ const newLevel = loadStatus.level;
326
+
327
+ if (previousLevel !== newLevel) {
328
+ this.degradationLevel = newLevel;
329
+ this.degradationSince = Date.now();
330
+
331
+ if (newLevel === DEGRADATION_LEVELS.CRITICAL) {
332
+ this.overloadEvents++;
333
+ this.lastOverloadTime = Date.now();
334
+ logger.warn(`Worker pool entering critical degradation: ${loadStatus.reason} (queue: ${loadStatus.queueSize}, utilization: ${loadStatus.utilizationPercent}%)`);
335
+ } else if (newLevel === DEGRADATION_LEVELS.WARNING) {
336
+ logger.warn(`Worker pool entering warning state: ${loadStatus.reason} (queue: ${loadStatus.queueSize}, utilization: ${loadStatus.utilizationPercent}%)`);
337
+ } else if (previousLevel !== DEGRADATION_LEVELS.NONE) {
338
+ logger.info(`Worker pool returning to normal operation from ${previousLevel}`);
339
+ }
340
+ }
341
+
342
+ // Reset consecutive successes when level changes
343
+ if (previousLevel !== newLevel) {
344
+ this.successfulOperations = 0;
345
+ }
346
+ }
347
+
348
+ /**
349
+ * Record a successful operation for recovery tracking
350
+ */
351
+ recordSuccess() {
352
+ this.consecutiveFailures = 0;
353
+ this.successfulOperations++;
354
+
355
+ // Close circuit breaker if in half-open state
356
+ if (this.circuitBreakerState === CIRCUIT_STATES.HALF_OPEN) {
357
+ this.circuitBreakerState = CIRCUIT_STATES.CLOSED;
358
+ this.circuitBreakerOpenedAt = null;
359
+ logger.info('Circuit breaker closed - service recovered');
360
+ }
361
+
362
+ // Auto-recover if we've had enough successful operations
363
+ if (this.degradationLevel !== DEGRADATION_LEVELS.NONE) {
364
+ const cooldownElapsed = Date.now() - this.degradationSince >= this.degradationConfig.recovery.COOLDOWN_MS;
365
+ const minOpsMet = this.successfulOperations >= this.degradationConfig.recovery.MIN_NORMAL_OPERATIONS;
366
+
367
+ if (cooldownElapsed && minOpsMet) {
368
+ const loadStatus = this.checkOverload();
369
+ if (loadStatus.level === DEGRADATION_LEVELS.NONE) {
370
+ this.updateDegradationLevel(loadStatus);
371
+ }
372
+ }
373
+ }
374
+ }
375
+
376
+ /**
377
+ * Record a failed operation and potentially open circuit breaker
378
+ */
379
+ recordFailure() {
380
+ this.consecutiveFailures++;
381
+ this.successfulOperations = 0;
382
+
383
+ const threshold = this.degradationConfig.strategies.CIRCUIT_BREAKER.FAILURE_THRESHOLD;
384
+
385
+ if (this.degradationConfig.strategies.CIRCUIT_BREAKER.ENABLED &&
386
+ this.consecutiveFailures >= threshold &&
387
+ this.circuitBreakerState === CIRCUIT_STATES.CLOSED) {
388
+ this.circuitBreakerState = CIRCUIT_STATES.OPEN;
389
+ this.circuitBreakerOpenedAt = Date.now();
390
+ logger.error(`Circuit breaker opened after ${this.consecutiveFailures} consecutive failures`);
391
+ }
392
+ }
393
+
394
+ /**
395
+ * Check if we should reject/shed a new task due to overload
396
+ * @param {Object} options - Task options
397
+ * @returns {boolean} True if task should be rejected
398
+ */
399
+ shouldShedLoad(options = {}) {
400
+ // Never shed critical tasks
401
+ if (options.critical || options.priority === 'high') {
402
+ return false;
403
+ }
404
+
405
+ // Check if load shedding is enabled
406
+ if (!this.degradationConfig.strategies.SHED_LOAD.ENABLED) {
407
+ return false;
408
+ }
409
+
410
+ // Shed non-critical tasks when in critical state
411
+ if (this.degradationLevel === DEGRADATION_LEVELS.CRITICAL &&
412
+ this.degradationConfig.strategies.SHED_LOAD.SHED_NON_CRITICAL) {
413
+ return true;
414
+ }
415
+
416
+ return false;
417
+ }
418
+
419
+ /**
420
+ * Execute a systeminformation command via worker thread
421
+ * @param {string} command - Command to execute
422
+ * @param {Object} options - Command options
423
+ * @returns {Promise<any>} Command result
424
+ */
425
+ async execute(command, options = {}) {
426
+ // Check for overload conditions
427
+ const loadStatus = this.checkOverload();
428
+ this.updateDegradationLevel(loadStatus);
429
+
430
+ // Check if queue is at max capacity (reject before load shedding)
431
+ if (loadStatus.level === DEGRADATION_LEVELS.CRITICAL && loadStatus.reason === 'max_queue_size') {
432
+ this.totalRejected++;
433
+ throw new WorkerPoolOverloadError('Worker pool queue at maximum capacity', {
434
+ degradationLevel: this.degradationLevel,
435
+ queueSize: loadStatus.queueSize,
436
+ utilizationPercent: loadStatus.utilizationPercent,
437
+ });
438
+ }
439
+
440
+ // Check if circuit breaker is open
441
+ if (loadStatus.circuitOpen) {
442
+ this.totalRejected++;
443
+ throw new WorkerPoolOverloadError('Worker pool circuit breaker is open', {
444
+ degradationLevel: this.degradationLevel,
445
+ queueSize: loadStatus.queueSize,
446
+ utilizationPercent: loadStatus.utilizationPercent,
447
+ });
448
+ }
449
+
450
+ // Check if we should shed load (after max queue check)
451
+ if (this.shouldShedLoad(options)) {
452
+ this.totalShed++;
453
+ logger.debug(`Shedding load for command: ${command}`);
454
+ // Fall back to direct execution for shed tasks
455
+ try {
456
+ const result = await this.fallbackExecute(command, options);
457
+ this.recordSuccess();
458
+ return result;
459
+ } catch (error) {
460
+ this.recordFailure();
461
+ throw error;
462
+ }
463
+ }
464
+
465
+ // Fall back to direct execution if workers aren't available
466
+ if (!this.workersSupported || !this.enableWorkers || this.workers.length === 0) {
467
+ try {
468
+ const result = await this.fallbackExecute(command, options);
469
+ this.recordSuccess();
470
+ return result;
471
+ } catch (error) {
472
+ this.recordFailure();
473
+ throw error;
474
+ }
475
+ }
476
+
477
+ return new Promise((resolve, reject) => {
478
+ const id = ++this.taskId;
479
+
480
+ // Use adaptive timeout based on degradation level
481
+ const adaptiveTimeout = this.getAdaptiveTimeout();
482
+
483
+ // Set up timeout with unref to prevent timer from keeping process alive
484
+ const timeout = setTimeout(() => {
485
+ this.pendingTasks.delete(id);
486
+ this.recordFailure();
487
+ reject(new Error(`Worker task timeout: ${command}`));
488
+ }, adaptiveTimeout).unref();
489
+
490
+ // Store task
491
+ this.pendingTasks.set(id, {
492
+ id,
493
+ command,
494
+ options,
495
+ resolve,
496
+ reject,
497
+ timeout,
498
+ timestamp: Date.now(),
499
+ });
500
+
501
+ // Add to queue
502
+ this.taskQueue.push({ id, command, options });
503
+ this.processQueue();
504
+ });
505
+ }
506
+
507
+ /**
508
+ * Process the task queue
509
+ */
510
+ processQueue() {
511
+ if (this.taskQueue.length === 0) return;
512
+
513
+ // Find an available worker
514
+ const availableWorker = this.workers.find(w => w.isReady && !w.isBusy);
515
+ if (!availableWorker) return;
516
+
517
+ // Get next task
518
+ const task = this.taskQueue.shift();
519
+ if (!task) return;
520
+
521
+ // Mark worker as busy and send task
522
+ availableWorker.isBusy = true;
523
+ availableWorker.postMessage({
524
+ id: task.id,
525
+ command: task.command,
526
+ options: task.options,
527
+ });
528
+ }
529
+
530
+ /**
531
+ * Fallback execution when workers aren't available
532
+ * Executes directly in the main thread
533
+ * @param {string} command - Command to execute
534
+ * @param {Object} options - Command options
535
+ * @returns {Promise<any>} Command result
536
+ */
537
+ async fallbackExecute(command, options = {}) {
538
+ try {
539
+ // Dynamically import systeminformation
540
+ const si = await import('systeminformation');
541
+ const systemInfo = si.default || si;
542
+
543
+ switch (command) {
544
+ case 'currentLoad':
545
+ return await systemInfo.currentLoad();
546
+ case 'mem':
547
+ return await systemInfo.mem();
548
+ case 'graphics':
549
+ return await systemInfo.graphics();
550
+ case 'networkStats':
551
+ return await systemInfo.networkStats();
552
+ case 'fsSize':
553
+ return await systemInfo.fsSize();
554
+ case 'systemData': {
555
+ const [os, ver, time] = await Promise.all([
556
+ systemInfo.osInfo(),
557
+ systemInfo.versions(),
558
+ systemInfo.time(),
559
+ ]);
560
+ return { os, ver, time };
561
+ }
562
+ default:
563
+ throw new Error(`Unknown command: ${command}`);
564
+ }
565
+ } catch (error) {
566
+ logger.warn(`Fallback execution failed for ${command}:`, error.message);
567
+ throw error;
568
+ }
569
+ }
570
+
571
+ /**
572
+ * Get worker pool status
573
+ * @returns {Object} Pool status
574
+ */
575
+ getStatus() {
576
+ const loadStatus = this.checkOverload();
577
+ return {
578
+ enabled: this.enableWorkers,
579
+ supported: this.workersSupported,
580
+ totalWorkers: this.workers.length,
581
+ busyWorkers: this.workers.filter(w => w.isBusy).length,
582
+ readyWorkers: this.workers.filter(w => w.isReady).length,
583
+ pendingTasks: this.pendingTasks.size,
584
+ queuedTasks: this.taskQueue.length,
585
+ degradation: {
586
+ level: this.degradationLevel,
587
+ since: this.degradationSince,
588
+ queueSize: loadStatus.queueSize,
589
+ utilizationPercent: loadStatus.utilizationPercent,
590
+ circuitBreakerState: this.circuitBreakerState,
591
+ consecutiveFailures: this.consecutiveFailures,
592
+ successfulOperations: this.successfulOperations,
593
+ overloadEvents: this.overloadEvents,
594
+ lastOverloadTime: this.lastOverloadTime,
595
+ totalRejected: this.totalRejected,
596
+ totalShed: this.totalShed,
597
+ },
598
+ };
599
+ }
600
+
601
+ /**
602
+ * Shut down all workers
603
+ */
604
+ async shutdown() {
605
+ this.isShutdown = true;
606
+
607
+ // Reject pending tasks
608
+ for (const [id, task] of this.pendingTasks) {
609
+ if (task.timeout) {
610
+ clearTimeout(task.timeout);
611
+ }
612
+ task.reject(new Error('Worker pool shutting down'));
613
+ }
614
+ this.pendingTasks.clear();
615
+ this.taskQueue = [];
616
+
617
+ // Terminate all workers
618
+ const terminationPromises = this.workers.map(worker =>
619
+ worker.terminate().catch(() => {})
620
+ );
621
+
622
+ await Promise.all(terminationPromises);
623
+ this.workers = [];
624
+
625
+ logger.info('Worker pool shut down');
626
+ }
627
+ }
628
+
629
+ // Create singleton instance
630
+ const workerPool = new WorkerPool();
631
+
632
+ export default workerPool;
633
+ export { WorkerPool, DEGRADATION_LEVELS, CIRCUIT_STATES };