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,551 @@
1
+ /**
2
+ * Widget Error Isolation Module
3
+ * Ensures that widget failures don't crash the entire dashboard
4
+ * Provides health tracking and recovery mechanisms
5
+ */
6
+
7
+ import logger from '../logger.js';
8
+ import { DashboardError, UIError } from '../errors.js';
9
+
10
+ // Safe logger wrapper for test environments
11
+ const safeLogger = logger || {
12
+ info: () => {},
13
+ warn: () => {},
14
+ error: () => {},
15
+ debug: () => {},
16
+ };
17
+
18
+ /**
19
+ * Widget health status constants
20
+ */
21
+ export const WidgetHealthStatus = {
22
+ HEALTHY: 'healthy',
23
+ DEGRADED: 'degraded', // Partially working with errors
24
+ FAILED: 'failed', // Completely failed, not rendering
25
+ RECOVERING: 'recovering', // Attempting recovery
26
+ };
27
+
28
+ /**
29
+ * Widget error types
30
+ */
31
+ export const WidgetErrorType = {
32
+ INIT_ERROR: 'init_error',
33
+ CREATE_ERROR: 'create_error',
34
+ DATA_ERROR: 'data_error',
35
+ RENDER_ERROR: 'render_error',
36
+ DESTROY_ERROR: 'destroy_error',
37
+ TIMEOUT_ERROR: 'timeout_error',
38
+ UNKNOWN_ERROR: 'unknown_error',
39
+ };
40
+
41
+ /**
42
+ * Default configuration for error isolation
43
+ */
44
+ export const DEFAULT_ISOLATION_CONFIG = {
45
+ // Error thresholds
46
+ maxConsecutiveErrors: 3,
47
+ errorWindowMs: 60000, // 1 minute window for error counting
48
+
49
+ // Recovery settings
50
+ autoRecover: true,
51
+ recoveryDelayMs: 5000,
52
+ maxRecoveryAttempts: 3,
53
+
54
+ // Timeout settings
55
+ initTimeoutMs: 5000,
56
+ createTimeoutMs: 5000,
57
+ dataTimeoutMs: 10000,
58
+ renderTimeoutMs: 1000,
59
+ destroyTimeoutMs: 3000,
60
+
61
+ // Behavior settings
62
+ failSilently: true, // Don't throw on widget errors
63
+ logErrors: true, // Log widget errors
64
+ degradeOnError: true, // Mark as degraded instead of failed on first errors
65
+ };
66
+
67
+ /**
68
+ * Widget error for isolated widget failures
69
+ */
70
+ export class WidgetIsolatedError extends DashboardError {
71
+ constructor(widgetId, operation, originalError, type = WidgetErrorType.UNKNOWN_ERROR) {
72
+ super(
73
+ `Widget '${widgetId}' ${operation} failed: ${originalError?.message || 'Unknown error'}`,
74
+ 'WIDGET_ISOLATED_ERROR',
75
+ 500,
76
+ { widgetId, operation, type, originalError: originalError?.message }
77
+ );
78
+ this.widgetId = widgetId;
79
+ this.operation = operation;
80
+ this.errorType = type;
81
+ this.originalError = originalError;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Widget health tracker for monitoring widget status
87
+ */
88
+ export class WidgetHealthTracker {
89
+ constructor(config = {}) {
90
+ this.config = { ...DEFAULT_ISOLATION_CONFIG, ...config };
91
+ this.healthStatus = new Map(); // widgetId -> health info
92
+ this.errorHistory = new Map(); // widgetId -> array of error timestamps
93
+ }
94
+
95
+ /**
96
+ * Get or create health record for a widget
97
+ * @private
98
+ */
99
+ _getHealthRecord(widgetId) {
100
+ if (!this.healthStatus.has(widgetId)) {
101
+ this.healthStatus.set(widgetId, {
102
+ status: WidgetHealthStatus.HEALTHY,
103
+ consecutiveErrors: 0,
104
+ totalErrors: 0,
105
+ recoveryAttempts: 0,
106
+ lastError: null,
107
+ lastSuccess: Date.now(),
108
+ firstFailure: null,
109
+ degradedSince: null,
110
+ failedSince: null,
111
+ });
112
+ }
113
+ return this.healthStatus.get(widgetId);
114
+ }
115
+
116
+ /**
117
+ * Record a successful widget operation
118
+ * @param {string} widgetId - Widget identifier
119
+ */
120
+ recordSuccess(widgetId) {
121
+ const record = this._getHealthRecord(widgetId);
122
+ record.status = WidgetHealthStatus.HEALTHY;
123
+ record.consecutiveErrors = 0;
124
+ record.lastSuccess = Date.now();
125
+ record.recoveryAttempts = 0;
126
+
127
+ // Clear error history for this widget
128
+ this.errorHistory.delete(widgetId);
129
+ }
130
+
131
+ /**
132
+ * Record a widget error and update health status
133
+ * @param {string} widgetId - Widget identifier
134
+ * @param {Error} error - The error that occurred
135
+ * @param {string} errorType - Type of error
136
+ * @returns {Object} Updated health status
137
+ */
138
+ recordError(widgetId, error, errorType = WidgetErrorType.UNKNOWN_ERROR) {
139
+ const record = this._getHealthRecord(widgetId);
140
+ const now = Date.now();
141
+
142
+ // Update error counts
143
+ record.consecutiveErrors++;
144
+ record.totalErrors++;
145
+ record.lastError = {
146
+ message: error?.message,
147
+ type: errorType,
148
+ timestamp: now,
149
+ stack: error?.stack,
150
+ };
151
+
152
+ // Track error history
153
+ if (!this.errorHistory.has(widgetId)) {
154
+ this.errorHistory.set(widgetId, []);
155
+ }
156
+ const errors = this.errorHistory.get(widgetId);
157
+ errors.push(now);
158
+
159
+ // Clean old errors outside the window
160
+ const cutoff = now - this.config.errorWindowMs;
161
+ while (errors.length > 0 && errors[0] < cutoff) {
162
+ errors.shift();
163
+ }
164
+
165
+ // Update status based on error thresholds
166
+ if (record.firstFailure === null) {
167
+ record.firstFailure = now;
168
+ }
169
+
170
+ // Check if we should mark as failed
171
+ const recentErrorCount = errors.length;
172
+ if (recentErrorCount >= this.config.maxConsecutiveErrors) {
173
+ record.status = WidgetHealthStatus.FAILED;
174
+ record.failedSince = now;
175
+ } else if (this.config.degradeOnError && record.status === WidgetHealthStatus.HEALTHY) {
176
+ record.status = WidgetHealthStatus.DEGRADED;
177
+ record.degradedSince = now;
178
+ }
179
+
180
+ return { ...record };
181
+ }
182
+
183
+ /**
184
+ * Mark widget as recovering
185
+ * @param {string} widgetId - Widget identifier
186
+ */
187
+ markRecovering(widgetId) {
188
+ const record = this._getHealthRecord(widgetId);
189
+ record.status = WidgetHealthStatus.RECOVERING;
190
+ record.recoveryAttempts++;
191
+ }
192
+
193
+ /**
194
+ * Get health status for a widget
195
+ * @param {string} widgetId - Widget identifier
196
+ * @returns {Object|null} Health status or null if not tracked
197
+ */
198
+ getHealth(widgetId) {
199
+ const record = this.healthStatus.get(widgetId);
200
+ if (!record) return null;
201
+
202
+ const errors = this.errorHistory.get(widgetId) || [];
203
+ return {
204
+ ...record,
205
+ recentErrorCount: errors.length,
206
+ isHealthy: record.status === WidgetHealthStatus.HEALTHY,
207
+ isOperational: record.status !== WidgetHealthStatus.FAILED,
208
+ };
209
+ }
210
+
211
+ /**
212
+ * Get health status for all tracked widgets
213
+ * @returns {Object} Map of widgetId to health status
214
+ */
215
+ getAllHealth() {
216
+ const result = {};
217
+ for (const [widgetId, record] of this.healthStatus) {
218
+ result[widgetId] = this.getHealth(widgetId);
219
+ }
220
+ return result;
221
+ }
222
+
223
+ /**
224
+ * Check if a widget should be allowed to recover
225
+ * @param {string} widgetId - Widget identifier
226
+ * @returns {boolean} True if recovery should be attempted
227
+ */
228
+ canRecover(widgetId) {
229
+ const record = this._getHealthRecord(widgetId);
230
+ if (!this.config.autoRecover) return false;
231
+ if (record.recoveryAttempts >= this.config.maxRecoveryAttempts) return false;
232
+ if (record.status === WidgetHealthStatus.FAILED) {
233
+ // Check if enough time has passed since last failure
234
+ const timeSinceFailure = Date.now() - (record.failedSince || 0);
235
+ return timeSinceFailure >= this.config.recoveryDelayMs;
236
+ }
237
+ return record.status !== WidgetHealthStatus.HEALTHY;
238
+ }
239
+
240
+ /**
241
+ * Reset health status for a widget
242
+ * @param {string} widgetId - Widget identifier
243
+ */
244
+ resetHealth(widgetId) {
245
+ this.healthStatus.delete(widgetId);
246
+ this.errorHistory.delete(widgetId);
247
+ }
248
+
249
+ /**
250
+ * Get summary statistics
251
+ * @returns {Object} Health statistics
252
+ */
253
+ getStats() {
254
+ const allHealth = Array.from(this.healthStatus.values());
255
+ return {
256
+ total: allHealth.length,
257
+ healthy: allHealth.filter(h => h.status === WidgetHealthStatus.HEALTHY).length,
258
+ degraded: allHealth.filter(h => h.status === WidgetHealthStatus.DEGRADED).length,
259
+ failed: allHealth.filter(h => h.status === WidgetHealthStatus.FAILED).length,
260
+ recovering: allHealth.filter(h => h.status === WidgetHealthStatus.RECOVERING).length,
261
+ totalErrors: allHealth.reduce((sum, h) => sum + h.totalErrors, 0),
262
+ };
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Widget error isolator - wraps widget operations with error handling
268
+ */
269
+ export class WidgetErrorIsolator {
270
+ constructor(config = {}) {
271
+ this.config = { ...DEFAULT_ISOLATION_CONFIG, ...config };
272
+ this.healthTracker = new WidgetHealthTracker(this.config);
273
+ this.failedWidgets = new Set();
274
+ this.recoveryTimers = new Map();
275
+ }
276
+
277
+ /**
278
+ * Create a timeout promise
279
+ * @private
280
+ */
281
+ _createTimeout(ms, message) {
282
+ return new Promise((_, reject) => {
283
+ setTimeout(() => reject(new Error(message)), ms);
284
+ });
285
+ }
286
+
287
+ /**
288
+ * Wrap a widget operation with timeout and error handling
289
+ * @private
290
+ */
291
+ async _wrapOperation(widgetId, operation, fn, timeoutMs, errorType) {
292
+ // Check if widget is already failed
293
+ const health = this.healthTracker.getHealth(widgetId);
294
+ if (health?.status === WidgetHealthStatus.FAILED) {
295
+ if (this.config.failSilently) {
296
+ return null;
297
+ }
298
+ throw new WidgetIsolatedError(widgetId, operation, new Error('Widget is in failed state'), errorType);
299
+ }
300
+
301
+ try {
302
+ // Race between operation and timeout
303
+ const result = await Promise.race([
304
+ fn(),
305
+ this._createTimeout(timeoutMs, `Operation timed out after ${timeoutMs}ms`),
306
+ ]);
307
+
308
+ // Record success
309
+ this.healthTracker.recordSuccess(widgetId);
310
+ this.failedWidgets.delete(widgetId);
311
+
312
+ return result;
313
+ } catch (error) {
314
+ // Record the error
315
+ this.healthTracker.recordError(widgetId, error, errorType);
316
+
317
+ if (this.config.logErrors) {
318
+ safeLogger.warn(`Widget '${widgetId}' ${operation} failed: ${error.message}`);
319
+ }
320
+
321
+ // Schedule recovery if appropriate
322
+ this._scheduleRecovery(widgetId);
323
+
324
+ if (this.config.failSilently) {
325
+ return null;
326
+ }
327
+
328
+ throw new WidgetIsolatedError(widgetId, operation, error, errorType);
329
+ }
330
+ }
331
+
332
+ /**
333
+ * Schedule a recovery attempt
334
+ * @private
335
+ */
336
+ _scheduleRecovery(widgetId) {
337
+ if (this.recoveryTimers.has(widgetId)) return;
338
+ if (!this.healthTracker.canRecover(widgetId)) return;
339
+
340
+ const timer = setTimeout(() => {
341
+ this.recoveryTimers.delete(widgetId);
342
+ this.healthTracker.markRecovering(widgetId);
343
+ if (this.config.logErrors) {
344
+ safeLogger.info(`Attempting recovery for widget '${widgetId}'`);
345
+ }
346
+ }, this.config.recoveryDelayMs);
347
+
348
+ this.recoveryTimers.set(widgetId, timer);
349
+ }
350
+
351
+ /**
352
+ * Wrap widget initialization
353
+ * @param {string} widgetId - Widget identifier
354
+ * @param {Function} initFn - Initialization function
355
+ * @returns {Promise<any>} Init result or null on failure
356
+ */
357
+ async wrapInit(widgetId, initFn) {
358
+ return this._wrapOperation(
359
+ widgetId,
360
+ 'init',
361
+ initFn,
362
+ this.config.initTimeoutMs,
363
+ WidgetErrorType.INIT_ERROR
364
+ );
365
+ }
366
+
367
+ /**
368
+ * Wrap widget creation
369
+ * @param {string} widgetId - Widget identifier
370
+ * @param {Function} createFn - Creation function
371
+ * @returns {Promise<any>} Create result or null on failure
372
+ */
373
+ async wrapCreate(widgetId, createFn) {
374
+ return this._wrapOperation(
375
+ widgetId,
376
+ 'create',
377
+ createFn,
378
+ this.config.createTimeoutMs,
379
+ WidgetErrorType.CREATE_ERROR
380
+ );
381
+ }
382
+
383
+ /**
384
+ * Wrap widget data fetching
385
+ * @param {string} widgetId - Widget identifier
386
+ * @param {Function} dataFn - Data fetching function
387
+ * @returns {Promise<any>} Data or null on failure
388
+ */
389
+ async wrapGetData(widgetId, dataFn) {
390
+ return this._wrapOperation(
391
+ widgetId,
392
+ 'getData',
393
+ dataFn,
394
+ this.config.dataTimeoutMs,
395
+ WidgetErrorType.DATA_ERROR
396
+ );
397
+ }
398
+
399
+ /**
400
+ * Wrap widget render
401
+ * @param {string} widgetId - Widget identifier
402
+ * @param {Function} renderFn - Render function
403
+ * @returns {Promise<any>} Render result or null on failure
404
+ */
405
+ async wrapRender(widgetId, renderFn) {
406
+ return this._wrapOperation(
407
+ widgetId,
408
+ 'render',
409
+ renderFn,
410
+ this.config.renderTimeoutMs,
411
+ WidgetErrorType.RENDER_ERROR
412
+ );
413
+ }
414
+
415
+ /**
416
+ * Wrap widget destruction
417
+ * @param {string} widgetId - Widget identifier
418
+ * @param {Function} destroyFn - Destroy function
419
+ * @returns {Promise<any>} Destroy result or null on failure
420
+ */
421
+ async wrapDestroy(widgetId, destroyFn) {
422
+ return this._wrapOperation(
423
+ widgetId,
424
+ 'destroy',
425
+ destroyFn,
426
+ this.config.destroyTimeoutMs,
427
+ WidgetErrorType.DESTROY_ERROR
428
+ );
429
+ }
430
+
431
+ /**
432
+ * Get health status for a widget
433
+ * @param {string} widgetId - Widget identifier
434
+ * @returns {Object|null} Health status
435
+ */
436
+ getHealth(widgetId) {
437
+ return this.healthTracker.getHealth(widgetId);
438
+ }
439
+
440
+ /**
441
+ * Get all health statuses
442
+ * @returns {Object} All health statuses
443
+ */
444
+ getAllHealth() {
445
+ return this.healthTracker.getAllHealth();
446
+ }
447
+
448
+ /**
449
+ * Check if a widget is operational (not failed)
450
+ * @param {string} widgetId - Widget identifier
451
+ * @returns {boolean} True if operational
452
+ */
453
+ isOperational(widgetId) {
454
+ const health = this.getHealth(widgetId);
455
+ return !health || health.status !== WidgetHealthStatus.FAILED;
456
+ }
457
+
458
+ /**
459
+ * Force reset a widget's health status
460
+ * @param {string} widgetId - Widget identifier
461
+ */
462
+ resetWidget(widgetId) {
463
+ this.healthTracker.resetHealth(widgetId);
464
+ this.failedWidgets.delete(widgetId);
465
+
466
+ const timer = this.recoveryTimers.get(widgetId);
467
+ if (timer) {
468
+ clearTimeout(timer);
469
+ this.recoveryTimers.delete(widgetId);
470
+ }
471
+ }
472
+
473
+ /**
474
+ * Get isolator statistics
475
+ * @returns {Object} Statistics
476
+ */
477
+ getStats() {
478
+ return {
479
+ ...this.healthTracker.getStats(),
480
+ failedWidgetCount: this.failedWidgets.size,
481
+ pendingRecoveries: this.recoveryTimers.size,
482
+ };
483
+ }
484
+
485
+ /**
486
+ * Shutdown the isolator and clear all timers
487
+ */
488
+ shutdown() {
489
+ for (const [widgetId, timer] of this.recoveryTimers) {
490
+ clearTimeout(timer);
491
+ }
492
+ this.recoveryTimers.clear();
493
+ this.failedWidgets.clear();
494
+ }
495
+ }
496
+
497
+ /**
498
+ * Execute multiple widget operations with isolation
499
+ * @param {Array<{widgetId: string, operation: Function}>} operations - Operations to execute
500
+ * @param {Object} config - Isolation configuration
501
+ * @returns {Promise<Array<{widgetId: string, success: boolean, result: any, error: Error|null}>>} Results
502
+ */
503
+ export async function executeWithIsolation(operations, config = {}) {
504
+ const isolator = new WidgetErrorIsolator({ ...config, failSilently: false });
505
+ const results = [];
506
+
507
+ await Promise.all(
508
+ operations.map(async ({ widgetId, operation, type = 'operation' }) => {
509
+ try {
510
+ const result = await isolator._wrapOperation(
511
+ widgetId,
512
+ type,
513
+ operation,
514
+ config.operationTimeoutMs || 10000,
515
+ WidgetErrorType.UNKNOWN_ERROR
516
+ );
517
+ results.push({ widgetId, success: true, result, error: null });
518
+ } catch (error) {
519
+ results.push({ widgetId, success: false, result: null, error });
520
+ }
521
+ })
522
+ );
523
+
524
+ return results;
525
+ }
526
+
527
+ // Singleton instance for simple usage
528
+ let defaultIsolator = null;
529
+
530
+ /**
531
+ * Get the default widget error isolator instance
532
+ * @param {Object} config - Configuration options
533
+ * @returns {WidgetErrorIsolator} Default isolator instance
534
+ */
535
+ export function getWidgetErrorIsolator(config = {}) {
536
+ if (!defaultIsolator) {
537
+ defaultIsolator = new WidgetErrorIsolator(config);
538
+ }
539
+ return defaultIsolator;
540
+ }
541
+
542
+ export default {
543
+ WidgetErrorIsolator,
544
+ WidgetHealthTracker,
545
+ WidgetIsolatedError,
546
+ WidgetHealthStatus,
547
+ WidgetErrorType,
548
+ executeWithIsolation,
549
+ getWidgetErrorIsolator,
550
+ DEFAULT_ISOLATION_CONFIG,
551
+ };