@mooncompany/uplink-chat 0.5.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.
Potentially problematic release.
This version of @mooncompany/uplink-chat might be problematic. Click here for more details.
- package/LICENSE +21 -0
- package/README.md +185 -0
- package/bin/uplink.js +279 -0
- package/middleware/error-handler.js +69 -0
- package/package.json +93 -0
- package/public/css/agents.36b98c0f.css +1469 -0
- package/public/css/agents.css +1469 -0
- package/public/css/app.a6a7f8f5.css +2731 -0
- package/public/css/app.css +2731 -0
- package/public/css/artifacts.css +444 -0
- package/public/css/commands.css +55 -0
- package/public/css/connection.css +131 -0
- package/public/css/dashboard.css +233 -0
- package/public/css/developer.css +328 -0
- package/public/css/files.css +123 -0
- package/public/css/markdown.css +156 -0
- package/public/css/message-actions.css +278 -0
- package/public/css/mobile.css +614 -0
- package/public/css/panels-unified.css +483 -0
- package/public/css/premium.css +415 -0
- package/public/css/realtime.css +189 -0
- package/public/css/satellites.css +401 -0
- package/public/css/shortcuts.css +185 -0
- package/public/css/split-view.4def0262.css +673 -0
- package/public/css/split-view.css +673 -0
- package/public/css/theme-generator.css +391 -0
- package/public/css/themes.css +387 -0
- package/public/css/timestamps.css +54 -0
- package/public/css/variables.css +78 -0
- package/public/dist/bundle.b55050c4.js +15757 -0
- package/public/favicon.svg +24 -0
- package/public/img/agents/ada.png +0 -0
- package/public/img/agents/clarice.png +0 -0
- package/public/img/agents/dennis-nedry.png +0 -0
- package/public/img/agents/elliot-alderson.png +0 -0
- package/public/img/agents/main.png +0 -0
- package/public/img/agents/scotty.png +0 -0
- package/public/img/agents/top-flight-security.png +0 -0
- package/public/index.html +1083 -0
- package/public/js/agents-data.js +234 -0
- package/public/js/agents-ui.js +72 -0
- package/public/js/agents.js +1525 -0
- package/public/js/app.js +79 -0
- package/public/js/appearance-settings.js +111 -0
- package/public/js/artifacts.js +432 -0
- package/public/js/audio-queue.js +168 -0
- package/public/js/bootstrap.js +54 -0
- package/public/js/chat.js +1211 -0
- package/public/js/commands.js +581 -0
- package/public/js/connection-api.js +121 -0
- package/public/js/connection.js +1231 -0
- package/public/js/context-tracker.js +271 -0
- package/public/js/core.js +172 -0
- package/public/js/dashboard.js +452 -0
- package/public/js/developer.js +432 -0
- package/public/js/encryption.js +124 -0
- package/public/js/errors.js +122 -0
- package/public/js/event-bus.js +77 -0
- package/public/js/fetch-utils.js +171 -0
- package/public/js/file-handler.js +229 -0
- package/public/js/files.js +352 -0
- package/public/js/gateway-chat.js +538 -0
- package/public/js/logger.js +112 -0
- package/public/js/markdown.js +190 -0
- package/public/js/message-actions.js +431 -0
- package/public/js/message-renderer.js +288 -0
- package/public/js/missed-messages.js +235 -0
- package/public/js/mobile-debug.js +95 -0
- package/public/js/notifications.js +367 -0
- package/public/js/offline-queue.js +178 -0
- package/public/js/onboarding.js +543 -0
- package/public/js/panels.js +156 -0
- package/public/js/premium.js +412 -0
- package/public/js/realtime-voice.js +844 -0
- package/public/js/satellite-sync.js +256 -0
- package/public/js/satellite-ui.js +175 -0
- package/public/js/satellites.js +1516 -0
- package/public/js/settings.js +1087 -0
- package/public/js/shortcuts.js +381 -0
- package/public/js/split-chat.js +1234 -0
- package/public/js/split-resize.js +211 -0
- package/public/js/splitview.js +340 -0
- package/public/js/storage.js +408 -0
- package/public/js/streaming-handler.js +324 -0
- package/public/js/stt-settings.js +316 -0
- package/public/js/theme-generator.js +661 -0
- package/public/js/themes.js +164 -0
- package/public/js/timestamps.js +198 -0
- package/public/js/tts-settings.js +575 -0
- package/public/js/ui.js +267 -0
- package/public/js/update-notifier.js +143 -0
- package/public/js/utils/constants.js +165 -0
- package/public/js/utils/sanitize.js +93 -0
- package/public/js/utils/sse-parser.js +195 -0
- package/public/js/voice.js +883 -0
- package/public/manifest.json +58 -0
- package/public/moon_texture.jpg +0 -0
- package/public/sw.js +221 -0
- package/public/three.min.js +6 -0
- package/server/channel.js +529 -0
- package/server/chat.js +270 -0
- package/server/config-store.js +362 -0
- package/server/config.js +159 -0
- package/server/context.js +131 -0
- package/server/gateway-commands.js +211 -0
- package/server/gateway-proxy.js +318 -0
- package/server/index.js +22 -0
- package/server/logger.js +89 -0
- package/server/middleware/auth.js +188 -0
- package/server/middleware.js +218 -0
- package/server/openclaw-discover.js +308 -0
- package/server/premium/index.js +156 -0
- package/server/premium/license.js +140 -0
- package/server/realtime/bridge.js +837 -0
- package/server/realtime/index.js +349 -0
- package/server/realtime/tts-stream.js +446 -0
- package/server/routes/agents.js +564 -0
- package/server/routes/artifacts.js +174 -0
- package/server/routes/chat.js +311 -0
- package/server/routes/config-settings.js +345 -0
- package/server/routes/config.js +603 -0
- package/server/routes/files.js +307 -0
- package/server/routes/index.js +18 -0
- package/server/routes/media.js +451 -0
- package/server/routes/missed-messages.js +107 -0
- package/server/routes/premium.js +75 -0
- package/server/routes/push.js +156 -0
- package/server/routes/satellite.js +406 -0
- package/server/routes/status.js +251 -0
- package/server/routes/stt.js +35 -0
- package/server/routes/voice.js +260 -0
- package/server/routes/webhooks.js +203 -0
- package/server/routes.js +206 -0
- package/server/runtime-config.js +336 -0
- package/server/share.js +305 -0
- package/server/stt/faster-whisper.js +72 -0
- package/server/stt/groq.js +51 -0
- package/server/stt/index.js +196 -0
- package/server/stt/openai.js +49 -0
- package/server/sync.js +244 -0
- package/server/tailscale-https.js +175 -0
- package/server/tts.js +646 -0
- package/server/update-checker.js +172 -0
- package/server/utils/filename.js +129 -0
- package/server/utils.js +147 -0
- package/server/watchdog.js +318 -0
- package/server/websocket/broadcast.js +359 -0
- package/server/websocket/connections.js +339 -0
- package/server/websocket/index.js +215 -0
- package/server/websocket/routing.js +277 -0
- package/server/websocket/sync.js +102 -0
- package/server.js +404 -0
- package/utils/detect-tool-usage.js +93 -0
- package/utils/errors.js +158 -0
- package/utils/html-escape.js +84 -0
- package/utils/id-sanitize.js +94 -0
- package/utils/response.js +130 -0
- package/utils/with-retry.js +105 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML Escape Utility (Server-side)
|
|
3
|
+
* Single source of truth for HTML escaping on the server
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// HTML entity map for escaping
|
|
7
|
+
const HTML_ENTITIES = {
|
|
8
|
+
'&': '&',
|
|
9
|
+
'<': '<',
|
|
10
|
+
'>': '>',
|
|
11
|
+
'"': '"',
|
|
12
|
+
"'": ''',
|
|
13
|
+
'/': '/',
|
|
14
|
+
'`': '`',
|
|
15
|
+
'=': '='
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Escape HTML special characters to prevent XSS
|
|
20
|
+
*
|
|
21
|
+
* @param {string} str - The string to escape
|
|
22
|
+
* @returns {string} - Escaped string safe for HTML insertion
|
|
23
|
+
*/
|
|
24
|
+
export function escapeHtml(str) {
|
|
25
|
+
if (typeof str !== 'string') {
|
|
26
|
+
return '';
|
|
27
|
+
}
|
|
28
|
+
return str.replace(/[&<>"'`=/]/g, char => HTML_ENTITIES[char]);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Escape HTML and convert newlines to <br> tags
|
|
33
|
+
*
|
|
34
|
+
* @param {string} str - The string to escape
|
|
35
|
+
* @returns {string} - Escaped string with line breaks
|
|
36
|
+
*/
|
|
37
|
+
export function escapeHtmlWithBreaks(str) {
|
|
38
|
+
return escapeHtml(str).replace(/\n/g, '<br>');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Escape a string for safe insertion into JavaScript
|
|
43
|
+
* Use when embedding strings in inline <script> tags
|
|
44
|
+
*
|
|
45
|
+
* @param {string} str - The string to escape
|
|
46
|
+
* @returns {string} - Escaped string safe for JS context
|
|
47
|
+
*/
|
|
48
|
+
export function escapeForJS(str) {
|
|
49
|
+
if (typeof str !== 'string') {
|
|
50
|
+
return '';
|
|
51
|
+
}
|
|
52
|
+
return str
|
|
53
|
+
.replace(/\\/g, '\\\\')
|
|
54
|
+
.replace(/'/g, "\\'")
|
|
55
|
+
.replace(/"/g, '\\"')
|
|
56
|
+
.replace(/\n/g, '\\n')
|
|
57
|
+
.replace(/\r/g, '\\r')
|
|
58
|
+
.replace(/<\/script>/gi, '<\\/script>');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Escape a string for safe use in HTML attributes
|
|
63
|
+
*
|
|
64
|
+
* @param {string} str - The string to escape
|
|
65
|
+
* @returns {string} - Escaped string safe for attribute values
|
|
66
|
+
*/
|
|
67
|
+
export function escapeAttribute(str) {
|
|
68
|
+
if (typeof str !== 'string') {
|
|
69
|
+
return '';
|
|
70
|
+
}
|
|
71
|
+
return str
|
|
72
|
+
.replace(/&/g, '&')
|
|
73
|
+
.replace(/"/g, '"')
|
|
74
|
+
.replace(/'/g, ''')
|
|
75
|
+
.replace(/</g, '<')
|
|
76
|
+
.replace(/>/g, '>');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export default {
|
|
80
|
+
escapeHtml,
|
|
81
|
+
escapeHtmlWithBreaks,
|
|
82
|
+
escapeForJS,
|
|
83
|
+
escapeAttribute
|
|
84
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ID Sanitization Utility
|
|
3
|
+
* Validates and sanitizes identifiers to prevent injection attacks
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Sanitize an ID string (alphanumeric + hyphen/underscore only)
|
|
8
|
+
* @param {string} id - Raw ID
|
|
9
|
+
* @param {number} maxLength - Maximum allowed length (default 64)
|
|
10
|
+
* @returns {string} - Sanitized ID
|
|
11
|
+
*/
|
|
12
|
+
export function sanitizeId(id, maxLength = 64) {
|
|
13
|
+
if (typeof id !== 'string') return '';
|
|
14
|
+
return id.replace(/[^a-zA-Z0-9_-]/g, '').substring(0, maxLength);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Sanitize a satellite ID
|
|
19
|
+
* @param {string} id - Raw satellite ID
|
|
20
|
+
* @returns {string} - Sanitized satellite ID (default 'main' if invalid)
|
|
21
|
+
*/
|
|
22
|
+
export function sanitizeSatelliteId(id) {
|
|
23
|
+
const sanitized = sanitizeId(id, 32);
|
|
24
|
+
return sanitized || 'main';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Sanitize a share ID (alphanumeric only, max 12 chars)
|
|
29
|
+
* @param {string} id - Raw share ID
|
|
30
|
+
* @returns {{ valid: boolean, sanitized: string, error: string|null }}
|
|
31
|
+
*/
|
|
32
|
+
export function sanitizeShareId(id) {
|
|
33
|
+
if (typeof id !== 'string' || !id) {
|
|
34
|
+
return { valid: false, sanitized: '', error: 'shareId must be a non-empty string' };
|
|
35
|
+
}
|
|
36
|
+
const sanitized = id.replace(/[^a-zA-Z0-9]/g, '').substring(0, 12);
|
|
37
|
+
if (!sanitized) {
|
|
38
|
+
return { valid: false, sanitized: '', error: 'shareId contains no valid characters' };
|
|
39
|
+
}
|
|
40
|
+
return { valid: true, sanitized, error: null };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Sanitize a session user string
|
|
45
|
+
* @param {string} user - Raw session user
|
|
46
|
+
* @returns {string} - Sanitized session user
|
|
47
|
+
*/
|
|
48
|
+
export function sanitizeSessionUser(user) {
|
|
49
|
+
if (typeof user !== 'string') return '';
|
|
50
|
+
// Allow alphanumeric, hyphen, underscore, colon (for satellite suffix)
|
|
51
|
+
return user.replace(/[^a-zA-Z0-9_:-]/g, '').substring(0, 64);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Validate that an ID matches expected format
|
|
56
|
+
* @param {string} id - ID to validate
|
|
57
|
+
* @param {RegExp} pattern - Pattern to match
|
|
58
|
+
* @returns {boolean} - True if valid
|
|
59
|
+
*/
|
|
60
|
+
export function isValidId(id, pattern = /^[a-zA-Z0-9_-]+$/) {
|
|
61
|
+
return typeof id === 'string' && pattern.test(id);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Sanitize a sync ID (alphanumeric + hyphen, max 64 chars)
|
|
66
|
+
* @param {string} id - Raw sync ID
|
|
67
|
+
* @returns {{ valid: boolean, sanitized: string, error: string|null }}
|
|
68
|
+
*/
|
|
69
|
+
export function sanitizeSyncId(id) {
|
|
70
|
+
if (typeof id !== 'string' || !id) {
|
|
71
|
+
return { valid: false, sanitized: '', error: 'syncId must be a non-empty string' };
|
|
72
|
+
}
|
|
73
|
+
const sanitized = id.replace(/[^a-zA-Z0-9-]/g, '').substring(0, 64);
|
|
74
|
+
if (!sanitized) {
|
|
75
|
+
return { valid: false, sanitized: '', error: 'syncId contains no valid characters' };
|
|
76
|
+
}
|
|
77
|
+
return { valid: true, sanitized, error: null };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Parse a numeric parameter with bounds checking
|
|
82
|
+
* @param {any} value - Value to parse
|
|
83
|
+
* @param {number} defaultVal - Default value if invalid
|
|
84
|
+
* @param {number} min - Minimum allowed value
|
|
85
|
+
* @param {number} max - Maximum allowed value
|
|
86
|
+
* @returns {number} - Parsed and bounded number
|
|
87
|
+
*/
|
|
88
|
+
export function parseNumericParam(value, defaultVal = 0, min = 0, max = Infinity) {
|
|
89
|
+
const parsed = parseInt(value, 10);
|
|
90
|
+
if (isNaN(parsed)) return defaultVal;
|
|
91
|
+
return Math.min(Math.max(parsed, min), max);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export default { sanitizeId, sanitizeSatelliteId, sanitizeShareId, sanitizeSessionUser, isValidId, sanitizeSyncId, parseNumericParam };
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standardized Response Utilities
|
|
3
|
+
* Ensures consistent API responses
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { sanitizeErrorMessage, createErrorResponse, AppError } from './errors.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Send a success response
|
|
10
|
+
* @param {Response} res - Express response
|
|
11
|
+
* @param {Object} data - Response data
|
|
12
|
+
* @param {number} statusCode - HTTP status code (default 200)
|
|
13
|
+
*/
|
|
14
|
+
export function sendSuccess(res, data = {}, statusCode = 200) {
|
|
15
|
+
res.status(statusCode).json({
|
|
16
|
+
ok: true,
|
|
17
|
+
...data
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Send an error response (sanitized)
|
|
23
|
+
* @param {Response} res - Express response
|
|
24
|
+
* @param {Error|string} error - Error or message
|
|
25
|
+
* @param {number} statusCode - HTTP status code (default 500)
|
|
26
|
+
*/
|
|
27
|
+
export function sendError(res, error, statusCode = 500) {
|
|
28
|
+
// If it's an AppError, use its status code
|
|
29
|
+
if (error instanceof AppError) {
|
|
30
|
+
statusCode = error.statusCode;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const message = error instanceof Error
|
|
34
|
+
? sanitizeErrorMessage(error)
|
|
35
|
+
: String(error);
|
|
36
|
+
|
|
37
|
+
res.status(statusCode).json({
|
|
38
|
+
error: true,
|
|
39
|
+
message,
|
|
40
|
+
...(error instanceof AppError && { code: error.code })
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Send a validation error (400)
|
|
46
|
+
* @param {Response} res - Express response
|
|
47
|
+
* @param {string} message - Error message
|
|
48
|
+
*/
|
|
49
|
+
export function sendValidationError(res, message = 'Invalid input') {
|
|
50
|
+
sendError(res, message, 400);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Send an authentication error (401)
|
|
55
|
+
* @param {Response} res - Express response
|
|
56
|
+
* @param {string} message - Error message
|
|
57
|
+
*/
|
|
58
|
+
export function sendAuthError(res, message = 'Authentication required') {
|
|
59
|
+
sendError(res, message, 401);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Send a forbidden error (403)
|
|
64
|
+
* @param {Response} res - Express response
|
|
65
|
+
* @param {string} message - Error message
|
|
66
|
+
*/
|
|
67
|
+
export function sendForbiddenError(res, message = 'Access denied') {
|
|
68
|
+
sendError(res, message, 403);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Send a not found error (404)
|
|
73
|
+
* @param {Response} res - Express response
|
|
74
|
+
* @param {string} message - Error message
|
|
75
|
+
*/
|
|
76
|
+
export function sendNotFoundError(res, message = 'Resource not found') {
|
|
77
|
+
sendError(res, message, 404);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Send a rate limit error (429)
|
|
82
|
+
* @param {Response} res - Express response
|
|
83
|
+
* @param {string} message - Error message
|
|
84
|
+
*/
|
|
85
|
+
export function sendRateLimitError(res, message = 'Too many requests') {
|
|
86
|
+
sendError(res, message, 429);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Send a server error (500) - always sanitized
|
|
91
|
+
* @param {Response} res - Express response
|
|
92
|
+
* @param {Error} error - Original error (will be sanitized)
|
|
93
|
+
*/
|
|
94
|
+
export function sendServerError(res, error) {
|
|
95
|
+
sendError(res, error, 500);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Express error handler middleware
|
|
100
|
+
*/
|
|
101
|
+
export function errorHandler(err, req, res, next) {
|
|
102
|
+
// Log the actual error for debugging
|
|
103
|
+
console.error('[Error]', err);
|
|
104
|
+
|
|
105
|
+
// Send sanitized response
|
|
106
|
+
const statusCode = err instanceof AppError ? err.statusCode : 500;
|
|
107
|
+
sendError(res, err, statusCode);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Wrap async route handlers to catch promise rejections
|
|
112
|
+
*/
|
|
113
|
+
export function asyncHandler(fn) {
|
|
114
|
+
return (req, res, next) => {
|
|
115
|
+
Promise.resolve(fn(req, res, next)).catch(next);
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export default {
|
|
120
|
+
sendSuccess,
|
|
121
|
+
sendError,
|
|
122
|
+
sendValidationError,
|
|
123
|
+
sendAuthError,
|
|
124
|
+
sendForbiddenError,
|
|
125
|
+
sendNotFoundError,
|
|
126
|
+
sendRateLimitError,
|
|
127
|
+
sendServerError,
|
|
128
|
+
errorHandler,
|
|
129
|
+
asyncHandler
|
|
130
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry Helper Utility (Server-side)
|
|
3
|
+
* Unified retry logic with exponential backoff
|
|
4
|
+
*
|
|
5
|
+
* Previously duplicated in:
|
|
6
|
+
* - server/utils.js (simple signature)
|
|
7
|
+
* - server/chat.js (options object signature)
|
|
8
|
+
*
|
|
9
|
+
* This version supports both calling conventions for backward compatibility.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Simple logger fallback
|
|
14
|
+
*/
|
|
15
|
+
function logWarn(...args) {
|
|
16
|
+
console.warn('[withRetry]', ...args);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Execute a function with automatic retry on failure
|
|
21
|
+
* Uses exponential backoff between retries
|
|
22
|
+
*
|
|
23
|
+
* Supports two calling conventions:
|
|
24
|
+
* 1. withRetry(fn, maxRetries, delay) - simple
|
|
25
|
+
* 2. withRetry(fn, { maxRetries, baseDelayMs, maxDelayMs }) - options object
|
|
26
|
+
*
|
|
27
|
+
* @param {Function} fn - The async function to retry
|
|
28
|
+
* @param {number|Object} optionsOrMaxRetries - Options object or max retries number
|
|
29
|
+
* @param {number} delayArg - Base delay (only used if second arg is a number)
|
|
30
|
+
* @returns {Promise<any>} - The result of the function
|
|
31
|
+
* @throws {Error} - The last error if all retries fail
|
|
32
|
+
*/
|
|
33
|
+
export async function withRetry(fn, optionsOrMaxRetries = 3, delayArg = 1000) {
|
|
34
|
+
// Normalize arguments - support both calling conventions
|
|
35
|
+
let maxRetries, baseDelayMs, maxDelayMs;
|
|
36
|
+
|
|
37
|
+
if (typeof optionsOrMaxRetries === 'object') {
|
|
38
|
+
// Options object style: withRetry(fn, { maxRetries, baseDelayMs, maxDelayMs })
|
|
39
|
+
maxRetries = optionsOrMaxRetries.maxRetries ?? 3;
|
|
40
|
+
baseDelayMs = optionsOrMaxRetries.baseDelayMs ?? 1000;
|
|
41
|
+
maxDelayMs = optionsOrMaxRetries.maxDelayMs ?? 10000;
|
|
42
|
+
} else {
|
|
43
|
+
// Simple style: withRetry(fn, maxRetries, delay)
|
|
44
|
+
maxRetries = optionsOrMaxRetries;
|
|
45
|
+
baseDelayMs = delayArg;
|
|
46
|
+
maxDelayMs = delayArg * Math.pow(2, maxRetries); // Calculate reasonable max
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let lastError;
|
|
50
|
+
|
|
51
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
52
|
+
try {
|
|
53
|
+
return await fn();
|
|
54
|
+
} catch (err) {
|
|
55
|
+
lastError = err;
|
|
56
|
+
|
|
57
|
+
if (attempt < maxRetries - 1) {
|
|
58
|
+
// Calculate delay with exponential backoff, capped at maxDelayMs
|
|
59
|
+
const delay = Math.min(baseDelayMs * Math.pow(2, attempt), maxDelayMs);
|
|
60
|
+
|
|
61
|
+
logWarn(`Retry ${attempt + 1}/${maxRetries} after ${delay}ms: ${err.message}`);
|
|
62
|
+
|
|
63
|
+
await sleep(delay);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
throw lastError;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Sleep for a specified duration
|
|
73
|
+
*
|
|
74
|
+
* @param {number} ms - Milliseconds to sleep
|
|
75
|
+
* @returns {Promise<void>}
|
|
76
|
+
*/
|
|
77
|
+
export function sleep(ms) {
|
|
78
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Create a configured retry function with preset options
|
|
83
|
+
* Useful when you want consistent retry behavior across multiple calls
|
|
84
|
+
*
|
|
85
|
+
* @param {Object} options - Default retry options
|
|
86
|
+
* @returns {Function} - Configured retry function
|
|
87
|
+
*/
|
|
88
|
+
export function createRetrier(options = {}) {
|
|
89
|
+
const defaults = {
|
|
90
|
+
maxRetries: options.maxRetries ?? 3,
|
|
91
|
+
baseDelayMs: options.baseDelayMs ?? 1000,
|
|
92
|
+
maxDelayMs: options.maxDelayMs ?? 10000,
|
|
93
|
+
...options
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
return (fn, overrides = {}) => {
|
|
97
|
+
return withRetry(fn, { ...defaults, ...overrides });
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export default {
|
|
102
|
+
withRetry,
|
|
103
|
+
sleep,
|
|
104
|
+
createRetrier
|
|
105
|
+
};
|