nova-terminal-assistant 0.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.
Potentially problematic release.
This version of nova-terminal-assistant might be problematic. Click here for more details.
- package/README.md +358 -0
- package/bin/nova +38 -0
- package/bin/nova.js +12 -0
- package/package.json +67 -0
- package/src/cli/commands/SmartCompletion.ts +458 -0
- package/src/cli/index.ts +5 -0
- package/src/cli/startup/IFlowRepl.ts +212 -0
- package/src/cli/startup/InkBasedRepl.ts +1056 -0
- package/src/cli/startup/InteractiveRepl.ts +2833 -0
- package/src/cli/startup/NovaApp.ts +1861 -0
- package/src/cli/startup/index.ts +4 -0
- package/src/cli/startup/parseArgs.ts +293 -0
- package/src/cli/test-modules.ts +27 -0
- package/src/cli/ui/IFlowDropdown.ts +425 -0
- package/src/cli/ui/ModernReplUI.ts +276 -0
- package/src/cli/ui/SimpleSelector2.ts +215 -0
- package/src/cli/ui/components/ConfirmDialog.ts +176 -0
- package/src/cli/ui/components/ErrorPanel.ts +364 -0
- package/src/cli/ui/components/InkAppRunner.tsx +67 -0
- package/src/cli/ui/components/InkComponents.tsx +613 -0
- package/src/cli/ui/components/NovaInkApp.tsx +312 -0
- package/src/cli/ui/components/ProgressBar.ts +177 -0
- package/src/cli/ui/components/ProgressIndicator.ts +298 -0
- package/src/cli/ui/components/QuickActions.ts +396 -0
- package/src/cli/ui/components/SimpleErrorPanel.ts +231 -0
- package/src/cli/ui/components/StatusBar.ts +194 -0
- package/src/cli/ui/components/ThinkingBlockRenderer.ts +401 -0
- package/src/cli/ui/components/index.ts +27 -0
- package/src/cli/ui/ink-prototype.tsx +347 -0
- package/src/cli/utils/CliUI.ts +336 -0
- package/src/cli/utils/CompletionHelper.ts +388 -0
- package/src/cli/utils/EnhancedCompleter.test.ts +226 -0
- package/src/cli/utils/EnhancedCompleter.ts +513 -0
- package/src/cli/utils/ErrorEnhancer.ts +429 -0
- package/src/cli/utils/OutputFormatter.ts +193 -0
- package/src/cli/utils/index.ts +9 -0
- package/src/core/agents/AgentOrchestrator.ts +515 -0
- package/src/core/agents/index.ts +17 -0
- package/src/core/audit/AuditLogger.ts +509 -0
- package/src/core/audit/index.ts +11 -0
- package/src/core/auth/AuthManager.d.ts.map +1 -0
- package/src/core/auth/AuthManager.ts +138 -0
- package/src/core/auth/index.d.ts.map +1 -0
- package/src/core/auth/index.ts +2 -0
- package/src/core/config/ConfigManager.d.ts.map +1 -0
- package/src/core/config/ConfigManager.test.ts +183 -0
- package/src/core/config/ConfigManager.ts +1219 -0
- package/src/core/config/index.d.ts.map +1 -0
- package/src/core/config/index.ts +1 -0
- package/src/core/context/ContextBuilder.d.ts.map +1 -0
- package/src/core/context/ContextBuilder.ts +171 -0
- package/src/core/context/ContextCompressor.d.ts.map +1 -0
- package/src/core/context/ContextCompressor.ts +642 -0
- package/src/core/context/LayeredMemoryManager.ts +657 -0
- package/src/core/context/MemoryDiscovery.d.ts.map +1 -0
- package/src/core/context/MemoryDiscovery.ts +175 -0
- package/src/core/context/defaultSystemPrompt.d.ts.map +1 -0
- package/src/core/context/defaultSystemPrompt.ts +35 -0
- package/src/core/context/index.d.ts.map +1 -0
- package/src/core/context/index.ts +22 -0
- package/src/core/extensions/SkillGenerator.ts +421 -0
- package/src/core/extensions/SkillInstaller.d.ts.map +1 -0
- package/src/core/extensions/SkillInstaller.ts +257 -0
- package/src/core/extensions/SkillRegistry.d.ts.map +1 -0
- package/src/core/extensions/SkillRegistry.ts +361 -0
- package/src/core/extensions/SkillValidator.ts +525 -0
- package/src/core/extensions/index.ts +15 -0
- package/src/core/index.d.ts.map +1 -0
- package/src/core/index.ts +42 -0
- package/src/core/mcp/McpManager.d.ts.map +1 -0
- package/src/core/mcp/McpManager.ts +632 -0
- package/src/core/mcp/index.d.ts.map +1 -0
- package/src/core/mcp/index.ts +2 -0
- package/src/core/model/ModelClient.d.ts.map +1 -0
- package/src/core/model/ModelClient.ts +217 -0
- package/src/core/model/ModelConnectionTester.ts +363 -0
- package/src/core/model/ModelValidator.ts +348 -0
- package/src/core/model/index.d.ts.map +1 -0
- package/src/core/model/index.ts +6 -0
- package/src/core/model/providers/AnthropicProvider.d.ts.map +1 -0
- package/src/core/model/providers/AnthropicProvider.ts +279 -0
- package/src/core/model/providers/CodingPlanProvider.d.ts.map +1 -0
- package/src/core/model/providers/CodingPlanProvider.ts +210 -0
- package/src/core/model/providers/OllamaCloudProvider.d.ts.map +1 -0
- package/src/core/model/providers/OllamaCloudProvider.ts +405 -0
- package/src/core/model/providers/OllamaManager.d.ts.map +1 -0
- package/src/core/model/providers/OllamaManager.ts +201 -0
- package/src/core/model/providers/OllamaProvider.d.ts.map +1 -0
- package/src/core/model/providers/OllamaProvider.ts +73 -0
- package/src/core/model/providers/OpenAICompatibleProvider.d.ts.map +1 -0
- package/src/core/model/providers/OpenAICompatibleProvider.ts +327 -0
- package/src/core/model/providers/OpenAIProvider.d.ts.map +1 -0
- package/src/core/model/providers/OpenAIProvider.ts +29 -0
- package/src/core/model/providers/index.d.ts.map +1 -0
- package/src/core/model/providers/index.ts +12 -0
- package/src/core/model/types.d.ts.map +1 -0
- package/src/core/model/types.ts +77 -0
- package/src/core/security/ApprovalManager.d.ts.map +1 -0
- package/src/core/security/ApprovalManager.ts +174 -0
- package/src/core/security/FileFilter.d.ts.map +1 -0
- package/src/core/security/FileFilter.ts +141 -0
- package/src/core/security/HookExecutor.d.ts.map +1 -0
- package/src/core/security/HookExecutor.ts +178 -0
- package/src/core/security/SandboxExecutor.ts +447 -0
- package/src/core/security/index.d.ts.map +1 -0
- package/src/core/security/index.ts +8 -0
- package/src/core/session/AgentLoop.d.ts.map +1 -0
- package/src/core/session/AgentLoop.ts +501 -0
- package/src/core/session/SessionManager.d.ts.map +1 -0
- package/src/core/session/SessionManager.test.ts +183 -0
- package/src/core/session/SessionManager.ts +460 -0
- package/src/core/session/index.d.ts.map +1 -0
- package/src/core/session/index.ts +3 -0
- package/src/core/telemetry/Telemetry.d.ts.map +1 -0
- package/src/core/telemetry/Telemetry.ts +90 -0
- package/src/core/telemetry/TelemetryService.ts +531 -0
- package/src/core/telemetry/index.d.ts.map +1 -0
- package/src/core/telemetry/index.ts +12 -0
- package/src/core/testing/AutoFixer.ts +385 -0
- package/src/core/testing/ErrorAnalyzer.ts +499 -0
- package/src/core/testing/TestRunner.ts +265 -0
- package/src/core/testing/agent-cli-tests.ts +538 -0
- package/src/core/testing/index.ts +11 -0
- package/src/core/tools/ToolRegistry.d.ts.map +1 -0
- package/src/core/tools/ToolRegistry.test.ts +206 -0
- package/src/core/tools/ToolRegistry.ts +260 -0
- package/src/core/tools/impl/EditFileTool.d.ts.map +1 -0
- package/src/core/tools/impl/EditFileTool.ts +97 -0
- package/src/core/tools/impl/ListDirectoryTool.d.ts.map +1 -0
- package/src/core/tools/impl/ListDirectoryTool.ts +142 -0
- package/src/core/tools/impl/MemoryTool.d.ts.map +1 -0
- package/src/core/tools/impl/MemoryTool.ts +102 -0
- package/src/core/tools/impl/ReadFileTool.d.ts.map +1 -0
- package/src/core/tools/impl/ReadFileTool.ts +58 -0
- package/src/core/tools/impl/SearchContentTool.d.ts.map +1 -0
- package/src/core/tools/impl/SearchContentTool.ts +94 -0
- package/src/core/tools/impl/SearchFileTool.d.ts.map +1 -0
- package/src/core/tools/impl/SearchFileTool.ts +61 -0
- package/src/core/tools/impl/ShellTool.d.ts.map +1 -0
- package/src/core/tools/impl/ShellTool.ts +118 -0
- package/src/core/tools/impl/TaskTool.d.ts.map +1 -0
- package/src/core/tools/impl/TaskTool.ts +207 -0
- package/src/core/tools/impl/TodoTool.d.ts.map +1 -0
- package/src/core/tools/impl/TodoTool.ts +122 -0
- package/src/core/tools/impl/WebFetchTool.d.ts.map +1 -0
- package/src/core/tools/impl/WebFetchTool.ts +103 -0
- package/src/core/tools/impl/WebSearchTool.d.ts.map +1 -0
- package/src/core/tools/impl/WebSearchTool.ts +89 -0
- package/src/core/tools/impl/WriteFileTool.d.ts.map +1 -0
- package/src/core/tools/impl/WriteFileTool.ts +49 -0
- package/src/core/tools/impl/index.d.ts.map +1 -0
- package/src/core/tools/impl/index.ts +16 -0
- package/src/core/tools/index.d.ts.map +1 -0
- package/src/core/tools/index.ts +7 -0
- package/src/core/tools/schemas/execution.d.ts.map +1 -0
- package/src/core/tools/schemas/execution.ts +42 -0
- package/src/core/tools/schemas/file.d.ts.map +1 -0
- package/src/core/tools/schemas/file.ts +119 -0
- package/src/core/tools/schemas/index.d.ts.map +1 -0
- package/src/core/tools/schemas/index.ts +11 -0
- package/src/core/tools/schemas/memory.d.ts.map +1 -0
- package/src/core/tools/schemas/memory.ts +52 -0
- package/src/core/tools/schemas/orchestration.d.ts.map +1 -0
- package/src/core/tools/schemas/orchestration.ts +44 -0
- package/src/core/tools/schemas/search.d.ts.map +1 -0
- package/src/core/tools/schemas/search.ts +112 -0
- package/src/core/tools/schemas/todo.d.ts.map +1 -0
- package/src/core/tools/schemas/todo.ts +32 -0
- package/src/core/tools/schemas/web.d.ts.map +1 -0
- package/src/core/tools/schemas/web.ts +86 -0
- package/src/core/types/config.d.ts.map +1 -0
- package/src/core/types/config.ts +200 -0
- package/src/core/types/errors.d.ts.map +1 -0
- package/src/core/types/errors.ts +204 -0
- package/src/core/types/index.d.ts.map +1 -0
- package/src/core/types/index.ts +8 -0
- package/src/core/types/session.d.ts.map +1 -0
- package/src/core/types/session.ts +216 -0
- package/src/core/types/tools.d.ts.map +1 -0
- package/src/core/types/tools.ts +157 -0
- package/src/core/utils/CheckpointManager.d.ts.map +1 -0
- package/src/core/utils/CheckpointManager.ts +327 -0
- package/src/core/utils/Logger.d.ts.map +1 -0
- package/src/core/utils/Logger.ts +98 -0
- package/src/core/utils/RetryManager.ts +471 -0
- package/src/core/utils/TokenCounter.d.ts.map +1 -0
- package/src/core/utils/TokenCounter.ts +414 -0
- package/src/core/utils/VectorMemoryStore.ts +440 -0
- package/src/core/utils/helpers.d.ts.map +1 -0
- package/src/core/utils/helpers.ts +89 -0
- package/src/core/utils/index.d.ts.map +1 -0
- package/src/core/utils/index.ts +19 -0
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// RetryManager - Exponential backoff retry with jitter
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
import { createLogger } from './Logger.js';
|
|
6
|
+
|
|
7
|
+
const logger = createLogger('RetryManager');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Retry configuration options
|
|
11
|
+
*/
|
|
12
|
+
export interface RetryConfig {
|
|
13
|
+
/** Maximum number of retry attempts */
|
|
14
|
+
maxAttempts: number;
|
|
15
|
+
/** Initial delay in milliseconds */
|
|
16
|
+
initialDelay: number;
|
|
17
|
+
/** Maximum delay in milliseconds */
|
|
18
|
+
maxDelay: number;
|
|
19
|
+
/** Backoff multiplier (default 2) */
|
|
20
|
+
backoffMultiplier?: number;
|
|
21
|
+
/** Jitter factor (0-1, default 0.1) */
|
|
22
|
+
jitterFactor?: number;
|
|
23
|
+
/** Errors that should trigger retry */
|
|
24
|
+
retryableErrors?: string[];
|
|
25
|
+
/** HTTP status codes that should trigger retry */
|
|
26
|
+
retryableStatusCodes?: number[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Default retry configuration
|
|
31
|
+
*/
|
|
32
|
+
const DEFAULT_RETRY_CONFIG: RetryConfig = {
|
|
33
|
+
maxAttempts: 3,
|
|
34
|
+
initialDelay: 1000,
|
|
35
|
+
maxDelay: 30000,
|
|
36
|
+
backoffMultiplier: 2,
|
|
37
|
+
jitterFactor: 0.1,
|
|
38
|
+
retryableErrors: [
|
|
39
|
+
'ECONNRESET',
|
|
40
|
+
'ENOTFOUND',
|
|
41
|
+
'ETIMEDOUT',
|
|
42
|
+
'ECONNREFUSED',
|
|
43
|
+
'ENETDOWN',
|
|
44
|
+
'ENETUNREACH',
|
|
45
|
+
'EHOSTDOWN',
|
|
46
|
+
'EHOSTUNREACH',
|
|
47
|
+
'EPIPE',
|
|
48
|
+
'rate_limit',
|
|
49
|
+
'overloaded',
|
|
50
|
+
'timeout',
|
|
51
|
+
],
|
|
52
|
+
retryableStatusCodes: [429, 500, 502, 503, 504],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Calculate delay with exponential backoff and jitter
|
|
57
|
+
*/
|
|
58
|
+
function calculateDelay(
|
|
59
|
+
attempt: number,
|
|
60
|
+
config: RetryConfig
|
|
61
|
+
): number {
|
|
62
|
+
const { initialDelay, maxDelay, backoffMultiplier = 2, jitterFactor = 0.1 } = config;
|
|
63
|
+
|
|
64
|
+
// Exponential backoff: initialDelay * (2 ^ attempt)
|
|
65
|
+
const exponentialDelay = initialDelay * Math.pow(backoffMultiplier, attempt);
|
|
66
|
+
|
|
67
|
+
// Cap at max delay
|
|
68
|
+
const cappedDelay = Math.min(exponentialDelay, maxDelay);
|
|
69
|
+
|
|
70
|
+
// Add jitter: random variation to prevent thundering herd
|
|
71
|
+
const jitter = cappedDelay * jitterFactor * (Math.random() * 2 - 1);
|
|
72
|
+
|
|
73
|
+
return Math.max(0, cappedDelay + jitter);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Check if an error is retryable
|
|
78
|
+
*/
|
|
79
|
+
function isRetryableError(
|
|
80
|
+
error: unknown,
|
|
81
|
+
config: RetryConfig
|
|
82
|
+
): boolean {
|
|
83
|
+
const { retryableErrors = [], retryableStatusCodes = [] } = config;
|
|
84
|
+
|
|
85
|
+
// Check error message
|
|
86
|
+
const errorMessage = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
87
|
+
for (const retryable of retryableErrors) {
|
|
88
|
+
if (errorMessage.includes(retryable.toLowerCase())) {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Check error name
|
|
94
|
+
if (error instanceof Error) {
|
|
95
|
+
for (const retryable of retryableErrors) {
|
|
96
|
+
if (error.name.toLowerCase().includes(retryable.toLowerCase())) {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Check status code
|
|
103
|
+
const anyError = error as any;
|
|
104
|
+
if (anyError?.status && retryableStatusCodes.includes(anyError.status)) {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
if (anyError?.statusCode && retryableStatusCodes.includes(anyError.statusCode)) {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Check for specific error types
|
|
112
|
+
if (anyError?.error?.type === 'rate_limit_error') return true;
|
|
113
|
+
if (anyError?.error?.type === 'overloaded_error') return true;
|
|
114
|
+
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Sleep for a given duration
|
|
120
|
+
*/
|
|
121
|
+
function sleep(ms: number): Promise<void> {
|
|
122
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Execute a function with retry logic
|
|
127
|
+
*/
|
|
128
|
+
export async function withRetry<T>(
|
|
129
|
+
fn: () => Promise<T>,
|
|
130
|
+
config: Partial<RetryConfig> = {}
|
|
131
|
+
): Promise<T> {
|
|
132
|
+
const fullConfig = { ...DEFAULT_RETRY_CONFIG, ...config };
|
|
133
|
+
const { maxAttempts } = fullConfig;
|
|
134
|
+
|
|
135
|
+
let lastError: unknown;
|
|
136
|
+
|
|
137
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
138
|
+
try {
|
|
139
|
+
return await fn();
|
|
140
|
+
} catch (error) {
|
|
141
|
+
lastError = error;
|
|
142
|
+
|
|
143
|
+
// Check if retryable
|
|
144
|
+
if (!isRetryableError(error, fullConfig)) {
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Check if we've exhausted attempts
|
|
149
|
+
if (attempt >= maxAttempts - 1) {
|
|
150
|
+
logger.warn(`Retry exhausted after ${maxAttempts} attempts`, { error });
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Calculate delay and wait
|
|
155
|
+
const delay = calculateDelay(attempt, fullConfig);
|
|
156
|
+
logger.debug(`Retrying in ${delay}ms (attempt ${attempt + 1}/${maxAttempts})`, { error });
|
|
157
|
+
|
|
158
|
+
await sleep(delay);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
throw lastError;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Create a retry wrapper for a function
|
|
167
|
+
*/
|
|
168
|
+
export function createRetryWrapper<TArgs extends any[], TResult>(
|
|
169
|
+
fn: (...args: TArgs) => Promise<TResult>,
|
|
170
|
+
config: Partial<RetryConfig> = {}
|
|
171
|
+
): (...args: TArgs) => Promise<TResult> {
|
|
172
|
+
return (...args: TArgs) => withRetry(() => fn(...args), config);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ============================================================================
|
|
176
|
+
// RateLimiter - Token bucket rate limiting
|
|
177
|
+
// ============================================================================
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Rate limiter configuration
|
|
181
|
+
*/
|
|
182
|
+
export interface RateLimiterConfig {
|
|
183
|
+
/** Maximum tokens in bucket */
|
|
184
|
+
maxTokens: number;
|
|
185
|
+
/** Tokens replenished per second */
|
|
186
|
+
tokensPerSecond: number;
|
|
187
|
+
/** Maximum wait time in milliseconds (default 60000) */
|
|
188
|
+
maxWaitTime?: number;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Token bucket rate limiter
|
|
193
|
+
*/
|
|
194
|
+
export class RateLimiter {
|
|
195
|
+
private tokens: number;
|
|
196
|
+
private lastRefill: number;
|
|
197
|
+
private config: RateLimiterConfig;
|
|
198
|
+
private waitQueue: Array<{
|
|
199
|
+
tokens: number;
|
|
200
|
+
resolve: () => void;
|
|
201
|
+
reject: (err: Error) => void;
|
|
202
|
+
timestamp: number;
|
|
203
|
+
}> = [];
|
|
204
|
+
|
|
205
|
+
constructor(config: RateLimiterConfig) {
|
|
206
|
+
this.config = config;
|
|
207
|
+
this.tokens = config.maxTokens;
|
|
208
|
+
this.lastRefill = Date.now();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Refill tokens based on elapsed time
|
|
213
|
+
*/
|
|
214
|
+
private refill(): void {
|
|
215
|
+
const now = Date.now();
|
|
216
|
+
const elapsed = (now - this.lastRefill) / 1000;
|
|
217
|
+
const tokensToAdd = elapsed * this.config.tokensPerSecond;
|
|
218
|
+
|
|
219
|
+
this.tokens = Math.min(this.config.maxTokens, this.tokens + tokensToAdd);
|
|
220
|
+
this.lastRefill = now;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Try to acquire tokens without waiting
|
|
225
|
+
* @returns true if tokens were acquired, false otherwise
|
|
226
|
+
*/
|
|
227
|
+
tryAcquire(tokens: number): boolean {
|
|
228
|
+
this.refill();
|
|
229
|
+
|
|
230
|
+
if (this.tokens >= tokens) {
|
|
231
|
+
this.tokens -= tokens;
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Acquire tokens, waiting if necessary
|
|
240
|
+
*/
|
|
241
|
+
async acquire(tokens: number): Promise<void> {
|
|
242
|
+
this.refill();
|
|
243
|
+
|
|
244
|
+
// If we have enough tokens, acquire immediately
|
|
245
|
+
if (this.tokens >= tokens) {
|
|
246
|
+
this.tokens -= tokens;
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Calculate wait time
|
|
251
|
+
const tokensNeeded = tokens - this.tokens;
|
|
252
|
+
const waitTimeMs = (tokensNeeded / this.config.tokensPerSecond) * 1000;
|
|
253
|
+
const maxWaitTime = this.config.maxWaitTime ?? 60000;
|
|
254
|
+
|
|
255
|
+
if (waitTimeMs > maxWaitTime) {
|
|
256
|
+
throw new Error(`Rate limit wait time (${waitTimeMs}ms) exceeds maximum (${maxWaitTime}ms)`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Add to wait queue
|
|
260
|
+
return new Promise((resolve, reject) => {
|
|
261
|
+
this.waitQueue.push({
|
|
262
|
+
tokens,
|
|
263
|
+
resolve,
|
|
264
|
+
reject,
|
|
265
|
+
timestamp: Date.now(),
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Start processing queue
|
|
269
|
+
this.processQueue();
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Process waiting requests
|
|
275
|
+
*/
|
|
276
|
+
private processQueue(): void {
|
|
277
|
+
const checkAndProcess = () => {
|
|
278
|
+
this.refill();
|
|
279
|
+
|
|
280
|
+
// Process queue in order
|
|
281
|
+
while (this.waitQueue.length > 0 && this.tokens >= (this.waitQueue[0]?.tokens ?? 0)) {
|
|
282
|
+
const request = this.waitQueue.shift();
|
|
283
|
+
if (request) {
|
|
284
|
+
this.tokens -= request.tokens;
|
|
285
|
+
request.resolve();
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Schedule next check if queue not empty
|
|
290
|
+
const nextRequest = this.waitQueue[0];
|
|
291
|
+
if (nextRequest) {
|
|
292
|
+
const tokensNeeded = nextRequest.tokens - this.tokens;
|
|
293
|
+
const waitTimeMs = (tokensNeeded / this.config.tokensPerSecond) * 1000;
|
|
294
|
+
|
|
295
|
+
setTimeout(checkAndProcess, Math.min(waitTimeMs, 100));
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// Check for timed-out requests
|
|
300
|
+
const maxWaitTime = this.config.maxWaitTime ?? 60000;
|
|
301
|
+
const now = Date.now();
|
|
302
|
+
this.waitQueue = this.waitQueue.filter((request) => {
|
|
303
|
+
if (now - request.timestamp > maxWaitTime) {
|
|
304
|
+
request.reject(new Error('Rate limit wait timeout'));
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
return true;
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
checkAndProcess();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Get current token count
|
|
315
|
+
*/
|
|
316
|
+
getTokens(): number {
|
|
317
|
+
this.refill();
|
|
318
|
+
return this.tokens;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Get estimated wait time for a given number of tokens
|
|
323
|
+
*/
|
|
324
|
+
getWaitTime(tokens: number): number {
|
|
325
|
+
this.refill();
|
|
326
|
+
|
|
327
|
+
if (this.tokens >= tokens) return 0;
|
|
328
|
+
|
|
329
|
+
const tokensNeeded = tokens - this.tokens;
|
|
330
|
+
return (tokensNeeded / this.config.tokensPerSecond) * 1000;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Create a rate-limited wrapper for a function
|
|
336
|
+
*/
|
|
337
|
+
export function withRateLimit<TArgs extends any[], TResult>(
|
|
338
|
+
fn: (...args: TArgs) => Promise<TResult>,
|
|
339
|
+
limiter: RateLimiter,
|
|
340
|
+
tokensPerCall: number = 1
|
|
341
|
+
): (...args: TArgs) => Promise<TResult> {
|
|
342
|
+
return async (...args: TArgs) => {
|
|
343
|
+
await limiter.acquire(tokensPerCall);
|
|
344
|
+
return fn(...args);
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ============================================================================
|
|
349
|
+
// ConcurrencyLimiter - Limit concurrent operations
|
|
350
|
+
// ============================================================================
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Concurrency limiter using semaphore pattern
|
|
354
|
+
*/
|
|
355
|
+
export class ConcurrencyLimiter {
|
|
356
|
+
private running = 0;
|
|
357
|
+
private queue: Array<{
|
|
358
|
+
resolve: () => void;
|
|
359
|
+
reject: (err: Error) => void;
|
|
360
|
+
}> = [];
|
|
361
|
+
|
|
362
|
+
constructor(private maxConcurrent: number) {}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Acquire a slot
|
|
366
|
+
*/
|
|
367
|
+
async acquire(): Promise<void> {
|
|
368
|
+
if (this.running < this.maxConcurrent) {
|
|
369
|
+
this.running++;
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return new Promise((resolve, reject) => {
|
|
374
|
+
this.queue.push({ resolve, reject });
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Release a slot
|
|
380
|
+
*/
|
|
381
|
+
release(): void {
|
|
382
|
+
this.running--;
|
|
383
|
+
|
|
384
|
+
if (this.queue.length > 0) {
|
|
385
|
+
const next = this.queue.shift()!;
|
|
386
|
+
this.running++;
|
|
387
|
+
next.resolve();
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Execute a function with concurrency limit
|
|
393
|
+
*/
|
|
394
|
+
async run<T>(fn: () => Promise<T>): Promise<T> {
|
|
395
|
+
await this.acquire();
|
|
396
|
+
try {
|
|
397
|
+
return await fn();
|
|
398
|
+
} finally {
|
|
399
|
+
this.release();
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Get current state
|
|
405
|
+
*/
|
|
406
|
+
getState(): { running: number; queued: number; available: number } {
|
|
407
|
+
return {
|
|
408
|
+
running: this.running,
|
|
409
|
+
queued: this.queue.length,
|
|
410
|
+
available: this.maxConcurrent - this.running,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ============================================================================
|
|
416
|
+
// Composite utilities
|
|
417
|
+
// ============================================================================
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Create a resilient function with retry, rate limiting, and concurrency control
|
|
421
|
+
*/
|
|
422
|
+
export function createResilientFunction<TArgs extends any[], TResult>(
|
|
423
|
+
fn: (...args: TArgs) => Promise<TResult>,
|
|
424
|
+
options: {
|
|
425
|
+
retry?: Partial<RetryConfig>;
|
|
426
|
+
rateLimit?: RateLimiterConfig;
|
|
427
|
+
maxConcurrent?: number;
|
|
428
|
+
} = {}
|
|
429
|
+
): (...args: TArgs) => Promise<TResult> {
|
|
430
|
+
let wrapped = fn;
|
|
431
|
+
|
|
432
|
+
// Add retry
|
|
433
|
+
if (options.retry) {
|
|
434
|
+
wrapped = createRetryWrapper(wrapped, options.retry);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Add rate limiting
|
|
438
|
+
if (options.rateLimit) {
|
|
439
|
+
const limiter = new RateLimiter(options.rateLimit);
|
|
440
|
+
wrapped = withRateLimit(wrapped, limiter);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Add concurrency control
|
|
444
|
+
if (options.maxConcurrent) {
|
|
445
|
+
const limiter = new ConcurrencyLimiter(options.maxConcurrent);
|
|
446
|
+
const originalWrapped = wrapped;
|
|
447
|
+
wrapped = async (...args: TArgs) => limiter.run(() => originalWrapped(...args));
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return wrapped;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Default rate limiter for API calls
|
|
455
|
+
* - 60 requests per minute (1 per second)
|
|
456
|
+
* - Burst of up to 10 requests
|
|
457
|
+
*/
|
|
458
|
+
export function createDefaultApiRateLimiter(): RateLimiter {
|
|
459
|
+
return new RateLimiter({
|
|
460
|
+
maxTokens: 10,
|
|
461
|
+
tokensPerSecond: 1,
|
|
462
|
+
maxWaitTime: 60000,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Default concurrency limiter for API calls
|
|
468
|
+
*/
|
|
469
|
+
export function createDefaultConcurrencyLimiter(): ConcurrencyLimiter {
|
|
470
|
+
return new ConcurrencyLimiter(5);
|
|
471
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TokenCounter.d.ts","sourceRoot":"","sources":["TokenCounter.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAiIjE;;GAEG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAe;IACtC,OAAO,CAAC,UAAU,CAA2C;IAC7D,OAAO,CAAC,KAAK,CAAkC;IAC/C,OAAO,CAAC,YAAY,CAAQ;IAE5B,OAAO;IAEP,MAAM,CAAC,WAAW,IAAI,YAAY;IAOlC;;OAEG;IACH,OAAO,CAAC,YAAY;IAYpB;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAa1B;;OAEG;IACH,OAAO,CAAC,eAAe;IAOvB;;OAEG;IACH,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,GAAE,MAAkB,GAAG,MAAM;IAyB5D;;OAEG;IACH,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,GAAE,MAAkB,GAAG,MAAM;IAsBvE;;OAEG;IACH,uBAAuB,CAAC,KAAK,EAAE,YAAY,EAAE,KAAK,GAAE,MAAkB,GAAG,MAAM;IAsC/E;;OAEG;IACH,mBAAmB,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,KAAK,GAAE,MAAkB,GAAG,MAAM;IAc3E;;OAEG;IACH,uBAAuB,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,GAAE,MAAkB,GAAG,MAAM;IAKhF;;;OAGG;IACH,eAAe,CACb,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,KAAK,GAAE,MAAkB,EACzB,OAAO,GAAE;QACP,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACd,GACL;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,OAAO,CAAA;KAAE;IAsD1D;;OAEG;IACH,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM;IAIpD;;OAEG;IACH,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU;IAOzE;;OAEG;IACH,UAAU,IAAI,IAAI;CAGnB;AAGD,eAAO,MAAM,YAAY,cAA6B,CAAC;AAGvD,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAEhE;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAE/E"}
|