claw-dashboard 1.9.0 → 2.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.
- package/.c8rc.json +38 -0
- package/.dockerignore +68 -0
- package/.github/dependabot.yml +45 -0
- package/.github/pull_request_template.md +39 -0
- package/.github/workflows/ci.yml +147 -0
- package/.github/workflows/release.yml +40 -0
- package/.github/workflows/security.yml +84 -0
- package/.husky/pre-commit +1 -0
- package/.planning/codebase/ARCHITECTURE.md +206 -0
- package/.planning/codebase/INTEGRATIONS.md +150 -0
- package/.planning/codebase/STACK.md +122 -0
- package/.planning/codebase/STRUCTURE.md +201 -0
- package/CHANGELOG.md +293 -0
- package/CONTRIBUTING.md +378 -0
- package/Dockerfile +54 -0
- package/FEATURES.md +195 -21
- package/README.md +83 -6
- package/TODO.md +28 -0
- package/build-cjs.js +127 -0
- package/cjs-shim.js +13 -0
- package/dist/clawdash +1729 -0
- package/dist/clawdash.meta.json +2236 -0
- package/dist/widgets.cjs +6511 -0
- package/docker-compose.yml +67 -0
- package/docs/API.md +1050 -0
- package/docs/PLUGINS.md +1504 -0
- package/esbuild.config.js +158 -0
- package/eslint.config.js +56 -0
- package/examples/plugins/README.md +122 -0
- package/examples/plugins/api-status/index.js +294 -0
- package/examples/plugins/api-status/plugin.json +19 -0
- package/examples/plugins/hello-world/index.js +104 -0
- package/examples/plugins/hello-world/plugin.json +15 -0
- package/examples/plugins/system-metrics-chart/index.js +339 -0
- package/examples/plugins/system-metrics-chart/plugin.json +18 -0
- package/examples/plugins/weather-widget/index.js +136 -0
- package/examples/plugins/weather-widget/plugin.json +19 -0
- package/index.cjs +23005 -0
- package/index.js +5285 -566
- package/jest.config.js +11 -0
- package/man/clawdash.1 +276 -0
- package/package.json +54 -6
- package/schemas/plugin-manifest.json +128 -0
- package/scripts/release.js +595 -0
- package/src/alerts.js +693 -0
- package/src/auto-save.js +584 -0
- package/src/cache.js +390 -0
- package/src/checksum.js +146 -0
- package/src/cli/args.js +110 -0
- package/src/cli/export-schedule.js +423 -0
- package/src/cli/export-snapshot.js +138 -0
- package/src/cli/help.js +69 -0
- package/src/cli/import-snapshot.js +230 -0
- package/src/cli/index.js +14 -0
- package/src/cli/list-templates.js +69 -0
- package/src/cli/validate-config.js +76 -0
- package/src/cli/validate-plugin.js +187 -0
- package/src/cli/version.js +15 -0
- package/src/config-validator.js +586 -0
- package/src/config-watcher.js +401 -0
- package/src/config.js +684 -0
- package/src/container-detector.js +499 -0
- package/src/database.js +734 -0
- package/src/differential-render.js +327 -0
- package/src/errors.js +169 -0
- package/src/export-scheduler.js +581 -0
- package/src/gateway-manager.js +685 -0
- package/src/hints.js +371 -0
- package/src/loading-states.js +509 -0
- package/src/logger.js +185 -0
- package/src/memory-pressure.js +467 -0
- package/src/performance-monitor.js +374 -0
- package/src/plugin-errors.js +586 -0
- package/src/plugin-manifest-validator.js +219 -0
- package/src/plugin-reload.js +481 -0
- package/src/plugin-scaffold.js +1934 -0
- package/src/plugin-validator.js +284 -0
- package/src/retry.js +218 -0
- package/src/security.js +860 -0
- package/src/snapshot.js +372 -0
- package/src/splash.js +115 -0
- package/src/theme-selector.js +411 -0
- package/src/themes.js +874 -0
- package/src/transitions.js +534 -0
- package/src/types.d.ts +174 -0
- package/src/validation.js +926 -0
- package/src/web-server.js +885 -0
- package/src/widgets/builtin-widgets.js +1056 -0
- package/src/widgets/config-processor.js +401 -0
- package/src/widgets/dependency-resolver.js +596 -0
- package/src/widgets/index.js +79 -0
- package/src/widgets/plugin-api.js +845 -0
- package/src/widgets/widget-error-boundary.js +773 -0
- package/src/widgets/widget-error-isolation.js +551 -0
- package/src/widgets/widget-loader.js +1437 -0
- package/src/workers/system-worker.js +130 -0
- package/src/workers/worker-pool.js +633 -0
- package/tests/alerts.test.js +437 -0
- package/tests/auto-save.test.js +529 -0
- package/tests/cache.test.js +317 -0
- package/tests/cli.test.js +351 -0
- package/tests/command-palette.test.js +320 -0
- package/tests/config-processor.test.js +452 -0
- package/tests/config-validator.test.js +710 -0
- package/tests/config-watcher.test.js +594 -0
- package/tests/config.test.js +755 -0
- package/tests/database.test.js +438 -0
- package/tests/errors.test.js +189 -0
- package/tests/example-plugins.test.js +624 -0
- package/tests/integration.test.js +1321 -0
- package/tests/loading-states.test.js +300 -0
- package/tests/manifest-validation-on-load.test.js +671 -0
- package/tests/performance-monitor.test.js +190 -0
- package/tests/plugin-api-rate-limit.test.js +302 -0
- package/tests/plugin-errors.test.js +311 -0
- package/tests/plugin-lifecycle-e2e.test.js +1036 -0
- package/tests/plugin-reload.test.js +764 -0
- package/tests/rate-limiter.test.js +539 -0
- package/tests/retry.test.js +308 -0
- package/tests/security.test.js +411 -0
- package/tests/settings-modal.test.js +226 -0
- package/tests/theme-selector.test.js +57 -0
- package/tests/utils.js +242 -0
- package/tests/utils.test.js +317 -0
- package/tests/validate-plugin-cli.test.js +359 -0
- package/tests/validation.test.js +837 -0
- package/tests/web-server.test.js +646 -0
- package/tests/widget-arrange-mode.test.js +183 -0
- package/tests/widget-config-hot-reload.test.js +465 -0
- package/tests/widget-dependency.test.js +591 -0
- package/tests/widget-error-boundary.test.js +749 -0
- package/tests/widget-error-isolation.test.js +469 -0
- package/tests/widget-integration.test.js +1147 -0
- package/tests/widget-refresh-intervals.test.js +284 -0
- 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
|
+
};
|