@push.rocks/smartregistry 2.2.3 → 2.4.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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/cargo/classes.cargoregistry.d.ts +7 -1
- package/dist_ts/cargo/classes.cargoregistry.js +42 -4
- package/dist_ts/cargo/classes.cargoupstream.d.ts +44 -0
- package/dist_ts/cargo/classes.cargoupstream.js +129 -0
- package/dist_ts/cargo/index.d.ts +1 -0
- package/dist_ts/cargo/index.js +2 -1
- package/dist_ts/classes.smartregistry.d.ts +33 -2
- package/dist_ts/classes.smartregistry.js +45 -12
- package/dist_ts/composer/classes.composerregistry.d.ts +7 -1
- package/dist_ts/composer/classes.composerregistry.js +34 -3
- package/dist_ts/composer/classes.composerupstream.d.ts +40 -0
- package/dist_ts/composer/classes.composerupstream.js +159 -0
- package/dist_ts/composer/index.d.ts +1 -0
- package/dist_ts/composer/index.js +2 -1
- package/dist_ts/core/classes.authmanager.d.ts +30 -80
- package/dist_ts/core/classes.authmanager.js +63 -337
- package/dist_ts/core/classes.defaultauthprovider.d.ts +78 -0
- package/dist_ts/core/classes.defaultauthprovider.js +311 -0
- package/dist_ts/core/classes.registrystorage.d.ts +70 -4
- package/dist_ts/core/classes.registrystorage.js +165 -5
- package/dist_ts/core/index.d.ts +3 -0
- package/dist_ts/core/index.js +7 -2
- package/dist_ts/core/interfaces.auth.d.ts +83 -0
- package/dist_ts/core/interfaces.auth.js +2 -0
- package/dist_ts/core/interfaces.core.d.ts +38 -0
- package/dist_ts/core/interfaces.storage.d.ts +120 -0
- package/dist_ts/core/interfaces.storage.js +2 -0
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +3 -1
- package/dist_ts/maven/classes.mavenregistry.d.ts +12 -1
- package/dist_ts/maven/classes.mavenregistry.js +69 -4
- package/dist_ts/maven/classes.mavenupstream.d.ts +45 -0
- package/dist_ts/maven/classes.mavenupstream.js +153 -0
- package/dist_ts/maven/index.d.ts +1 -0
- package/dist_ts/maven/index.js +2 -1
- package/dist_ts/npm/classes.npmregistry.d.ts +3 -1
- package/dist_ts/npm/classes.npmregistry.js +55 -6
- package/dist_ts/npm/classes.npmupstream.d.ts +51 -0
- package/dist_ts/npm/classes.npmupstream.js +206 -0
- package/dist_ts/npm/index.d.ts +1 -0
- package/dist_ts/npm/index.js +2 -1
- package/dist_ts/oci/classes.ociregistry.d.ts +4 -1
- package/dist_ts/oci/classes.ociregistry.js +78 -17
- package/dist_ts/oci/classes.ociupstream.d.ts +62 -0
- package/dist_ts/oci/classes.ociupstream.js +206 -0
- package/dist_ts/oci/index.d.ts +1 -0
- package/dist_ts/oci/index.js +2 -1
- package/dist_ts/plugins.d.ts +4 -1
- package/dist_ts/plugins.js +6 -2
- package/dist_ts/pypi/classes.pypiregistry.d.ts +7 -1
- package/dist_ts/pypi/classes.pypiregistry.js +60 -4
- package/dist_ts/pypi/classes.pypiupstream.d.ts +48 -0
- package/dist_ts/pypi/classes.pypiupstream.js +165 -0
- package/dist_ts/pypi/index.d.ts +1 -0
- package/dist_ts/pypi/index.js +2 -1
- package/dist_ts/rubygems/classes.rubygemsregistry.d.ts +7 -1
- package/dist_ts/rubygems/classes.rubygemsregistry.js +35 -4
- package/dist_ts/rubygems/classes.rubygemsupstream.d.ts +47 -0
- package/dist_ts/rubygems/classes.rubygemsupstream.js +184 -0
- package/dist_ts/rubygems/index.d.ts +1 -0
- package/dist_ts/rubygems/index.js +2 -1
- package/dist_ts/upstream/classes.baseupstream.d.ts +112 -0
- package/dist_ts/upstream/classes.baseupstream.js +411 -0
- package/dist_ts/upstream/classes.circuitbreaker.d.ts +111 -0
- package/dist_ts/upstream/classes.circuitbreaker.js +192 -0
- package/dist_ts/upstream/classes.upstreamcache.d.ts +170 -0
- package/dist_ts/upstream/classes.upstreamcache.js +485 -0
- package/dist_ts/upstream/index.d.ts +6 -0
- package/dist_ts/upstream/index.js +7 -0
- package/dist_ts/upstream/interfaces.upstream.d.ts +169 -0
- package/dist_ts/upstream/interfaces.upstream.js +23 -0
- package/package.json +4 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/cargo/classes.cargoregistry.ts +48 -3
- package/ts/cargo/classes.cargoupstream.ts +159 -0
- package/ts/cargo/index.ts +1 -0
- package/ts/classes.smartregistry.ts +88 -11
- package/ts/composer/classes.composerregistry.ts +39 -2
- package/ts/composer/classes.composerupstream.ts +200 -0
- package/ts/composer/index.ts +1 -0
- package/ts/core/classes.authmanager.ts +74 -412
- package/ts/core/classes.defaultauthprovider.ts +393 -0
- package/ts/core/classes.registrystorage.ts +199 -5
- package/ts/core/index.ts +8 -1
- package/ts/core/interfaces.auth.ts +91 -0
- package/ts/core/interfaces.core.ts +42 -0
- package/ts/core/interfaces.storage.ts +130 -0
- package/ts/index.ts +3 -0
- package/ts/maven/classes.mavenregistry.ts +84 -3
- package/ts/maven/classes.mavenupstream.ts +220 -0
- package/ts/maven/index.ts +1 -0
- package/ts/npm/classes.npmregistry.ts +61 -5
- package/ts/npm/classes.npmupstream.ts +260 -0
- package/ts/npm/index.ts +1 -0
- package/ts/oci/classes.ociregistry.ts +89 -17
- package/ts/oci/classes.ociupstream.ts +263 -0
- package/ts/oci/index.ts +1 -0
- package/ts/plugins.ts +7 -1
- package/ts/pypi/classes.pypiregistry.ts +68 -3
- package/ts/pypi/classes.pypiupstream.ts +211 -0
- package/ts/pypi/index.ts +1 -0
- package/ts/rubygems/classes.rubygemsregistry.ts +40 -3
- package/ts/rubygems/classes.rubygemsupstream.ts +230 -0
- package/ts/rubygems/index.ts +1 -0
- package/ts/upstream/classes.baseupstream.ts +526 -0
- package/ts/upstream/classes.circuitbreaker.ts +238 -0
- package/ts/upstream/classes.upstreamcache.ts +626 -0
- package/ts/upstream/index.ts +11 -0
- package/ts/upstream/interfaces.upstream.ts +195 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import { DEFAULT_CACHE_CONFIG, DEFAULT_RESILIENCE_CONFIG, } from './interfaces.upstream.js';
|
|
3
|
+
import { CircuitBreaker, CircuitOpenError, withCircuitBreaker } from './classes.circuitbreaker.js';
|
|
4
|
+
import { UpstreamCache } from './classes.upstreamcache.js';
|
|
5
|
+
/**
|
|
6
|
+
* Base class for protocol-specific upstream implementations.
|
|
7
|
+
*
|
|
8
|
+
* Provides:
|
|
9
|
+
* - Multi-upstream routing with priority
|
|
10
|
+
* - Scope-based filtering (glob patterns)
|
|
11
|
+
* - Authentication handling
|
|
12
|
+
* - Circuit breaker per upstream
|
|
13
|
+
* - Caching with TTL
|
|
14
|
+
* - Retry with exponential backoff
|
|
15
|
+
* - 429 rate limit handling
|
|
16
|
+
*/
|
|
17
|
+
export class BaseUpstream {
|
|
18
|
+
/** Upstream configuration */
|
|
19
|
+
config;
|
|
20
|
+
/** Resolved cache configuration */
|
|
21
|
+
cacheConfig;
|
|
22
|
+
/** Resolved resilience configuration */
|
|
23
|
+
resilienceConfig;
|
|
24
|
+
/** Circuit breakers per upstream */
|
|
25
|
+
circuitBreakers = new Map();
|
|
26
|
+
/** Upstream cache */
|
|
27
|
+
cache;
|
|
28
|
+
/** Logger instance */
|
|
29
|
+
logger;
|
|
30
|
+
constructor(config, logger) {
|
|
31
|
+
this.config = config;
|
|
32
|
+
this.cacheConfig = { ...DEFAULT_CACHE_CONFIG, ...config.cache };
|
|
33
|
+
this.resilienceConfig = { ...DEFAULT_RESILIENCE_CONFIG, ...config.resilience };
|
|
34
|
+
this.cache = new UpstreamCache(this.cacheConfig);
|
|
35
|
+
this.logger = logger || new plugins.smartlog.Smartlog({
|
|
36
|
+
logContext: {
|
|
37
|
+
company: 'smartregistry',
|
|
38
|
+
companyunit: 'upstream',
|
|
39
|
+
environment: 'production',
|
|
40
|
+
runtime: 'node',
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
// Initialize circuit breakers for each upstream
|
|
44
|
+
for (const upstream of config.upstreams) {
|
|
45
|
+
const upstreamResilience = { ...this.resilienceConfig, ...upstream.resilience };
|
|
46
|
+
this.circuitBreakers.set(upstream.id, new CircuitBreaker(upstream.id, upstreamResilience));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Check if upstream is enabled.
|
|
51
|
+
*/
|
|
52
|
+
isEnabled() {
|
|
53
|
+
return this.config.enabled;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Get all configured upstreams.
|
|
57
|
+
*/
|
|
58
|
+
getUpstreams() {
|
|
59
|
+
return this.config.upstreams;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Get circuit breaker state for an upstream.
|
|
63
|
+
*/
|
|
64
|
+
getCircuitState(upstreamId) {
|
|
65
|
+
const breaker = this.circuitBreakers.get(upstreamId);
|
|
66
|
+
return breaker ? breaker.getState() : null;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Get cache statistics.
|
|
70
|
+
*/
|
|
71
|
+
getCacheStats() {
|
|
72
|
+
return this.cache.getStats();
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Fetch a resource from upstreams.
|
|
76
|
+
* Tries upstreams in priority order, respecting circuit breakers and scope rules.
|
|
77
|
+
*/
|
|
78
|
+
async fetch(context) {
|
|
79
|
+
if (!this.config.enabled) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
// Get applicable upstreams sorted by priority
|
|
83
|
+
const applicableUpstreams = this.getApplicableUpstreams(context.resource);
|
|
84
|
+
if (applicableUpstreams.length === 0) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
// Use the first applicable upstream's URL for cache key
|
|
88
|
+
const primaryUpstreamUrl = applicableUpstreams[0]?.url;
|
|
89
|
+
// Check cache first
|
|
90
|
+
const cached = await this.cache.get(context, primaryUpstreamUrl);
|
|
91
|
+
if (cached && !cached.stale) {
|
|
92
|
+
return {
|
|
93
|
+
success: true,
|
|
94
|
+
status: 200,
|
|
95
|
+
headers: cached.headers,
|
|
96
|
+
body: cached.data,
|
|
97
|
+
upstreamId: cached.upstreamId,
|
|
98
|
+
fromCache: true,
|
|
99
|
+
latencyMs: 0,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
// Check for negative cache (recent 404)
|
|
103
|
+
if (await this.cache.hasNegative(context, primaryUpstreamUrl)) {
|
|
104
|
+
return {
|
|
105
|
+
success: false,
|
|
106
|
+
status: 404,
|
|
107
|
+
headers: {},
|
|
108
|
+
upstreamId: 'cache',
|
|
109
|
+
fromCache: true,
|
|
110
|
+
latencyMs: 0,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// If we have stale cache, return it immediately and revalidate in background
|
|
114
|
+
if (cached?.stale && this.cacheConfig.staleWhileRevalidate) {
|
|
115
|
+
// Fire and forget revalidation
|
|
116
|
+
this.revalidateInBackground(context, applicableUpstreams);
|
|
117
|
+
return {
|
|
118
|
+
success: true,
|
|
119
|
+
status: 200,
|
|
120
|
+
headers: cached.headers,
|
|
121
|
+
body: cached.data,
|
|
122
|
+
upstreamId: cached.upstreamId,
|
|
123
|
+
fromCache: true,
|
|
124
|
+
latencyMs: 0,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
// Try each upstream in order
|
|
128
|
+
let lastError = null;
|
|
129
|
+
for (const upstream of applicableUpstreams) {
|
|
130
|
+
const breaker = this.circuitBreakers.get(upstream.id);
|
|
131
|
+
if (!breaker)
|
|
132
|
+
continue;
|
|
133
|
+
try {
|
|
134
|
+
const result = await withCircuitBreaker(breaker, () => this.fetchFromUpstream(upstream, context));
|
|
135
|
+
// Cache successful responses
|
|
136
|
+
if (result.success && result.body) {
|
|
137
|
+
await this.cache.set(context, Buffer.isBuffer(result.body) ? result.body : Buffer.from(JSON.stringify(result.body)), result.headers['content-type'] || 'application/octet-stream', result.headers, upstream.id, upstream.url);
|
|
138
|
+
}
|
|
139
|
+
// Cache 404 responses
|
|
140
|
+
if (result.status === 404) {
|
|
141
|
+
await this.cache.setNegative(context, upstream.id, upstream.url);
|
|
142
|
+
}
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
if (error instanceof CircuitOpenError) {
|
|
147
|
+
this.logger.log('debug', `Circuit open for upstream ${upstream.id}, trying next`);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
this.logger.log('warn', `Upstream ${upstream.id} failed: ${error.message}`);
|
|
151
|
+
}
|
|
152
|
+
lastError = error;
|
|
153
|
+
// Continue to next upstream
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// All upstreams failed
|
|
157
|
+
if (lastError) {
|
|
158
|
+
this.logger.log('error', `All upstreams failed for ${context.resource}: ${lastError.message}`);
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Invalidate cache for a resource pattern.
|
|
164
|
+
*/
|
|
165
|
+
async invalidateCache(pattern) {
|
|
166
|
+
return this.cache.invalidatePattern(pattern);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Clear all cache entries.
|
|
170
|
+
*/
|
|
171
|
+
async clearCache() {
|
|
172
|
+
await this.cache.clear();
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Stop the upstream (cleanup resources).
|
|
176
|
+
*/
|
|
177
|
+
stop() {
|
|
178
|
+
this.cache.stop();
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Get upstreams that apply to a resource, sorted by priority.
|
|
182
|
+
*/
|
|
183
|
+
getApplicableUpstreams(resource) {
|
|
184
|
+
return this.config.upstreams
|
|
185
|
+
.filter(upstream => {
|
|
186
|
+
if (!upstream.enabled)
|
|
187
|
+
return false;
|
|
188
|
+
// Check circuit breaker
|
|
189
|
+
const breaker = this.circuitBreakers.get(upstream.id);
|
|
190
|
+
if (breaker && !breaker.canRequest())
|
|
191
|
+
return false;
|
|
192
|
+
// Check scope rules
|
|
193
|
+
return this.matchesScopeRules(resource, upstream.scopeRules);
|
|
194
|
+
})
|
|
195
|
+
.sort((a, b) => a.priority - b.priority);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Check if a resource matches scope rules.
|
|
199
|
+
* Empty rules = match all.
|
|
200
|
+
*/
|
|
201
|
+
matchesScopeRules(resource, rules) {
|
|
202
|
+
if (!rules || rules.length === 0) {
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
// Process rules in order
|
|
206
|
+
// Start with default exclude (nothing matches)
|
|
207
|
+
let matched = false;
|
|
208
|
+
for (const rule of rules) {
|
|
209
|
+
const isMatch = plugins.minimatch(resource, rule.pattern);
|
|
210
|
+
if (isMatch) {
|
|
211
|
+
matched = rule.action === 'include';
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return matched;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Fetch from a specific upstream with retry logic.
|
|
218
|
+
*/
|
|
219
|
+
async fetchFromUpstream(upstream, context) {
|
|
220
|
+
const upstreamResilience = { ...this.resilienceConfig, ...upstream.resilience };
|
|
221
|
+
const startTime = Date.now();
|
|
222
|
+
let lastError = null;
|
|
223
|
+
for (let attempt = 0; attempt <= upstreamResilience.maxRetries; attempt++) {
|
|
224
|
+
try {
|
|
225
|
+
const result = await this.executeRequest(upstream, context, upstreamResilience.timeoutMs);
|
|
226
|
+
return {
|
|
227
|
+
...result,
|
|
228
|
+
upstreamId: upstream.id,
|
|
229
|
+
fromCache: false,
|
|
230
|
+
latencyMs: Date.now() - startTime,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
lastError = error;
|
|
235
|
+
// Don't retry on 4xx errors (except 429)
|
|
236
|
+
if (this.isNonRetryableError(error)) {
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
// Calculate delay with exponential backoff and jitter
|
|
240
|
+
if (attempt < upstreamResilience.maxRetries) {
|
|
241
|
+
const delay = this.calculateBackoffDelay(attempt, upstreamResilience.retryDelayMs, upstreamResilience.retryMaxDelayMs);
|
|
242
|
+
await this.sleep(delay);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
throw lastError || new Error('Request failed');
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Execute a single HTTP request to an upstream.
|
|
250
|
+
*/
|
|
251
|
+
async executeRequest(upstream, context, timeoutMs) {
|
|
252
|
+
// Build the full URL
|
|
253
|
+
const url = this.buildUpstreamUrl(upstream, context);
|
|
254
|
+
// Build headers with auth
|
|
255
|
+
const headers = this.buildHeaders(upstream, context);
|
|
256
|
+
// Make the request using SmartRequest
|
|
257
|
+
const request = plugins.smartrequest.SmartRequest.create()
|
|
258
|
+
.url(url)
|
|
259
|
+
.method(context.method)
|
|
260
|
+
.headers(headers)
|
|
261
|
+
.timeout(timeoutMs)
|
|
262
|
+
.handle429Backoff({ maxRetries: 3, fallbackDelay: 1000, maxWaitTime: 30000 });
|
|
263
|
+
// Add query params if present
|
|
264
|
+
if (Object.keys(context.query).length > 0) {
|
|
265
|
+
request.query(context.query);
|
|
266
|
+
}
|
|
267
|
+
let response;
|
|
268
|
+
switch (context.method.toUpperCase()) {
|
|
269
|
+
case 'GET':
|
|
270
|
+
response = await request.get();
|
|
271
|
+
break;
|
|
272
|
+
case 'HEAD':
|
|
273
|
+
// SmartRequest doesn't have head(), use options
|
|
274
|
+
response = await request.method('HEAD').get();
|
|
275
|
+
break;
|
|
276
|
+
default:
|
|
277
|
+
response = await request.get();
|
|
278
|
+
}
|
|
279
|
+
// Parse response
|
|
280
|
+
const responseHeaders = {};
|
|
281
|
+
for (const [key, value] of Object.entries(response.headers)) {
|
|
282
|
+
responseHeaders[key.toLowerCase()] = Array.isArray(value) ? value[0] : value;
|
|
283
|
+
}
|
|
284
|
+
let body;
|
|
285
|
+
const contentType = responseHeaders['content-type'] || '';
|
|
286
|
+
if (response.ok) {
|
|
287
|
+
if (contentType.includes('application/json')) {
|
|
288
|
+
body = await response.json();
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
292
|
+
body = Buffer.from(arrayBuffer);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return {
|
|
296
|
+
success: response.ok,
|
|
297
|
+
status: response.status,
|
|
298
|
+
headers: responseHeaders,
|
|
299
|
+
body,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Build the full URL for an upstream request.
|
|
304
|
+
* Subclasses can override for protocol-specific URL building.
|
|
305
|
+
*/
|
|
306
|
+
buildUpstreamUrl(upstream, context) {
|
|
307
|
+
// Remove leading slash if URL already has trailing slash
|
|
308
|
+
let path = context.path;
|
|
309
|
+
if (upstream.url.endsWith('/') && path.startsWith('/')) {
|
|
310
|
+
path = path.slice(1);
|
|
311
|
+
}
|
|
312
|
+
return `${upstream.url}${path}`;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Build headers including authentication.
|
|
316
|
+
*/
|
|
317
|
+
buildHeaders(upstream, context) {
|
|
318
|
+
const headers = { ...context.headers };
|
|
319
|
+
// Remove host header (will be set by HTTP client)
|
|
320
|
+
delete headers['host'];
|
|
321
|
+
// Add authentication
|
|
322
|
+
this.addAuthHeaders(headers, upstream.auth);
|
|
323
|
+
return headers;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Add authentication headers based on auth config.
|
|
327
|
+
*/
|
|
328
|
+
addAuthHeaders(headers, auth) {
|
|
329
|
+
switch (auth.type) {
|
|
330
|
+
case 'basic':
|
|
331
|
+
if (auth.username && auth.password) {
|
|
332
|
+
const credentials = Buffer.from(`${auth.username}:${auth.password}`).toString('base64');
|
|
333
|
+
headers['authorization'] = `Basic ${credentials}`;
|
|
334
|
+
}
|
|
335
|
+
break;
|
|
336
|
+
case 'bearer':
|
|
337
|
+
if (auth.token) {
|
|
338
|
+
headers['authorization'] = `Bearer ${auth.token}`;
|
|
339
|
+
}
|
|
340
|
+
break;
|
|
341
|
+
case 'api-key':
|
|
342
|
+
if (auth.token) {
|
|
343
|
+
const headerName = auth.headerName || 'authorization';
|
|
344
|
+
headers[headerName.toLowerCase()] = auth.token;
|
|
345
|
+
}
|
|
346
|
+
break;
|
|
347
|
+
case 'none':
|
|
348
|
+
default:
|
|
349
|
+
// No authentication
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Check if an error should not be retried.
|
|
355
|
+
*/
|
|
356
|
+
isNonRetryableError(error) {
|
|
357
|
+
// Check for HTTP status errors
|
|
358
|
+
if (error && typeof error === 'object' && 'status' in error) {
|
|
359
|
+
const status = error.status;
|
|
360
|
+
// Don't retry 4xx errors except 429 (rate limited)
|
|
361
|
+
if (status >= 400 && status < 500 && status !== 429) {
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Calculate backoff delay with exponential backoff and jitter.
|
|
369
|
+
*/
|
|
370
|
+
calculateBackoffDelay(attempt, baseDelayMs, maxDelayMs) {
|
|
371
|
+
// Exponential backoff: delay = base * 2^attempt
|
|
372
|
+
const exponentialDelay = baseDelayMs * Math.pow(2, attempt);
|
|
373
|
+
// Cap at max delay
|
|
374
|
+
const cappedDelay = Math.min(exponentialDelay, maxDelayMs);
|
|
375
|
+
// Add jitter (±25%)
|
|
376
|
+
const jitter = cappedDelay * 0.25 * (Math.random() * 2 - 1);
|
|
377
|
+
return Math.floor(cappedDelay + jitter);
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Sleep for a specified duration.
|
|
381
|
+
*/
|
|
382
|
+
sleep(ms) {
|
|
383
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Revalidate cache in background.
|
|
387
|
+
*/
|
|
388
|
+
async revalidateInBackground(context, upstreams) {
|
|
389
|
+
try {
|
|
390
|
+
for (const upstream of upstreams) {
|
|
391
|
+
const breaker = this.circuitBreakers.get(upstream.id);
|
|
392
|
+
if (!breaker || !breaker.canRequest())
|
|
393
|
+
continue;
|
|
394
|
+
try {
|
|
395
|
+
const result = await withCircuitBreaker(breaker, () => this.fetchFromUpstream(upstream, context));
|
|
396
|
+
if (result.success && result.body) {
|
|
397
|
+
await this.cache.set(context, Buffer.isBuffer(result.body) ? result.body : Buffer.from(JSON.stringify(result.body)), result.headers['content-type'] || 'application/octet-stream', result.headers, upstream.id, upstream.url);
|
|
398
|
+
return; // Successfully revalidated
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
// Continue to next upstream
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
catch (error) {
|
|
407
|
+
this.logger.log('debug', `Background revalidation failed: ${error.message}`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5iYXNldXBzdHJlYW0uanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy91cHN0cmVhbS9jbGFzc2VzLmJhc2V1cHN0cmVhbS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGVBQWUsQ0FBQztBQVl6QyxPQUFPLEVBQ0wsb0JBQW9CLEVBQ3BCLHlCQUF5QixHQUMxQixNQUFNLDBCQUEwQixDQUFDO0FBQ2xDLE9BQU8sRUFBRSxjQUFjLEVBQUUsZ0JBQWdCLEVBQUUsa0JBQWtCLEVBQUUsTUFBTSw2QkFBNkIsQ0FBQztBQUNuRyxPQUFPLEVBQUUsYUFBYSxFQUFFLE1BQU0sNEJBQTRCLENBQUM7QUFFM0Q7Ozs7Ozs7Ozs7O0dBV0c7QUFDSCxNQUFNLE9BQWdCLFlBQVk7SUFJaEMsNkJBQTZCO0lBQ1YsTUFBTSxDQUEwQjtJQUVuRCxtQ0FBbUM7SUFDaEIsV0FBVyxDQUF1QjtJQUVyRCx3Q0FBd0M7SUFDckIsZ0JBQWdCLENBQTRCO0lBRS9ELG9DQUFvQztJQUNqQixlQUFlLEdBQWdDLElBQUksR0FBRyxFQUFFLENBQUM7SUFFNUUscUJBQXFCO0lBQ0YsS0FBSyxDQUFnQjtJQUV4QyxzQkFBc0I7SUFDSCxNQUFNLENBQTRCO0lBRXJELFlBQVksTUFBK0IsRUFBRSxNQUFrQztRQUM3RSxJQUFJLENBQUMsTUFBTSxHQUFHLE1BQU0sQ0FBQztRQUNyQixJQUFJLENBQUMsV0FBVyxHQUFHLEVBQUUsR0FBRyxvQkFBb0IsRUFBRSxHQUFHLE1BQU0sQ0FBQyxLQUFLLEVBQUUsQ0FBQztRQUNoRSxJQUFJLENBQUMsZ0JBQWdCLEdBQUcsRUFBRSxHQUFHLHlCQUF5QixFQUFFLEdBQUcsTUFBTSxDQUFDLFVBQVUsRUFBRSxDQUFDO1FBQy9FLElBQUksQ0FBQyxLQUFLLEdBQUcsSUFBSSxhQUFhLENBQUMsSUFBSSxDQUFDLFdBQVcsQ0FBQyxDQUFDO1FBQ2pELElBQUksQ0FBQyxNQUFNLEdBQUcs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
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { TCircuitState, IUpstreamResilienceConfig } from './interfaces.upstream.js';
|
|
2
|
+
/**
|
|
3
|
+
* Circuit breaker implementation for upstream resilience.
|
|
4
|
+
*
|
|
5
|
+
* States:
|
|
6
|
+
* - CLOSED: Normal operation, requests pass through
|
|
7
|
+
* - OPEN: Circuit is tripped, requests fail fast
|
|
8
|
+
* - HALF_OPEN: Testing if upstream has recovered
|
|
9
|
+
*
|
|
10
|
+
* Transitions:
|
|
11
|
+
* - CLOSED → OPEN: When failure count exceeds threshold
|
|
12
|
+
* - OPEN → HALF_OPEN: After reset timeout expires
|
|
13
|
+
* - HALF_OPEN → CLOSED: On successful request
|
|
14
|
+
* - HALF_OPEN → OPEN: On failed request
|
|
15
|
+
*/
|
|
16
|
+
export declare class CircuitBreaker {
|
|
17
|
+
/** Unique identifier for logging and metrics */
|
|
18
|
+
readonly id: string;
|
|
19
|
+
/** Current circuit state */
|
|
20
|
+
private state;
|
|
21
|
+
/** Count of consecutive failures */
|
|
22
|
+
private failureCount;
|
|
23
|
+
/** Timestamp when circuit was opened */
|
|
24
|
+
private openedAt;
|
|
25
|
+
/** Number of successful requests in half-open state */
|
|
26
|
+
private halfOpenSuccesses;
|
|
27
|
+
/** Configuration */
|
|
28
|
+
private readonly config;
|
|
29
|
+
/** Number of successes required to close circuit from half-open */
|
|
30
|
+
private readonly halfOpenThreshold;
|
|
31
|
+
constructor(id: string, config?: Partial<IUpstreamResilienceConfig>);
|
|
32
|
+
/**
|
|
33
|
+
* Get current circuit state.
|
|
34
|
+
*/
|
|
35
|
+
getState(): TCircuitState;
|
|
36
|
+
/**
|
|
37
|
+
* Check if circuit allows requests.
|
|
38
|
+
* Returns true if requests should be allowed.
|
|
39
|
+
*/
|
|
40
|
+
canRequest(): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Record a successful request.
|
|
43
|
+
* May transition circuit from HALF_OPEN to CLOSED.
|
|
44
|
+
*/
|
|
45
|
+
recordSuccess(): void;
|
|
46
|
+
/**
|
|
47
|
+
* Record a failed request.
|
|
48
|
+
* May transition circuit from CLOSED/HALF_OPEN to OPEN.
|
|
49
|
+
*/
|
|
50
|
+
recordFailure(): void;
|
|
51
|
+
/**
|
|
52
|
+
* Force circuit to open state.
|
|
53
|
+
* Useful for manual intervention or external health checks.
|
|
54
|
+
*/
|
|
55
|
+
forceOpen(): void;
|
|
56
|
+
/**
|
|
57
|
+
* Force circuit to closed state.
|
|
58
|
+
* Useful for manual intervention after fixing upstream issues.
|
|
59
|
+
*/
|
|
60
|
+
forceClose(): void;
|
|
61
|
+
/**
|
|
62
|
+
* Reset circuit to initial state.
|
|
63
|
+
*/
|
|
64
|
+
reset(): void;
|
|
65
|
+
/**
|
|
66
|
+
* Get circuit metrics for monitoring.
|
|
67
|
+
*/
|
|
68
|
+
getMetrics(): ICircuitBreakerMetrics;
|
|
69
|
+
/**
|
|
70
|
+
* Transition to a new state with proper cleanup.
|
|
71
|
+
*/
|
|
72
|
+
private transitionTo;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Metrics for circuit breaker monitoring.
|
|
76
|
+
*/
|
|
77
|
+
export interface ICircuitBreakerMetrics {
|
|
78
|
+
/** Circuit breaker identifier */
|
|
79
|
+
id: string;
|
|
80
|
+
/** Current state */
|
|
81
|
+
state: TCircuitState;
|
|
82
|
+
/** Number of consecutive failures */
|
|
83
|
+
failureCount: number;
|
|
84
|
+
/** When circuit was opened (null if never opened) */
|
|
85
|
+
openedAt: Date | null;
|
|
86
|
+
/** Milliseconds until circuit transitions to half-open (0 if not open) */
|
|
87
|
+
timeUntilHalfOpen: number;
|
|
88
|
+
/** Number of successes in half-open state */
|
|
89
|
+
halfOpenSuccesses: number;
|
|
90
|
+
/** Failure threshold for opening circuit */
|
|
91
|
+
threshold: number;
|
|
92
|
+
/** Reset timeout in milliseconds */
|
|
93
|
+
resetMs: number;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Execute a function with circuit breaker protection.
|
|
97
|
+
*
|
|
98
|
+
* @param breaker The circuit breaker to use
|
|
99
|
+
* @param fn The async function to execute
|
|
100
|
+
* @param fallback Optional fallback function when circuit is open
|
|
101
|
+
* @returns The result of fn or fallback
|
|
102
|
+
* @throws CircuitOpenError if circuit is open and no fallback provided
|
|
103
|
+
*/
|
|
104
|
+
export declare function withCircuitBreaker<T>(breaker: CircuitBreaker, fn: () => Promise<T>, fallback?: () => Promise<T>): Promise<T>;
|
|
105
|
+
/**
|
|
106
|
+
* Error thrown when circuit is open and no fallback is provided.
|
|
107
|
+
*/
|
|
108
|
+
export declare class CircuitOpenError extends Error {
|
|
109
|
+
readonly circuitId: string;
|
|
110
|
+
constructor(circuitId: string);
|
|
111
|
+
}
|