@revenium/anthropic 1.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.
- package/LICENSE +21 -0
- package/README.md +807 -0
- package/dist/cjs/config.js +208 -0
- package/dist/cjs/constants.js +144 -0
- package/dist/cjs/index.js +148 -0
- package/dist/cjs/tracking.js +252 -0
- package/dist/cjs/types/anthropic-augmentation.js +87 -0
- package/dist/cjs/types.js +6 -0
- package/dist/cjs/utils/circuit-breaker.js +232 -0
- package/dist/cjs/utils/error-handling.js +233 -0
- package/dist/cjs/utils/validation.js +307 -0
- package/dist/cjs/wrapper.js +374 -0
- package/dist/esm/config.js +197 -0
- package/dist/esm/constants.js +141 -0
- package/dist/esm/index.js +121 -0
- package/dist/esm/tracking.js +243 -0
- package/dist/esm/types/anthropic-augmentation.js +86 -0
- package/dist/esm/types.js +5 -0
- package/dist/esm/utils/circuit-breaker.js +223 -0
- package/dist/esm/utils/error-handling.js +216 -0
- package/dist/esm/utils/validation.js +296 -0
- package/dist/esm/wrapper.js +366 -0
- package/dist/types/config.d.ts +43 -0
- package/dist/types/constants.d.ts +141 -0
- package/dist/types/index.d.ts +54 -0
- package/dist/types/tracking.d.ts +42 -0
- package/dist/types/types/anthropic-augmentation.d.ts +182 -0
- package/dist/types/types.d.ts +647 -0
- package/dist/types/utils/circuit-breaker.d.ts +110 -0
- package/dist/types/utils/error-handling.d.ts +108 -0
- package/dist/types/utils/validation.d.ts +57 -0
- package/dist/types/wrapper.d.ts +16 -0
- package/package.json +74 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Tracking implementation for Anthropic middleware with resilience patterns
|
|
4
|
+
*/
|
|
5
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
6
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.sendReveniumMetrics = sendReveniumMetrics;
|
|
10
|
+
exports.trackUsageAsync = trackUsageAsync;
|
|
11
|
+
exports.extractUsageFromResponse = extractUsageFromResponse;
|
|
12
|
+
exports.extractUsageFromStream = extractUsageFromStream;
|
|
13
|
+
const node_fetch_1 = __importDefault(require("node-fetch"));
|
|
14
|
+
const config_1 = require("./config");
|
|
15
|
+
const circuit_breaker_1 = require("./utils/circuit-breaker");
|
|
16
|
+
const error_handling_1 = require("./utils/error-handling");
|
|
17
|
+
const constants_1 = require("./constants");
|
|
18
|
+
// Global logger
|
|
19
|
+
const logger = (0, config_1.getLogger)();
|
|
20
|
+
/**
|
|
21
|
+
* Send tracking data to Revenium API with resilience patterns
|
|
22
|
+
*/
|
|
23
|
+
async function sendReveniumMetrics(data) {
|
|
24
|
+
const config = (0, config_1.getConfig)();
|
|
25
|
+
if (!config)
|
|
26
|
+
throw new Error("Revenium configuration not available");
|
|
27
|
+
const requestId = data.requestId;
|
|
28
|
+
logger.debug("Preparing to send metrics to Revenium", {
|
|
29
|
+
requestId,
|
|
30
|
+
model: data.model,
|
|
31
|
+
inputTokens: data.inputTokens,
|
|
32
|
+
outputTokens: data.outputTokens,
|
|
33
|
+
duration: data.duration,
|
|
34
|
+
isStreamed: data.isStreamed,
|
|
35
|
+
});
|
|
36
|
+
// Build payload using exact structure from working implementations
|
|
37
|
+
const payload = buildReveniumPayload(data);
|
|
38
|
+
// Create request options
|
|
39
|
+
const requestOptions = {
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: {
|
|
42
|
+
"Content-Type": "application/json",
|
|
43
|
+
"x-api-key": config.reveniumApiKey,
|
|
44
|
+
"User-Agent": constants_1.LOGGING_CONFIG.USER_AGENT,
|
|
45
|
+
},
|
|
46
|
+
body: JSON.stringify(payload),
|
|
47
|
+
timeout: config.apiTimeout || constants_1.DEFAULT_CONFIG.API_TIMEOUT,
|
|
48
|
+
};
|
|
49
|
+
// Add abort signal for timeout
|
|
50
|
+
const controller = new AbortController();
|
|
51
|
+
const timeoutId = setTimeout(() => controller.abort(), requestOptions.timeout);
|
|
52
|
+
requestOptions.signal = controller.signal;
|
|
53
|
+
// Handle abort signal errors to prevent unhandled error events
|
|
54
|
+
controller.signal.addEventListener("abort", () => {
|
|
55
|
+
// Silently handle abort - this is expected behavior for timeouts
|
|
56
|
+
});
|
|
57
|
+
try {
|
|
58
|
+
// Execute with circuit breaker and retry logic
|
|
59
|
+
await (0, circuit_breaker_1.executeWithCircuitBreaker)(async () => {
|
|
60
|
+
return (0, error_handling_1.withRetry)(async () => {
|
|
61
|
+
logger.debug("Sending request to Revenium API", {
|
|
62
|
+
requestId,
|
|
63
|
+
url: `${config.reveniumBaseUrl}${constants_1.API_ENDPOINTS.AI_COMPLETIONS}`,
|
|
64
|
+
payloadSize: requestOptions.body.length,
|
|
65
|
+
});
|
|
66
|
+
let response;
|
|
67
|
+
try {
|
|
68
|
+
response = await (0, node_fetch_1.default)(`${config.reveniumBaseUrl}${constants_1.API_ENDPOINTS.AI_COMPLETIONS}`, requestOptions);
|
|
69
|
+
}
|
|
70
|
+
catch (fetchError) {
|
|
71
|
+
// Handle AbortError and other fetch errors
|
|
72
|
+
if (fetchError instanceof Error && fetchError.name === "AbortError") {
|
|
73
|
+
throw new Error(`Request timeout after ${requestOptions.timeout}ms`);
|
|
74
|
+
}
|
|
75
|
+
throw fetchError;
|
|
76
|
+
}
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
const responseText = await response
|
|
79
|
+
.text()
|
|
80
|
+
.catch(() => "Unable to read response");
|
|
81
|
+
const errorContext = (0, error_handling_1.createErrorContext)()
|
|
82
|
+
.withRequestId(requestId)
|
|
83
|
+
.withModel(data.model)
|
|
84
|
+
.withStatus(response.status)
|
|
85
|
+
.with("responseBody", responseText)
|
|
86
|
+
.build();
|
|
87
|
+
throw new error_handling_1.ReveniumApiError(`Revenium API error: ${response.status} ${response.statusText}`, response.status, responseText, errorContext);
|
|
88
|
+
}
|
|
89
|
+
logger.debug("Successfully sent metrics to Revenium", {
|
|
90
|
+
requestId,
|
|
91
|
+
status: response.status,
|
|
92
|
+
duration: data.duration,
|
|
93
|
+
});
|
|
94
|
+
return response;
|
|
95
|
+
}, config.maxRetries || constants_1.DEFAULT_CONFIG.MAX_RETRIES);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
const errorContext = (0, error_handling_1.createErrorContext)()
|
|
100
|
+
.withRequestId(requestId)
|
|
101
|
+
.withModel(data.model)
|
|
102
|
+
.withDuration(data.duration)
|
|
103
|
+
.build();
|
|
104
|
+
(0, error_handling_1.handleError)(error, logger, errorContext);
|
|
105
|
+
// Always fail silently for tracking errors to prevent breaking user's application
|
|
106
|
+
// Tracking errors should never break the user's main application flow
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
clearTimeout(timeoutId);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Build Revenium payload from tracking data
|
|
114
|
+
*/
|
|
115
|
+
function buildReveniumPayload(data) {
|
|
116
|
+
const now = new Date().toISOString();
|
|
117
|
+
const requestTime = data.requestTime.toISOString();
|
|
118
|
+
const completionStartTime = data.responseTime.toISOString();
|
|
119
|
+
return {
|
|
120
|
+
stopReason: getStopReason(data.stopReason),
|
|
121
|
+
costType: "AI",
|
|
122
|
+
isStreamed: data.isStreamed,
|
|
123
|
+
taskType: data.metadata?.taskType,
|
|
124
|
+
agent: data.metadata?.agent,
|
|
125
|
+
operationType: "CHAT",
|
|
126
|
+
inputTokenCount: data.inputTokens,
|
|
127
|
+
outputTokenCount: data.outputTokens,
|
|
128
|
+
reasoningTokenCount: 0, // Anthropic doesn't currently have reasoning tokens
|
|
129
|
+
cacheCreationTokenCount: data.cacheCreationTokens || 0,
|
|
130
|
+
cacheReadTokenCount: data.cacheReadTokens || 0,
|
|
131
|
+
totalTokenCount: data.inputTokens + data.outputTokens,
|
|
132
|
+
organizationId: data.metadata?.organizationId,
|
|
133
|
+
productId: data.metadata?.productId,
|
|
134
|
+
subscriber: data.metadata?.subscriber, // Pass through nested subscriber object directly
|
|
135
|
+
subscriptionId: data.metadata?.subscriptionId,
|
|
136
|
+
model: data.model,
|
|
137
|
+
transactionId: data.requestId,
|
|
138
|
+
responseTime: now,
|
|
139
|
+
requestDuration: Math.round(data.duration),
|
|
140
|
+
provider: "Anthropic",
|
|
141
|
+
requestTime: requestTime,
|
|
142
|
+
completionStartTime: completionStartTime,
|
|
143
|
+
timeToFirstToken: data.timeToFirstToken || 0,
|
|
144
|
+
traceId: data.metadata?.traceId,
|
|
145
|
+
middlewareSource: "nodejs",
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Normalize stop reason for Revenium API
|
|
150
|
+
*/
|
|
151
|
+
function getStopReason(stopReason) {
|
|
152
|
+
if (!stopReason)
|
|
153
|
+
return "END";
|
|
154
|
+
// Use predefined mapping from constants
|
|
155
|
+
return (constants_1.ANTHROPIC_PATTERNS.REVENIUM_STOP_REASON_MAP[stopReason] || "END");
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Fire-and-forget async tracking wrapper
|
|
159
|
+
* Ensures tracking never blocks the main application flow
|
|
160
|
+
*/
|
|
161
|
+
function trackUsageAsync(trackingData) {
|
|
162
|
+
const config = (0, config_1.getConfig)();
|
|
163
|
+
if (!config)
|
|
164
|
+
return logger.warn("Revenium configuration not available - skipping tracking", {
|
|
165
|
+
requestId: trackingData.requestId,
|
|
166
|
+
});
|
|
167
|
+
// Run tracking in background without awaiting
|
|
168
|
+
sendReveniumMetrics(trackingData)
|
|
169
|
+
.then(() => {
|
|
170
|
+
logger.debug("Revenium tracking completed successfully", {
|
|
171
|
+
requestId: trackingData.requestId,
|
|
172
|
+
model: trackingData.model,
|
|
173
|
+
totalTokens: trackingData.inputTokens + trackingData.outputTokens,
|
|
174
|
+
});
|
|
175
|
+
})
|
|
176
|
+
.catch((error) => {
|
|
177
|
+
const errorContext = (0, error_handling_1.createErrorContext)()
|
|
178
|
+
.withRequestId(trackingData.requestId)
|
|
179
|
+
.withModel(trackingData.model)
|
|
180
|
+
.build();
|
|
181
|
+
logger.warn("Revenium tracking failed", {
|
|
182
|
+
error: error instanceof Error ? error.message : String(error),
|
|
183
|
+
requestId: trackingData.requestId,
|
|
184
|
+
context: errorContext,
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Extract usage data from Anthropic response
|
|
190
|
+
*/
|
|
191
|
+
function extractUsageFromResponse(response) {
|
|
192
|
+
const usage = response.usage || {};
|
|
193
|
+
return {
|
|
194
|
+
inputTokens: usage.input_tokens || 0,
|
|
195
|
+
outputTokens: usage.output_tokens || 0,
|
|
196
|
+
cacheCreationTokens: usage.cache_creation_input_tokens,
|
|
197
|
+
cacheReadTokens: usage.cache_read_input_tokens,
|
|
198
|
+
stopReason: response.stop_reason,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Extract usage data from streaming chunks
|
|
203
|
+
*/
|
|
204
|
+
function extractUsageFromStream(chunks) {
|
|
205
|
+
let inputTokens = 0;
|
|
206
|
+
let outputTokens = 0;
|
|
207
|
+
let cacheCreationTokens;
|
|
208
|
+
let cacheReadTokens;
|
|
209
|
+
let stopReason;
|
|
210
|
+
for (const chunk of chunks) {
|
|
211
|
+
let usage = null;
|
|
212
|
+
// According to Anthropic docs, usage data appears in:
|
|
213
|
+
// 1. message_start event: chunk.message.usage (initial token count)
|
|
214
|
+
// 2. message_delta event: chunk.usage (final token count)
|
|
215
|
+
// 3. Direct usage field on chunk
|
|
216
|
+
if (chunk?.type === "message_start" && chunk?.message?.usage) {
|
|
217
|
+
usage = chunk?.message?.usage;
|
|
218
|
+
}
|
|
219
|
+
else if (chunk?.usage) {
|
|
220
|
+
usage = chunk?.usage;
|
|
221
|
+
}
|
|
222
|
+
else if (chunk?.delta?.usage) {
|
|
223
|
+
usage = chunk?.delta?.usage;
|
|
224
|
+
}
|
|
225
|
+
//Verify usage with optional chaining
|
|
226
|
+
// Use the highest token counts found (message_delta should have final counts)
|
|
227
|
+
if (usage?.input_tokens) {
|
|
228
|
+
inputTokens = Math.max(inputTokens, usage?.input_tokens);
|
|
229
|
+
}
|
|
230
|
+
if (usage?.output_tokens) {
|
|
231
|
+
outputTokens = Math.max(outputTokens, usage?.output_tokens);
|
|
232
|
+
}
|
|
233
|
+
if (usage?.cache_creation_input_tokens) {
|
|
234
|
+
cacheCreationTokens = usage?.cache_creation_input_tokens;
|
|
235
|
+
}
|
|
236
|
+
if (usage?.cache_read_input_tokens) {
|
|
237
|
+
cacheReadTokens = usage?.cache_read_input_tokens;
|
|
238
|
+
}
|
|
239
|
+
// Extract stop reason from delta
|
|
240
|
+
if (chunk?.delta?.stop_reason) {
|
|
241
|
+
stopReason = chunk?.delta?.stop_reason;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
inputTokens,
|
|
246
|
+
outputTokens,
|
|
247
|
+
cacheCreationTokens,
|
|
248
|
+
cacheReadTokens,
|
|
249
|
+
stopReason,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
//# sourceMappingURL=tracking.js.map
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* TypeScript module augmentation for Anthropic SDK
|
|
4
|
+
*
|
|
5
|
+
* This file extends Anthropic's existing types to include the usageMetadata field
|
|
6
|
+
* through TypeScript's declaration merging feature. This allows developers to
|
|
7
|
+
* use usageMetadata directly in Anthropic API calls without type casting or
|
|
8
|
+
* TypeScript errors.
|
|
9
|
+
*
|
|
10
|
+
* ## What is Module Augmentation?
|
|
11
|
+
*
|
|
12
|
+
* Module augmentation is a TypeScript feature that allows you to extend existing
|
|
13
|
+
* interfaces from external libraries. When you import this middleware, TypeScript
|
|
14
|
+
* automatically recognizes the usageMetadata field as a valid parameter.
|
|
15
|
+
*
|
|
16
|
+
* ## Benefits:
|
|
17
|
+
* - **Type Safety**: Full IntelliSense support for usageMetadata
|
|
18
|
+
* - **No Type Casting**: Use usageMetadata directly without `as any`
|
|
19
|
+
* - **Automatic Validation**: TypeScript validates the structure at compile time
|
|
20
|
+
* - **Better Developer Experience**: Auto-completion and error detection
|
|
21
|
+
*
|
|
22
|
+
* ## Usage Examples:
|
|
23
|
+
*
|
|
24
|
+
* ### Basic Usage:
|
|
25
|
+
* ```typescript
|
|
26
|
+
* import 'revenium-middleware-anthropic-node';
|
|
27
|
+
* import Anthropic from '@anthropic-ai/sdk';
|
|
28
|
+
*
|
|
29
|
+
* const anthropic = new Anthropic();
|
|
30
|
+
*
|
|
31
|
+
* const response = await anthropic.messages.create({
|
|
32
|
+
* model: 'claude-3-5-sonnet-latest',
|
|
33
|
+
* max_tokens: 1024,
|
|
34
|
+
* messages: [{ role: 'user', content: 'Hello!' }],
|
|
35
|
+
* usageMetadata: { // TypeScript recognizes this natively
|
|
36
|
+
* subscriber: { id: 'user-123', email: 'user@example.com' },
|
|
37
|
+
* organizationId: 'my-company',
|
|
38
|
+
* taskType: 'customer-support',
|
|
39
|
+
* traceId: 'session-abc-123'
|
|
40
|
+
* }
|
|
41
|
+
* });
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* ### Streaming Usage:
|
|
45
|
+
* ```typescript
|
|
46
|
+
* const stream = await anthropic.messages.stream({
|
|
47
|
+
* model: 'claude-3-5-sonnet-latest',
|
|
48
|
+
* max_tokens: 1024,
|
|
49
|
+
* messages: [{ role: 'user', content: 'Generate a report' }],
|
|
50
|
+
* usageMetadata: {
|
|
51
|
+
* taskType: 'content-generation',
|
|
52
|
+
* productId: 'report-generator',
|
|
53
|
+
* responseQualityScore: 0.95
|
|
54
|
+
* }
|
|
55
|
+
* });
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* ### Advanced Usage with All Fields:
|
|
59
|
+
* ```typescript
|
|
60
|
+
* const response = await anthropic.messages.create({
|
|
61
|
+
* model: 'claude-3-5-sonnet-latest',
|
|
62
|
+
* max_tokens: 2048,
|
|
63
|
+
* messages: [{ role: 'user', content: 'Complex analysis task' }],
|
|
64
|
+
* usageMetadata: {
|
|
65
|
+
* subscriber: {
|
|
66
|
+
* id: 'user-456',
|
|
67
|
+
* email: 'analyst@company.com',
|
|
68
|
+
* credential: { name: 'api-key', value: 'sk-...' }
|
|
69
|
+
* },
|
|
70
|
+
* traceId: 'analysis-session-789',
|
|
71
|
+
* taskId: 'task-001',
|
|
72
|
+
* taskType: 'data-analysis',
|
|
73
|
+
* organizationId: 'enterprise-client',
|
|
74
|
+
* subscriptionId: 'premium-plan',
|
|
75
|
+
* productId: 'analytics-suite',
|
|
76
|
+
* agent: 'data-analyst-bot',
|
|
77
|
+
* responseQualityScore: 0.98,
|
|
78
|
+
* customField: 'custom-value' // Extensible with custom fields
|
|
79
|
+
* }
|
|
80
|
+
* });
|
|
81
|
+
* ```
|
|
82
|
+
*
|
|
83
|
+
* @public
|
|
84
|
+
* @since 1.1.0
|
|
85
|
+
*/
|
|
86
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
87
|
+
//# sourceMappingURL=anthropic-augmentation.js.map
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Circuit breaker pattern implementation for handling repeated failures
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.CircuitBreaker = exports.DEFAULT_CIRCUIT_CONFIG = exports.CircuitState = void 0;
|
|
7
|
+
exports.getCircuitBreaker = getCircuitBreaker;
|
|
8
|
+
exports.resetCircuitBreaker = resetCircuitBreaker;
|
|
9
|
+
exports.canExecuteRequest = canExecuteRequest;
|
|
10
|
+
exports.executeWithCircuitBreaker = executeWithCircuitBreaker;
|
|
11
|
+
exports.getCircuitBreakerStats = getCircuitBreakerStats;
|
|
12
|
+
const constants_1 = require("../constants");
|
|
13
|
+
/**
|
|
14
|
+
* Circuit breaker states
|
|
15
|
+
*/
|
|
16
|
+
var CircuitState;
|
|
17
|
+
(function (CircuitState) {
|
|
18
|
+
CircuitState["CLOSED"] = "CLOSED";
|
|
19
|
+
CircuitState["OPEN"] = "OPEN";
|
|
20
|
+
CircuitState["HALF_OPEN"] = "HALF_OPEN"; // Testing if service recovered
|
|
21
|
+
})(CircuitState || (exports.CircuitState = CircuitState = {}));
|
|
22
|
+
/**
|
|
23
|
+
* Default circuit breaker configuration
|
|
24
|
+
*/
|
|
25
|
+
exports.DEFAULT_CIRCUIT_CONFIG = {
|
|
26
|
+
failureThreshold: constants_1.CIRCUIT_BREAKER_CONFIG.FAILURE_THRESHOLD,
|
|
27
|
+
recoveryTimeout: constants_1.CIRCUIT_BREAKER_CONFIG.RECOVERY_TIMEOUT,
|
|
28
|
+
successThreshold: constants_1.CIRCUIT_BREAKER_CONFIG.SUCCESS_THRESHOLD,
|
|
29
|
+
timeWindow: constants_1.CIRCUIT_BREAKER_CONFIG.TIME_WINDOW
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Circuit breaker implementation
|
|
33
|
+
*/
|
|
34
|
+
class CircuitBreaker {
|
|
35
|
+
constructor(config = exports.DEFAULT_CIRCUIT_CONFIG) {
|
|
36
|
+
this.config = config;
|
|
37
|
+
this.state = CircuitState.CLOSED;
|
|
38
|
+
this.failureCount = 0;
|
|
39
|
+
this.successCount = 0;
|
|
40
|
+
this.lastFailureTime = 0;
|
|
41
|
+
this.failures = []; // Timestamps of failures
|
|
42
|
+
this.lastCleanupTime = 0;
|
|
43
|
+
this.maxFailureHistorySize = constants_1.CIRCUIT_BREAKER_CONFIG.MAX_FAILURE_HISTORY_SIZE;
|
|
44
|
+
this.lastCleanupTime = Date.now();
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Execute a function with circuit breaker protection
|
|
48
|
+
*/
|
|
49
|
+
async execute(fn) {
|
|
50
|
+
if (this.state === CircuitState.OPEN) {
|
|
51
|
+
if (this.shouldAttemptRecovery()) {
|
|
52
|
+
this.state = CircuitState.HALF_OPEN;
|
|
53
|
+
this.successCount = 0;
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
throw new Error('Circuit breaker is OPEN - failing fast');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const result = await fn();
|
|
61
|
+
this.onSuccess();
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
this.onFailure();
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Check if circuit breaker allows execution
|
|
71
|
+
*/
|
|
72
|
+
canExecute() {
|
|
73
|
+
if (this.state === CircuitState.CLOSED || this.state === CircuitState.HALF_OPEN)
|
|
74
|
+
return true;
|
|
75
|
+
if (this.state === CircuitState.OPEN && this.shouldAttemptRecovery())
|
|
76
|
+
return true;
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Get current circuit breaker state
|
|
81
|
+
*/
|
|
82
|
+
getState() {
|
|
83
|
+
return this.state;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Get circuit breaker statistics
|
|
87
|
+
*/
|
|
88
|
+
getStats() {
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
const recentFailures = this.failures.filter(timestamp => now - timestamp < this.config.timeWindow).length;
|
|
91
|
+
let timeUntilRecovery;
|
|
92
|
+
if (this.state === CircuitState.OPEN) {
|
|
93
|
+
const timeSinceLastFailure = now - this.lastFailureTime;
|
|
94
|
+
timeUntilRecovery = Math.max(0, this.config.recoveryTimeout - timeSinceLastFailure);
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
state: this.state,
|
|
98
|
+
failureCount: this.failureCount,
|
|
99
|
+
successCount: this.successCount,
|
|
100
|
+
recentFailures,
|
|
101
|
+
timeUntilRecovery
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Reset circuit breaker to closed state
|
|
106
|
+
*/
|
|
107
|
+
reset() {
|
|
108
|
+
this.state = CircuitState.CLOSED;
|
|
109
|
+
this.failureCount = 0;
|
|
110
|
+
this.successCount = 0;
|
|
111
|
+
this.lastFailureTime = 0;
|
|
112
|
+
this.lastCleanupTime = Date.now();
|
|
113
|
+
this.failures = [];
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Handle successful execution
|
|
117
|
+
*/
|
|
118
|
+
onSuccess() {
|
|
119
|
+
if (this.state === CircuitState.HALF_OPEN) {
|
|
120
|
+
this.successCount++;
|
|
121
|
+
if (this.successCount >= this.config.successThreshold) {
|
|
122
|
+
this.state = CircuitState.CLOSED;
|
|
123
|
+
this.failureCount = 0;
|
|
124
|
+
this.failures = [];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else if (this.state === CircuitState.CLOSED) {
|
|
128
|
+
// Reset failure count on success in closed state
|
|
129
|
+
this.failureCount = 0;
|
|
130
|
+
this.performPeriodicCleanup();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Handle failed execution
|
|
135
|
+
*/
|
|
136
|
+
onFailure() {
|
|
137
|
+
const now = Date.now();
|
|
138
|
+
this.failureCount++;
|
|
139
|
+
this.lastFailureTime = now;
|
|
140
|
+
// Prevent unbounded growth of failures array
|
|
141
|
+
if (this.failures.length >= this.maxFailureHistorySize) {
|
|
142
|
+
this.failures = this.failures.slice(-Math.floor(this.maxFailureHistorySize / 2));
|
|
143
|
+
}
|
|
144
|
+
this.failures.push(now);
|
|
145
|
+
this.cleanupOldFailures();
|
|
146
|
+
if (this.state === CircuitState.HALF_OPEN) {
|
|
147
|
+
// Go back to open state on any failure in half-open
|
|
148
|
+
this.state = CircuitState.OPEN;
|
|
149
|
+
this.successCount = 0;
|
|
150
|
+
}
|
|
151
|
+
else if (this.state === CircuitState.CLOSED) {
|
|
152
|
+
// Check if we should open the circuit
|
|
153
|
+
const recentFailures = this.failures.filter(timestamp => now - timestamp < this.config.timeWindow).length;
|
|
154
|
+
if (recentFailures >= this.config.failureThreshold) {
|
|
155
|
+
this.state = CircuitState.OPEN;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Check if we should attempt recovery from open state
|
|
161
|
+
*/
|
|
162
|
+
shouldAttemptRecovery() {
|
|
163
|
+
const now = Date.now();
|
|
164
|
+
return now - this.lastFailureTime >= this.config.recoveryTimeout;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Remove old failure timestamps outside the time window
|
|
168
|
+
*/
|
|
169
|
+
cleanupOldFailures() {
|
|
170
|
+
const now = Date.now();
|
|
171
|
+
this.failures = this.failures.filter(timestamp => now - timestamp < this.config.timeWindow);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Perform periodic cleanup to prevent memory leaks
|
|
175
|
+
* Only runs cleanup if enough time has passed since last cleanup
|
|
176
|
+
*/
|
|
177
|
+
performPeriodicCleanup() {
|
|
178
|
+
const now = Date.now();
|
|
179
|
+
const timeSinceLastCleanup = now - this.lastCleanupTime;
|
|
180
|
+
// Use constants for cleanup thresholds
|
|
181
|
+
if (timeSinceLastCleanup > constants_1.CIRCUIT_BREAKER_CONFIG.CLEANUP_INTERVAL ||
|
|
182
|
+
this.failures.length > constants_1.CIRCUIT_BREAKER_CONFIG.CLEANUP_SIZE_THRESHOLD) {
|
|
183
|
+
this.cleanupOldFailures();
|
|
184
|
+
this.lastCleanupTime = now;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
exports.CircuitBreaker = CircuitBreaker;
|
|
189
|
+
/**
|
|
190
|
+
* Global circuit breaker instance for Revenium API calls
|
|
191
|
+
*/
|
|
192
|
+
let globalCircuitBreaker = null;
|
|
193
|
+
/**
|
|
194
|
+
* Get or create the global circuit breaker instance
|
|
195
|
+
*/
|
|
196
|
+
function getCircuitBreaker(config) {
|
|
197
|
+
if (!globalCircuitBreaker) {
|
|
198
|
+
const finalConfig = config ? { ...exports.DEFAULT_CIRCUIT_CONFIG, ...config } : exports.DEFAULT_CIRCUIT_CONFIG;
|
|
199
|
+
globalCircuitBreaker = new CircuitBreaker(finalConfig);
|
|
200
|
+
}
|
|
201
|
+
return globalCircuitBreaker;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Reset the global circuit breaker
|
|
205
|
+
*/
|
|
206
|
+
function resetCircuitBreaker() {
|
|
207
|
+
if (globalCircuitBreaker) {
|
|
208
|
+
globalCircuitBreaker.reset();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Check if the global circuit breaker allows execution
|
|
213
|
+
*/
|
|
214
|
+
function canExecuteRequest() {
|
|
215
|
+
const circuitBreaker = getCircuitBreaker();
|
|
216
|
+
return circuitBreaker.canExecute();
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Execute a function with global circuit breaker protection
|
|
220
|
+
*/
|
|
221
|
+
async function executeWithCircuitBreaker(fn) {
|
|
222
|
+
const circuitBreaker = getCircuitBreaker();
|
|
223
|
+
return circuitBreaker.execute(fn);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Get global circuit breaker statistics
|
|
227
|
+
*/
|
|
228
|
+
function getCircuitBreakerStats() {
|
|
229
|
+
const circuitBreaker = getCircuitBreaker();
|
|
230
|
+
return circuitBreaker.getStats();
|
|
231
|
+
}
|
|
232
|
+
//# sourceMappingURL=circuit-breaker.js.map
|