claw-dashboard 1.9.0 → 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 +5236 -566
  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,284 @@
1
+ /**
2
+ * Plugin Manifest Validator
3
+ * Validates plugin.json files against the JSON Schema
4
+ */
5
+
6
+ import { readFileSync, existsSync } from 'fs';
7
+ import { resolve, dirname, join } from 'path';
8
+ import { fileURLToPath } from 'url';
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = dirname(__filename);
12
+
13
+ // Load the schema
14
+ const SCHEMA_PATH = resolve(__dirname, '../schemas/plugin-manifest.json');
15
+ let schema = null;
16
+
17
+ try {
18
+ if (existsSync(SCHEMA_PATH)) {
19
+ const schemaContent = readFileSync(SCHEMA_PATH, 'utf8');
20
+ schema = JSON.parse(schemaContent);
21
+ }
22
+ } catch (err) {
23
+ console.warn(`Failed to load plugin schema: ${err.message}`);
24
+ }
25
+
26
+ /**
27
+ * Validation result type
28
+ * @typedef {Object} ValidationResult
29
+ * @property {boolean} valid - Whether the manifest is valid
30
+ * @property {string[]} errors - Array of error messages
31
+ * @property {string[]} warnings - Array of warning messages
32
+ * @property {string[]} info - Array of info messages
33
+ */
34
+
35
+ /**
36
+ * Validate a value against a schema property
37
+ * @private
38
+ * @param {*} value - The value to validate
39
+ * @param {Object} propSchema - The property schema
40
+ * @param {string} path - The current path in the object
41
+ * @returns {string[]} Array of error messages
42
+ */
43
+ function validateProperty(value, propSchema, path) {
44
+ const errors = [];
45
+
46
+ if (value === undefined) {
47
+ // Check if required (handled at object level)
48
+ return errors;
49
+ }
50
+
51
+ // Type validation
52
+ if (propSchema.type) {
53
+ const actualType = Array.isArray(value) ? 'array' : typeof value;
54
+ if (actualType !== propSchema.type) {
55
+ // Special case for numbers (can be strings that parse as numbers)
56
+ if (propSchema.type === 'number' && !isNaN(Number(value))) {
57
+ // OK
58
+ } else {
59
+ errors.push(`'${path}' must be of type ${propSchema.type}, got ${actualType}`);
60
+ return errors;
61
+ }
62
+ }
63
+ }
64
+
65
+ // String validations
66
+ if (propSchema.type === 'string' && typeof value === 'string') {
67
+ if (propSchema.minLength !== undefined && value.length < propSchema.minLength) {
68
+ errors.push(`'${path}' must be at least ${propSchema.minLength} characters`);
69
+ }
70
+ if (propSchema.maxLength !== undefined && value.length > propSchema.maxLength) {
71
+ errors.push(`'${path}' must be at most ${propSchema.maxLength} characters`);
72
+ }
73
+ if (propSchema.pattern) {
74
+ const regex = new RegExp(propSchema.pattern);
75
+ if (!regex.test(value)) {
76
+ errors.push(`'${path}' does not match required pattern: ${propSchema.pattern}`);
77
+ }
78
+ }
79
+ if (propSchema.enum && !propSchema.enum.includes(value)) {
80
+ errors.push(`'${path}' must be one of: ${propSchema.enum.join(', ')}`);
81
+ }
82
+ }
83
+
84
+ // Number validations
85
+ if (propSchema.type === 'number' && typeof value === 'number') {
86
+ if (propSchema.minimum !== undefined && value < propSchema.minimum) {
87
+ errors.push(`'${path}' must be at least ${propSchema.minimum}`);
88
+ }
89
+ if (propSchema.maximum !== undefined && value > propSchema.maximum) {
90
+ errors.push(`'${path}' must be at most ${propSchema.maximum}`);
91
+ }
92
+ }
93
+
94
+ // Array validations
95
+ if (propSchema.type === 'array' && Array.isArray(value)) {
96
+ if (propSchema.items) {
97
+ value.forEach((item, index) => {
98
+ const itemErrors = validateProperty(item, propSchema.items, `${path}[${index}]`);
99
+ errors.push(...itemErrors);
100
+ });
101
+ }
102
+ if (propSchema.uniqueItems) {
103
+ const uniqueSet = new Set(value.map(JSON.stringify));
104
+ if (uniqueSet.size !== value.length) {
105
+ errors.push(`'${path}' must have unique items`);
106
+ }
107
+ }
108
+ }
109
+
110
+ // Object validations
111
+ if (propSchema.type === 'object' && typeof value === 'object' && !Array.isArray(value)) {
112
+ if (propSchema.properties) {
113
+ for (const [key, subSchema] of Object.entries(propSchema.properties)) {
114
+ const subErrors = validateProperty(value[key], subSchema, `${path}.${key}`);
115
+ errors.push(...subErrors);
116
+ }
117
+ }
118
+ }
119
+
120
+ return errors;
121
+ }
122
+
123
+ /**
124
+ * Validate a plugin manifest against the schema
125
+ * @param {Object} manifest - The parsed plugin.json object
126
+ * @param {Object} options - Validation options
127
+ * @param {boolean} options.strict - Whether to fail on unknown properties
128
+ * @returns {ValidationResult} Validation result
129
+ */
130
+ export function validateManifest(manifest, options = {}) {
131
+ const { strict = false } = options;
132
+ const errors = [];
133
+ const warnings = [];
134
+ const info = [];
135
+
136
+ if (!manifest || typeof manifest !== 'object') {
137
+ return {
138
+ valid: false,
139
+ errors: ['Manifest must be a valid JSON object'],
140
+ warnings,
141
+ info
142
+ };
143
+ }
144
+
145
+ if (!schema) {
146
+ return {
147
+ valid: true,
148
+ errors: [],
149
+ warnings: ['Schema not loaded, skipping validation'],
150
+ info: ['Manifest structure was not validated']
151
+ };
152
+ }
153
+
154
+ // Check required fields
155
+ if (schema.required) {
156
+ for (const field of schema.required) {
157
+ if (!(field in manifest)) {
158
+ errors.push(`Missing required field: '${field}'`);
159
+ }
160
+ }
161
+ }
162
+
163
+ // Validate properties
164
+ if (schema.properties) {
165
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
166
+ if (key in manifest) {
167
+ const propErrors = validateProperty(manifest[key], propSchema, key);
168
+ errors.push(...propErrors);
169
+ }
170
+ }
171
+ }
172
+
173
+ // Check for unknown properties in strict mode
174
+ if (strict && schema.additionalProperties === false) {
175
+ const knownProps = Object.keys(schema.properties || {});
176
+ for (const key of Object.keys(manifest)) {
177
+ if (!knownProps.includes(key)) {
178
+ errors.push(`Unknown property: '${key}'`);
179
+ }
180
+ }
181
+ }
182
+
183
+ // Add warnings for deprecated fields
184
+ if (schema.properties) {
185
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
186
+ if (propSchema.deprecated && key in manifest) {
187
+ warnings.push(`Field '${key}' is deprecated: ${propSchema.description}`);
188
+ }
189
+ }
190
+ }
191
+
192
+ // Add info messages for optional fields with defaults
193
+ if (schema.properties) {
194
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
195
+ if (!(key in manifest) && 'default' in propSchema) {
196
+ info.push(`Using default value for '${key}': ${JSON.stringify(propSchema.default)}`);
197
+ }
198
+ }
199
+ }
200
+
201
+ return {
202
+ valid: errors.length === 0,
203
+ errors,
204
+ warnings,
205
+ info
206
+ };
207
+ }
208
+
209
+ /**
210
+ * Validate a plugin manifest from a file path
211
+ * @param {string} filePath - Path to the plugin.json file
212
+ * @param {Object} options - Validation options
213
+ * @returns {ValidationResult} Validation result
214
+ */
215
+ export function validateManifestFile(filePath, options = {}) {
216
+ const resolvedPath = resolve(filePath);
217
+
218
+ if (!existsSync(resolvedPath)) {
219
+ return {
220
+ valid: false,
221
+ errors: [`File not found: ${resolvedPath}`],
222
+ warnings: [],
223
+ info: []
224
+ };
225
+ }
226
+
227
+ let manifest;
228
+ try {
229
+ const content = readFileSync(resolvedPath, 'utf8');
230
+ manifest = JSON.parse(content);
231
+ } catch (err) {
232
+ return {
233
+ valid: false,
234
+ errors: [`Failed to parse JSON: ${err.message}`],
235
+ warnings: [],
236
+ info: []
237
+ };
238
+ }
239
+
240
+ return validateManifest(manifest, options);
241
+ }
242
+
243
+ /**
244
+ * Format validation results for display
245
+ * @param {ValidationResult} result - The validation result
246
+ * @param {string} pluginName - Optional plugin name for context
247
+ * @returns {string} Formatted output
248
+ */
249
+ export function formatValidationResult(result, pluginName = '') {
250
+ const lines = [];
251
+ const name = pluginName ? ` ${pluginName} ` : '';
252
+
253
+ if (result.valid) {
254
+ lines.push(`✓ Plugin${name}is valid`);
255
+ } else {
256
+ lines.push(`✗ Plugin${name}validation failed`);
257
+ }
258
+
259
+ if (result.errors.length > 0) {
260
+ lines.push('');
261
+ lines.push('Errors:');
262
+ result.errors.forEach(err => lines.push(` • ${err}`));
263
+ }
264
+
265
+ if (result.warnings.length > 0) {
266
+ lines.push('');
267
+ lines.push('Warnings:');
268
+ result.warnings.forEach(warn => lines.push(` • ${warn}`));
269
+ }
270
+
271
+ if (result.info.length > 0) {
272
+ lines.push('');
273
+ lines.push('Info:');
274
+ result.info.forEach(i => lines.push(` • ${i}`));
275
+ }
276
+
277
+ return lines.join('\n');
278
+ }
279
+
280
+ export default {
281
+ validateManifest,
282
+ validateManifestFile,
283
+ formatValidationResult
284
+ };
package/src/retry.js ADDED
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Retry utility for OpenClaw API calls
3
+ * Implements exponential backoff with configurable options
4
+ */
5
+
6
+ import logger from './logger.js';
7
+ import config from './config.js';
8
+
9
+ // Default retry configuration
10
+ const DEFAULT_OPTIONS = config.DEFAULT_RETRY_OPTIONS;
11
+
12
+ /**
13
+ * Sleep for specified milliseconds
14
+ * @param {number} ms - Milliseconds to sleep
15
+ * @returns {Promise} Resolves after delay
16
+ */
17
+ function sleep(ms) {
18
+ return new Promise(resolve => setTimeout(resolve, ms));
19
+ }
20
+
21
+ /**
22
+ * Calculate delay with exponential backoff
23
+ * @param {number} attempt - Current attempt number (0-indexed)
24
+ * @param {object} options - Retry options
25
+ * @returns {number} Delay in milliseconds
26
+ */
27
+ function calculateDelay(attempt, options) {
28
+ const delay = Math.min(
29
+ options.initialDelay * Math.pow(options.backoffMultiplier, attempt),
30
+ options.maxDelay
31
+ );
32
+
33
+ // Add jitter (±10%) to prevent thundering herd
34
+ const jitter = delay * 0.1 * (Math.random() * 2 - 1);
35
+ return Math.floor(delay + jitter);
36
+ }
37
+
38
+ /**
39
+ * Check if error is retryable
40
+ * @param {Error} error - Error object
41
+ * @param {object} options - Retry options
42
+ * @returns {boolean} Whether the error is retryable
43
+ */
44
+ function isRetryableError(error, options) {
45
+ // Check for retryable error codes
46
+ if (error.code && options.retryableErrors.includes(error.code)) {
47
+ return true;
48
+ }
49
+
50
+ // Check for retryable HTTP status codes (if response exists)
51
+ if (error.status && options.retryableStatuses.includes(error.status)) {
52
+ return true;
53
+ }
54
+
55
+ // Check error message for common retryable patterns
56
+ const message = error.message || '';
57
+ const retryablePatterns = [
58
+ /connection refused/i,
59
+ /timeout/i,
60
+ /network/i,
61
+ /econnreset/i,
62
+ /eai_again/i,
63
+ /temporary failure/i,
64
+ /service unavailable/i,
65
+ /internal server error/i,
66
+ /bad gateway/i,
67
+ /gateway timeout/i,
68
+ ];
69
+
70
+ return retryablePatterns.some(pattern => pattern.test(message));
71
+ }
72
+
73
+ /**
74
+ * Wrap a function with retry logic
75
+ * @param {Function} fn - Function to wrap (must return a Promise)
76
+ * @param {object} options - Retry options
77
+ * @returns {Function} Wrapped function
78
+ */
79
+ function withRetry(fn, options = {}) {
80
+ const opts = { ...DEFAULT_OPTIONS, ...options };
81
+
82
+ return async function retryWrapper(...args) {
83
+ let lastError;
84
+
85
+ for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
86
+ try {
87
+ // Execute the function
88
+ const result = await fn(...args);
89
+
90
+ // Log successful retry if this wasn't the first attempt
91
+ if (attempt > 0) {
92
+ logger.info(`[RETRY] Operation succeeded after ${attempt} retries`);
93
+ }
94
+
95
+ return result;
96
+
97
+ } catch (error) {
98
+ lastError = error;
99
+
100
+ // Check if we should retry
101
+ const isLastAttempt = attempt >= opts.maxRetries;
102
+ const shouldRetry = !isLastAttempt && isRetryableError(error, opts);
103
+
104
+ if (!shouldRetry) {
105
+ // Log the final failure
106
+ logger.error(`[RETRY] Operation failed after ${attempt} attempts: ${error.message}`);
107
+ throw error;
108
+ }
109
+
110
+ // Calculate delay and wait
111
+ const delay = calculateDelay(attempt, opts);
112
+ logger.warn(`[RETRY] Attempt ${attempt + 1}/${opts.maxRetries + 1} failed: ${error.message}. Retrying in ${Math.round(delay)}ms...`);
113
+ await sleep(delay);
114
+ }
115
+ }
116
+
117
+ // This should never be reached, but just in case
118
+ throw lastError;
119
+ };
120
+ }
121
+
122
+ /**
123
+ * Retry a function until it succeeds or timeout
124
+ * @param {Function} fn - Function to retry
125
+ * @param {number} timeout - Maximum time to keep retrying (ms)
126
+ * @param {number} interval - Time between retries (ms)
127
+ * @returns {Promise} Result of function
128
+ */
129
+ async function retryUntil(fn, timeout = config.RETRY.TIMEOUT, interval = config.RETRY.INTERVAL) {
130
+ const startTime = Date.now();
131
+
132
+ while (true) {
133
+ try {
134
+ return await fn();
135
+ } catch (error) {
136
+ if (Date.now() - startTime >= timeout) {
137
+ throw new Error(`Retry timeout after ${timeout}ms: ${error.message}`);
138
+ }
139
+ logger.warn(`[RETRY] Waiting for condition: ${error.message}`);
140
+ await sleep(interval);
141
+ }
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Create a retryable HTTP request wrapper
147
+ * @param {Function} fetchFn - Fetch function (fetch, axios, etc.)
148
+ * @param {object} options - Retry options
149
+ * @returns {Function} Wrapped fetch function
150
+ */
151
+ function createRetryableFetch(fetchFn, options = {}) {
152
+ const opts = { ...DEFAULT_OPTIONS, ...options };
153
+
154
+ return async function retryableFetch(url, fetchOptions = {}) {
155
+ let lastError;
156
+
157
+ for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
158
+ try {
159
+ const response = await fetchFn(url, fetchOptions);
160
+
161
+ // Check if response status is retryable
162
+ if (opts.retryableStatuses.includes(response.status)) {
163
+ const error = new Error(`HTTP ${response.status}: ${response.statusText}`);
164
+ error.status = response.status;
165
+ throw error;
166
+ }
167
+
168
+ // Return response for non-retryable statuses (caller handles 2xx, 4xx)
169
+ return response;
170
+
171
+ } catch (error) {
172
+ lastError = error;
173
+
174
+ // Check if we should retry
175
+ const isLastAttempt = attempt >= opts.maxRetries;
176
+ const shouldRetry = !isLastAttempt && isRetryableError(error, opts);
177
+
178
+ if (!shouldRetry) {
179
+ throw error;
180
+ }
181
+
182
+ // Calculate delay and wait
183
+ const delay = calculateDelay(attempt, opts);
184
+ logger.warn(`[RETRY] HTTP request attempt ${attempt + 1}/${opts.maxRetries + 1} failed: ${error.message}. Retrying in ${Math.round(delay)}ms...`);
185
+ await sleep(delay);
186
+ }
187
+ }
188
+
189
+ throw lastError;
190
+ };
191
+ }
192
+
193
+ /**
194
+ * Batch retry multiple independent operations
195
+ * @param {Array<Function>} fns - Array of functions to execute
196
+ * @param {object} options - Retry options
197
+ * @returns {Promise<Array>} Array of results
198
+ */
199
+ async function retryBatch(fns, options = {}) {
200
+ const opts = { ...DEFAULT_OPTIONS, ...options };
201
+
202
+ // Wrap each function with retry
203
+ const wrappedFns = fns.map(fn => withRetry(fn, opts));
204
+
205
+ // Execute all in parallel
206
+ return Promise.allSettled(wrappedFns.map(fn => fn()));
207
+ }
208
+
209
+ export default {
210
+ DEFAULT_OPTIONS,
211
+ withRetry,
212
+ retryUntil,
213
+ createRetryableFetch,
214
+ retryBatch,
215
+ sleep,
216
+ calculateDelay,
217
+ isRetryableError
218
+ };