@push.rocks/smartregistry 2.2.3 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/cargo/classes.cargoregistry.d.ts +7 -1
  3. package/dist_ts/cargo/classes.cargoregistry.js +42 -4
  4. package/dist_ts/cargo/classes.cargoupstream.d.ts +44 -0
  5. package/dist_ts/cargo/classes.cargoupstream.js +129 -0
  6. package/dist_ts/cargo/index.d.ts +1 -0
  7. package/dist_ts/cargo/index.js +2 -1
  8. package/dist_ts/classes.smartregistry.js +8 -8
  9. package/dist_ts/composer/classes.composerregistry.d.ts +7 -1
  10. package/dist_ts/composer/classes.composerregistry.js +34 -3
  11. package/dist_ts/composer/classes.composerupstream.d.ts +40 -0
  12. package/dist_ts/composer/classes.composerupstream.js +159 -0
  13. package/dist_ts/composer/index.d.ts +1 -0
  14. package/dist_ts/composer/index.js +2 -1
  15. package/dist_ts/core/interfaces.core.d.ts +3 -0
  16. package/dist_ts/index.d.ts +1 -0
  17. package/dist_ts/index.js +3 -1
  18. package/dist_ts/maven/classes.mavenregistry.d.ts +12 -1
  19. package/dist_ts/maven/classes.mavenregistry.js +69 -4
  20. package/dist_ts/maven/classes.mavenupstream.d.ts +45 -0
  21. package/dist_ts/maven/classes.mavenupstream.js +153 -0
  22. package/dist_ts/maven/index.d.ts +1 -0
  23. package/dist_ts/maven/index.js +2 -1
  24. package/dist_ts/npm/classes.npmregistry.d.ts +3 -1
  25. package/dist_ts/npm/classes.npmregistry.js +55 -6
  26. package/dist_ts/npm/classes.npmupstream.d.ts +51 -0
  27. package/dist_ts/npm/classes.npmupstream.js +206 -0
  28. package/dist_ts/npm/index.d.ts +1 -0
  29. package/dist_ts/npm/index.js +2 -1
  30. package/dist_ts/oci/classes.ociregistry.d.ts +4 -1
  31. package/dist_ts/oci/classes.ociregistry.js +78 -17
  32. package/dist_ts/oci/classes.ociupstream.d.ts +62 -0
  33. package/dist_ts/oci/classes.ociupstream.js +206 -0
  34. package/dist_ts/oci/index.d.ts +1 -0
  35. package/dist_ts/oci/index.js +2 -1
  36. package/dist_ts/plugins.d.ts +4 -1
  37. package/dist_ts/plugins.js +6 -2
  38. package/dist_ts/pypi/classes.pypiregistry.d.ts +7 -1
  39. package/dist_ts/pypi/classes.pypiregistry.js +60 -4
  40. package/dist_ts/pypi/classes.pypiupstream.d.ts +48 -0
  41. package/dist_ts/pypi/classes.pypiupstream.js +165 -0
  42. package/dist_ts/pypi/index.d.ts +1 -0
  43. package/dist_ts/pypi/index.js +2 -1
  44. package/dist_ts/rubygems/classes.rubygemsregistry.d.ts +7 -1
  45. package/dist_ts/rubygems/classes.rubygemsregistry.js +35 -4
  46. package/dist_ts/rubygems/classes.rubygemsupstream.d.ts +47 -0
  47. package/dist_ts/rubygems/classes.rubygemsupstream.js +184 -0
  48. package/dist_ts/rubygems/index.d.ts +1 -0
  49. package/dist_ts/rubygems/index.js +2 -1
  50. package/dist_ts/upstream/classes.baseupstream.d.ts +112 -0
  51. package/dist_ts/upstream/classes.baseupstream.js +409 -0
  52. package/dist_ts/upstream/classes.circuitbreaker.d.ts +111 -0
  53. package/dist_ts/upstream/classes.circuitbreaker.js +192 -0
  54. package/dist_ts/upstream/classes.upstreamcache.d.ts +123 -0
  55. package/dist_ts/upstream/classes.upstreamcache.js +328 -0
  56. package/dist_ts/upstream/index.d.ts +6 -0
  57. package/dist_ts/upstream/index.js +7 -0
  58. package/dist_ts/upstream/interfaces.upstream.d.ts +169 -0
  59. package/dist_ts/upstream/interfaces.upstream.js +23 -0
  60. package/package.json +4 -2
  61. package/ts/00_commitinfo_data.ts +1 -1
  62. package/ts/cargo/classes.cargoregistry.ts +48 -3
  63. package/ts/cargo/classes.cargoupstream.ts +159 -0
  64. package/ts/cargo/index.ts +1 -0
  65. package/ts/classes.smartregistry.ts +49 -7
  66. package/ts/composer/classes.composerregistry.ts +39 -2
  67. package/ts/composer/classes.composerupstream.ts +200 -0
  68. package/ts/composer/index.ts +1 -0
  69. package/ts/core/interfaces.core.ts +3 -0
  70. package/ts/index.ts +3 -0
  71. package/ts/maven/classes.mavenregistry.ts +84 -3
  72. package/ts/maven/classes.mavenupstream.ts +220 -0
  73. package/ts/maven/index.ts +1 -0
  74. package/ts/npm/classes.npmregistry.ts +61 -5
  75. package/ts/npm/classes.npmupstream.ts +260 -0
  76. package/ts/npm/index.ts +1 -0
  77. package/ts/oci/classes.ociregistry.ts +89 -17
  78. package/ts/oci/classes.ociupstream.ts +263 -0
  79. package/ts/oci/index.ts +1 -0
  80. package/ts/plugins.ts +7 -1
  81. package/ts/pypi/classes.pypiregistry.ts +68 -3
  82. package/ts/pypi/classes.pypiupstream.ts +211 -0
  83. package/ts/pypi/index.ts +1 -0
  84. package/ts/rubygems/classes.rubygemsregistry.ts +40 -3
  85. package/ts/rubygems/classes.rubygemsupstream.ts +230 -0
  86. package/ts/rubygems/index.ts +1 -0
  87. package/ts/upstream/classes.baseupstream.ts +521 -0
  88. package/ts/upstream/classes.circuitbreaker.ts +238 -0
  89. package/ts/upstream/classes.upstreamcache.ts +423 -0
  90. package/ts/upstream/index.ts +11 -0
  91. package/ts/upstream/interfaces.upstream.ts +195 -0
@@ -4,5 +4,6 @@
4
4
  */
5
5
  export * from './interfaces.rubygems.js';
6
6
  export * from './classes.rubygemsregistry.js';
7
+ export { RubygemsUpstream } from './classes.rubygemsupstream.js';
7
8
  export * as rubygemsHelpers from './helpers.rubygems.js';
8
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9ydWJ5Z2Vtcy9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7O0dBR0c7QUFFSCxjQUFjLDBCQUEwQixDQUFDO0FBQ3pDLGNBQWMsK0JBQStCLENBQUM7QUFDOUMsT0FBTyxLQUFLLGVBQWUsTUFBTSx1QkFBdUIsQ0FBQyJ9
9
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9ydWJ5Z2Vtcy9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7O0dBR0c7QUFFSCxjQUFjLDBCQUEwQixDQUFDO0FBQ3pDLGNBQWMsK0JBQStCLENBQUM7QUFDOUMsT0FBTyxFQUFFLGdCQUFnQixFQUFFLE1BQU0sK0JBQStCLENBQUM7QUFDakUsT0FBTyxLQUFLLGVBQWUsTUFBTSx1QkFBdUIsQ0FBQyJ9
@@ -0,0 +1,112 @@
1
+ import * as plugins from '../plugins.js';
2
+ import type { IUpstreamRegistryConfig, IUpstreamAuthConfig, IUpstreamCacheConfig, IUpstreamResilienceConfig, IUpstreamResult, IUpstreamFetchContext, IProtocolUpstreamConfig, IUpstreamScopeRule, TCircuitState } from './interfaces.upstream.js';
3
+ import { CircuitBreaker } 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 declare abstract class BaseUpstream {
18
+ /** Protocol name for logging */
19
+ protected abstract readonly protocolName: string;
20
+ /** Upstream configuration */
21
+ protected readonly config: IProtocolUpstreamConfig;
22
+ /** Resolved cache configuration */
23
+ protected readonly cacheConfig: IUpstreamCacheConfig;
24
+ /** Resolved resilience configuration */
25
+ protected readonly resilienceConfig: IUpstreamResilienceConfig;
26
+ /** Circuit breakers per upstream */
27
+ protected readonly circuitBreakers: Map<string, CircuitBreaker>;
28
+ /** Upstream cache */
29
+ protected readonly cache: UpstreamCache;
30
+ /** Logger instance */
31
+ protected readonly logger: plugins.smartlog.Smartlog;
32
+ constructor(config: IProtocolUpstreamConfig, logger?: plugins.smartlog.Smartlog);
33
+ /**
34
+ * Check if upstream is enabled.
35
+ */
36
+ isEnabled(): boolean;
37
+ /**
38
+ * Get all configured upstreams.
39
+ */
40
+ getUpstreams(): IUpstreamRegistryConfig[];
41
+ /**
42
+ * Get circuit breaker state for an upstream.
43
+ */
44
+ getCircuitState(upstreamId: string): TCircuitState | null;
45
+ /**
46
+ * Get cache statistics.
47
+ */
48
+ getCacheStats(): import("./classes.upstreamcache.js").ICacheStats;
49
+ /**
50
+ * Fetch a resource from upstreams.
51
+ * Tries upstreams in priority order, respecting circuit breakers and scope rules.
52
+ */
53
+ fetch(context: IUpstreamFetchContext): Promise<IUpstreamResult | null>;
54
+ /**
55
+ * Invalidate cache for a resource pattern.
56
+ */
57
+ invalidateCache(pattern: RegExp): number;
58
+ /**
59
+ * Clear all cache entries.
60
+ */
61
+ clearCache(): void;
62
+ /**
63
+ * Stop the upstream (cleanup resources).
64
+ */
65
+ stop(): void;
66
+ /**
67
+ * Get upstreams that apply to a resource, sorted by priority.
68
+ */
69
+ protected getApplicableUpstreams(resource: string): IUpstreamRegistryConfig[];
70
+ /**
71
+ * Check if a resource matches scope rules.
72
+ * Empty rules = match all.
73
+ */
74
+ protected matchesScopeRules(resource: string, rules?: IUpstreamScopeRule[]): boolean;
75
+ /**
76
+ * Fetch from a specific upstream with retry logic.
77
+ */
78
+ protected fetchFromUpstream(upstream: IUpstreamRegistryConfig, context: IUpstreamFetchContext): Promise<IUpstreamResult>;
79
+ /**
80
+ * Execute a single HTTP request to an upstream.
81
+ */
82
+ protected executeRequest(upstream: IUpstreamRegistryConfig, context: IUpstreamFetchContext, timeoutMs: number): Promise<Omit<IUpstreamResult, 'upstreamId' | 'fromCache' | 'latencyMs'>>;
83
+ /**
84
+ * Build the full URL for an upstream request.
85
+ * Subclasses can override for protocol-specific URL building.
86
+ */
87
+ protected buildUpstreamUrl(upstream: IUpstreamRegistryConfig, context: IUpstreamFetchContext): string;
88
+ /**
89
+ * Build headers including authentication.
90
+ */
91
+ protected buildHeaders(upstream: IUpstreamRegistryConfig, context: IUpstreamFetchContext): Record<string, string>;
92
+ /**
93
+ * Add authentication headers based on auth config.
94
+ */
95
+ protected addAuthHeaders(headers: Record<string, string>, auth: IUpstreamAuthConfig): void;
96
+ /**
97
+ * Check if an error should not be retried.
98
+ */
99
+ protected isNonRetryableError(error: unknown): boolean;
100
+ /**
101
+ * Calculate backoff delay with exponential backoff and jitter.
102
+ */
103
+ protected calculateBackoffDelay(attempt: number, baseDelayMs: number, maxDelayMs: number): number;
104
+ /**
105
+ * Sleep for a specified duration.
106
+ */
107
+ protected sleep(ms: number): Promise<void>;
108
+ /**
109
+ * Revalidate cache in background.
110
+ */
111
+ protected revalidateInBackground(context: IUpstreamFetchContext, upstreams: IUpstreamRegistryConfig[]): Promise<void>;
112
+ }
@@ -0,0 +1,409 @@
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
+ // Check cache first
83
+ const cached = this.cache.get(context);
84
+ if (cached && !cached.stale) {
85
+ return {
86
+ success: true,
87
+ status: 200,
88
+ headers: cached.headers,
89
+ body: cached.data,
90
+ upstreamId: cached.upstreamId,
91
+ fromCache: true,
92
+ latencyMs: 0,
93
+ };
94
+ }
95
+ // Check for negative cache (recent 404)
96
+ if (this.cache.hasNegative(context)) {
97
+ return {
98
+ success: false,
99
+ status: 404,
100
+ headers: {},
101
+ upstreamId: 'cache',
102
+ fromCache: true,
103
+ latencyMs: 0,
104
+ };
105
+ }
106
+ // Get applicable upstreams sorted by priority
107
+ const applicableUpstreams = this.getApplicableUpstreams(context.resource);
108
+ if (applicableUpstreams.length === 0) {
109
+ return null;
110
+ }
111
+ // If we have stale cache, return it immediately and revalidate in background
112
+ if (cached?.stale && this.cacheConfig.staleWhileRevalidate) {
113
+ // Fire and forget revalidation
114
+ this.revalidateInBackground(context, applicableUpstreams);
115
+ return {
116
+ success: true,
117
+ status: 200,
118
+ headers: cached.headers,
119
+ body: cached.data,
120
+ upstreamId: cached.upstreamId,
121
+ fromCache: true,
122
+ latencyMs: 0,
123
+ };
124
+ }
125
+ // Try each upstream in order
126
+ let lastError = null;
127
+ for (const upstream of applicableUpstreams) {
128
+ const breaker = this.circuitBreakers.get(upstream.id);
129
+ if (!breaker)
130
+ continue;
131
+ try {
132
+ const result = await withCircuitBreaker(breaker, () => this.fetchFromUpstream(upstream, context));
133
+ // Cache successful responses
134
+ if (result.success && result.body) {
135
+ 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);
136
+ }
137
+ // Cache 404 responses
138
+ if (result.status === 404) {
139
+ this.cache.setNegative(context, upstream.id);
140
+ }
141
+ return result;
142
+ }
143
+ catch (error) {
144
+ if (error instanceof CircuitOpenError) {
145
+ this.logger.log('debug', `Circuit open for upstream ${upstream.id}, trying next`);
146
+ }
147
+ else {
148
+ this.logger.log('warn', `Upstream ${upstream.id} failed: ${error.message}`);
149
+ }
150
+ lastError = error;
151
+ // Continue to next upstream
152
+ }
153
+ }
154
+ // All upstreams failed
155
+ if (lastError) {
156
+ this.logger.log('error', `All upstreams failed for ${context.resource}: ${lastError.message}`);
157
+ }
158
+ return null;
159
+ }
160
+ /**
161
+ * Invalidate cache for a resource pattern.
162
+ */
163
+ invalidateCache(pattern) {
164
+ return this.cache.invalidatePattern(pattern);
165
+ }
166
+ /**
167
+ * Clear all cache entries.
168
+ */
169
+ clearCache() {
170
+ this.cache.clear();
171
+ }
172
+ /**
173
+ * Stop the upstream (cleanup resources).
174
+ */
175
+ stop() {
176
+ this.cache.stop();
177
+ }
178
+ /**
179
+ * Get upstreams that apply to a resource, sorted by priority.
180
+ */
181
+ getApplicableUpstreams(resource) {
182
+ return this.config.upstreams
183
+ .filter(upstream => {
184
+ if (!upstream.enabled)
185
+ return false;
186
+ // Check circuit breaker
187
+ const breaker = this.circuitBreakers.get(upstream.id);
188
+ if (breaker && !breaker.canRequest())
189
+ return false;
190
+ // Check scope rules
191
+ return this.matchesScopeRules(resource, upstream.scopeRules);
192
+ })
193
+ .sort((a, b) => a.priority - b.priority);
194
+ }
195
+ /**
196
+ * Check if a resource matches scope rules.
197
+ * Empty rules = match all.
198
+ */
199
+ matchesScopeRules(resource, rules) {
200
+ if (!rules || rules.length === 0) {
201
+ return true;
202
+ }
203
+ // Process rules in order
204
+ // Start with default exclude (nothing matches)
205
+ let matched = false;
206
+ for (const rule of rules) {
207
+ const isMatch = plugins.minimatch(resource, rule.pattern);
208
+ if (isMatch) {
209
+ matched = rule.action === 'include';
210
+ }
211
+ }
212
+ return matched;
213
+ }
214
+ /**
215
+ * Fetch from a specific upstream with retry logic.
216
+ */
217
+ async fetchFromUpstream(upstream, context) {
218
+ const upstreamResilience = { ...this.resilienceConfig, ...upstream.resilience };
219
+ const startTime = Date.now();
220
+ let lastError = null;
221
+ for (let attempt = 0; attempt <= upstreamResilience.maxRetries; attempt++) {
222
+ try {
223
+ const result = await this.executeRequest(upstream, context, upstreamResilience.timeoutMs);
224
+ return {
225
+ ...result,
226
+ upstreamId: upstream.id,
227
+ fromCache: false,
228
+ latencyMs: Date.now() - startTime,
229
+ };
230
+ }
231
+ catch (error) {
232
+ lastError = error;
233
+ // Don't retry on 4xx errors (except 429)
234
+ if (this.isNonRetryableError(error)) {
235
+ break;
236
+ }
237
+ // Calculate delay with exponential backoff and jitter
238
+ if (attempt < upstreamResilience.maxRetries) {
239
+ const delay = this.calculateBackoffDelay(attempt, upstreamResilience.retryDelayMs, upstreamResilience.retryMaxDelayMs);
240
+ await this.sleep(delay);
241
+ }
242
+ }
243
+ }
244
+ throw lastError || new Error('Request failed');
245
+ }
246
+ /**
247
+ * Execute a single HTTP request to an upstream.
248
+ */
249
+ async executeRequest(upstream, context, timeoutMs) {
250
+ // Build the full URL
251
+ const url = this.buildUpstreamUrl(upstream, context);
252
+ // Build headers with auth
253
+ const headers = this.buildHeaders(upstream, context);
254
+ // Make the request using SmartRequest
255
+ const request = plugins.smartrequest.SmartRequest.create()
256
+ .url(url)
257
+ .method(context.method)
258
+ .headers(headers)
259
+ .timeout(timeoutMs)
260
+ .handle429Backoff({ maxRetries: 3, fallbackDelay: 1000, maxWaitTime: 30000 });
261
+ // Add query params if present
262
+ if (Object.keys(context.query).length > 0) {
263
+ request.query(context.query);
264
+ }
265
+ let response;
266
+ switch (context.method.toUpperCase()) {
267
+ case 'GET':
268
+ response = await request.get();
269
+ break;
270
+ case 'HEAD':
271
+ // SmartRequest doesn't have head(), use options
272
+ response = await request.method('HEAD').get();
273
+ break;
274
+ default:
275
+ response = await request.get();
276
+ }
277
+ // Parse response
278
+ const responseHeaders = {};
279
+ for (const [key, value] of Object.entries(response.headers)) {
280
+ responseHeaders[key.toLowerCase()] = Array.isArray(value) ? value[0] : value;
281
+ }
282
+ let body;
283
+ const contentType = responseHeaders['content-type'] || '';
284
+ if (response.ok) {
285
+ if (contentType.includes('application/json')) {
286
+ body = await response.json();
287
+ }
288
+ else {
289
+ const arrayBuffer = await response.arrayBuffer();
290
+ body = Buffer.from(arrayBuffer);
291
+ }
292
+ }
293
+ return {
294
+ success: response.ok,
295
+ status: response.status,
296
+ headers: responseHeaders,
297
+ body,
298
+ };
299
+ }
300
+ /**
301
+ * Build the full URL for an upstream request.
302
+ * Subclasses can override for protocol-specific URL building.
303
+ */
304
+ buildUpstreamUrl(upstream, context) {
305
+ // Remove leading slash if URL already has trailing slash
306
+ let path = context.path;
307
+ if (upstream.url.endsWith('/') && path.startsWith('/')) {
308
+ path = path.slice(1);
309
+ }
310
+ return `${upstream.url}${path}`;
311
+ }
312
+ /**
313
+ * Build headers including authentication.
314
+ */
315
+ buildHeaders(upstream, context) {
316
+ const headers = { ...context.headers };
317
+ // Remove host header (will be set by HTTP client)
318
+ delete headers['host'];
319
+ // Add authentication
320
+ this.addAuthHeaders(headers, upstream.auth);
321
+ return headers;
322
+ }
323
+ /**
324
+ * Add authentication headers based on auth config.
325
+ */
326
+ addAuthHeaders(headers, auth) {
327
+ switch (auth.type) {
328
+ case 'basic':
329
+ if (auth.username && auth.password) {
330
+ const credentials = Buffer.from(`${auth.username}:${auth.password}`).toString('base64');
331
+ headers['authorization'] = `Basic ${credentials}`;
332
+ }
333
+ break;
334
+ case 'bearer':
335
+ if (auth.token) {
336
+ headers['authorization'] = `Bearer ${auth.token}`;
337
+ }
338
+ break;
339
+ case 'api-key':
340
+ if (auth.token) {
341
+ const headerName = auth.headerName || 'authorization';
342
+ headers[headerName.toLowerCase()] = auth.token;
343
+ }
344
+ break;
345
+ case 'none':
346
+ default:
347
+ // No authentication
348
+ break;
349
+ }
350
+ }
351
+ /**
352
+ * Check if an error should not be retried.
353
+ */
354
+ isNonRetryableError(error) {
355
+ // Check for HTTP status errors
356
+ if (error && typeof error === 'object' && 'status' in error) {
357
+ const status = error.status;
358
+ // Don't retry 4xx errors except 429 (rate limited)
359
+ if (status >= 400 && status < 500 && status !== 429) {
360
+ return true;
361
+ }
362
+ }
363
+ return false;
364
+ }
365
+ /**
366
+ * Calculate backoff delay with exponential backoff and jitter.
367
+ */
368
+ calculateBackoffDelay(attempt, baseDelayMs, maxDelayMs) {
369
+ // Exponential backoff: delay = base * 2^attempt
370
+ const exponentialDelay = baseDelayMs * Math.pow(2, attempt);
371
+ // Cap at max delay
372
+ const cappedDelay = Math.min(exponentialDelay, maxDelayMs);
373
+ // Add jitter (±25%)
374
+ const jitter = cappedDelay * 0.25 * (Math.random() * 2 - 1);
375
+ return Math.floor(cappedDelay + jitter);
376
+ }
377
+ /**
378
+ * Sleep for a specified duration.
379
+ */
380
+ sleep(ms) {
381
+ return new Promise(resolve => setTimeout(resolve, ms));
382
+ }
383
+ /**
384
+ * Revalidate cache in background.
385
+ */
386
+ async revalidateInBackground(context, upstreams) {
387
+ try {
388
+ for (const upstream of upstreams) {
389
+ const breaker = this.circuitBreakers.get(upstream.id);
390
+ if (!breaker || !breaker.canRequest())
391
+ continue;
392
+ try {
393
+ const result = await withCircuitBreaker(breaker, () => this.fetchFromUpstream(upstream, context));
394
+ if (result.success && result.body) {
395
+ 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);
396
+ return; // Successfully revalidated
397
+ }
398
+ }
399
+ catch {
400
+ // Continue to next upstream
401
+ }
402
+ }
403
+ }
404
+ catch (error) {
405
+ this.logger.log('debug', `Background revalidation failed: ${error.message}`);
406
+ }
407
+ }
408
+ }
409
+ //# 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
+ }