@tamyla/clodo-framework 4.0.14 ā 4.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.
- package/CHANGELOG.md +19 -0
- package/dist/config/service-schema-config.js +22 -0
- package/dist/index.js +5 -0
- package/dist/monitoring/HealthChecker.js +286 -0
- package/dist/orchestration/modules/StateManager.js +11 -3
- package/dist/programmatic/createService.js +9 -0
- package/dist/security/ConfigurationValidator.js +216 -0
- package/dist/service-management/ServiceOrchestrator.js +48 -0
- package/dist/service-management/generators/BaseGenerator.js +31 -6
- package/dist/service-management/generators/utils/FileWriter.js +13 -1
- package/dist/services/ServiceClient.js +239 -0
- package/dist/utils/CircuitBreaker.js +192 -0
- package/dist/utils/EnvironmentValidator.js +147 -0
- package/dist/utils/TemplateRuntime.js +291 -0
- package/dist/utils/deployment/secret-generator.js +37 -26
- package/dist/validation/payloadValidation.js +159 -0
- package/dist/version/FrameworkInfo.js +104 -0
- package/dist/worker/integration.js +13 -1
- package/package.json +7 -2
- package/scripts/check-templates.js +105 -0
- package/scripts/debug-generate-worker.js +58 -0
- package/scripts/debug_validate_payload.mjs +12 -0
- package/scripts/migration/migrate-middleware-legacy-to-contract.js +18 -1
- package/scripts/verify-persistence-config.js +45 -0
- package/templates/generic/src/config/.config-is-sample +1 -0
- package/templates/static-site/.env.example +1 -1
- package/templates/static-site/src/config/.config-is-sample +1 -0
- package/templates/static-site/src/config/domains.js +3 -0
|
@@ -180,6 +180,9 @@ export class BaseGenerator {
|
|
|
180
180
|
const fullPath = path.join(this.servicePath, relativePath);
|
|
181
181
|
const overwrite = options.overwrite !== false; // Default to true
|
|
182
182
|
|
|
183
|
+
// Debug: log the target file path for diagnosis
|
|
184
|
+
this.logger.info(`BaseGenerator: writing file -> ${fullPath}`);
|
|
185
|
+
|
|
183
186
|
// Check if file exists and overwrite is disabled
|
|
184
187
|
try {
|
|
185
188
|
await fs.access(fullPath);
|
|
@@ -197,13 +200,35 @@ export class BaseGenerator {
|
|
|
197
200
|
recursive: true
|
|
198
201
|
});
|
|
199
202
|
|
|
200
|
-
// Write file
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
203
|
+
// Write file with retry logic for filesystem stability
|
|
204
|
+
const maxRetries = 3;
|
|
205
|
+
let lastError;
|
|
206
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
207
|
+
try {
|
|
208
|
+
this.logger.info(`BaseGenerator: attempt ${attempt} - writing to ${fullPath}`);
|
|
209
|
+
await fs.writeFile(fullPath, content, 'utf8');
|
|
210
|
+
|
|
211
|
+
// Add a small delay to let file system settle, especially on Windows
|
|
212
|
+
if (attempt === 1) {
|
|
213
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Verify the file was written successfully by checking it exists
|
|
217
|
+
await fs.access(fullPath, fs.constants.F_OK);
|
|
218
|
+
this.logger.info(`Generated: ${relativePath}`);
|
|
219
|
+
return; // Success
|
|
220
|
+
} catch (error) {
|
|
221
|
+
lastError = error;
|
|
222
|
+
this.logger.warn(`Write attempt ${attempt}/${maxRetries} failed for ${relativePath}: ${error.message}`);
|
|
223
|
+
if (attempt < maxRetries) {
|
|
224
|
+
// Wait before retry (exponential backoff)
|
|
225
|
+
await new Promise(resolve => setTimeout(resolve, 50 * attempt));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
206
228
|
}
|
|
229
|
+
|
|
230
|
+
// If all retries failed, throw the last error
|
|
231
|
+
throw new Error(`Failed to write and verify file '${relativePath}' after ${maxRetries} attempts: ${lastError.message}`);
|
|
207
232
|
}
|
|
208
233
|
|
|
209
234
|
/**
|
|
@@ -69,12 +69,22 @@ export class FileWriter {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
// Ensure parent directory exists
|
|
72
|
-
|
|
72
|
+
const dirPath = path.dirname(fullPath);
|
|
73
|
+
await this.ensureDirectory(dirPath);
|
|
73
74
|
|
|
74
75
|
// Write file atomically (write to temp file, then rename)
|
|
75
76
|
const tempPath = `${fullPath}.tmp`;
|
|
76
77
|
try {
|
|
78
|
+
// Write to temp file first
|
|
77
79
|
await fs.writeFile(tempPath, content, encoding);
|
|
80
|
+
|
|
81
|
+
// Small delay to ensure file is fully written on Windows
|
|
82
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
83
|
+
|
|
84
|
+
// Verify temp file exists before rename (prevents ENOENT)
|
|
85
|
+
await fs.access(tempPath);
|
|
86
|
+
|
|
87
|
+
// Rename to final destination
|
|
78
88
|
await fs.rename(tempPath, fullPath);
|
|
79
89
|
this.writtenFiles.push(fullPath);
|
|
80
90
|
return {
|
|
@@ -106,6 +116,8 @@ export class FileWriter {
|
|
|
106
116
|
await fs.mkdir(dirPath, {
|
|
107
117
|
recursive: true
|
|
108
118
|
});
|
|
119
|
+
// Verify the directory was created
|
|
120
|
+
await fs.access(dirPath);
|
|
109
121
|
} catch (error) {
|
|
110
122
|
// Ignore error if directory already exists
|
|
111
123
|
if (error.code !== 'EEXIST') {
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service Client for Inter-Service Communication
|
|
3
|
+
* Provides circuit breaker, retry logic, and service discovery
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { CircuitBreaker } from '../utils/CircuitBreaker.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Service Client for inter-service communication
|
|
10
|
+
*/
|
|
11
|
+
export class ServiceClient {
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
this.options = {
|
|
14
|
+
timeout: options.timeout || 10000,
|
|
15
|
+
retries: options.retries || 3,
|
|
16
|
+
retryDelay: options.retryDelay || 1000,
|
|
17
|
+
circuitBreakerEnabled: options.circuitBreakerEnabled !== false,
|
|
18
|
+
serviceDiscovery: options.serviceDiscovery || this.defaultServiceDiscovery,
|
|
19
|
+
...options
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Initialize circuit breaker if enabled
|
|
23
|
+
if (this.options.circuitBreakerEnabled) {
|
|
24
|
+
this.circuitBreaker = new CircuitBreaker({
|
|
25
|
+
failureThreshold: options.failureThreshold || 5,
|
|
26
|
+
recoveryTimeout: options.recoveryTimeout || 60000,
|
|
27
|
+
monitoringPeriod: options.monitoringPeriod || 10000
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Service URL cache
|
|
32
|
+
this.serviceCache = new Map();
|
|
33
|
+
this.cacheTimeout = options.cacheTimeout || 300000; // 5 minutes
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Make a call to another service
|
|
38
|
+
* @param {string} serviceName - Name of the target service
|
|
39
|
+
* @param {string} endpoint - API endpoint path
|
|
40
|
+
* @param {Object} options - Call options
|
|
41
|
+
* @returns {Promise<Object>} Response data
|
|
42
|
+
*/
|
|
43
|
+
async call(serviceName, endpoint, options = {}) {
|
|
44
|
+
const callOptions = {
|
|
45
|
+
method: options.method || 'GET',
|
|
46
|
+
headers: options.headers || {},
|
|
47
|
+
body: options.body,
|
|
48
|
+
timeout: options.timeout || this.options.timeout,
|
|
49
|
+
retries: options.retries || this.options.retries,
|
|
50
|
+
...options
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Get service URL
|
|
54
|
+
const serviceUrl = await this.getServiceUrl(serviceName);
|
|
55
|
+
if (!serviceUrl) {
|
|
56
|
+
throw new Error(`Service ${serviceName} not found`);
|
|
57
|
+
}
|
|
58
|
+
const fullUrl = `${serviceUrl}${endpoint}`;
|
|
59
|
+
|
|
60
|
+
// Check circuit breaker
|
|
61
|
+
if (this.circuitBreaker && !this.circuitBreaker.canExecute(serviceName)) {
|
|
62
|
+
throw new Error(`Circuit breaker open for service ${serviceName}`);
|
|
63
|
+
}
|
|
64
|
+
let lastError;
|
|
65
|
+
|
|
66
|
+
// Retry logic
|
|
67
|
+
for (let attempt = 0; attempt <= callOptions.retries; attempt++) {
|
|
68
|
+
try {
|
|
69
|
+
const response = await this.makeRequest(fullUrl, callOptions);
|
|
70
|
+
|
|
71
|
+
// Record success for circuit breaker
|
|
72
|
+
if (this.circuitBreaker) {
|
|
73
|
+
this.circuitBreaker.recordSuccess(serviceName);
|
|
74
|
+
}
|
|
75
|
+
return response;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
lastError = error;
|
|
78
|
+
|
|
79
|
+
// Record failure for circuit breaker
|
|
80
|
+
if (this.circuitBreaker) {
|
|
81
|
+
this.circuitBreaker.recordFailure(serviceName);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Don't retry on the last attempt
|
|
85
|
+
if (attempt === callOptions.retries) {
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Wait before retry
|
|
90
|
+
const delay = this.options.retryDelay * Math.pow(2, attempt); // Exponential backoff
|
|
91
|
+
await this.sleep(delay);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
throw lastError;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Make an HTTP request with timeout
|
|
99
|
+
* @param {string} url - Request URL
|
|
100
|
+
* @param {Object} options - Request options
|
|
101
|
+
* @returns {Promise<Object>} Response data
|
|
102
|
+
*/
|
|
103
|
+
async makeRequest(url, options) {
|
|
104
|
+
const controller = new AbortController();
|
|
105
|
+
const timeoutId = setTimeout(() => controller.abort(), options.timeout);
|
|
106
|
+
try {
|
|
107
|
+
const fetchOptions = {
|
|
108
|
+
method: options.method,
|
|
109
|
+
headers: {
|
|
110
|
+
'Content-Type': 'application/json',
|
|
111
|
+
...options.headers
|
|
112
|
+
},
|
|
113
|
+
signal: controller.signal
|
|
114
|
+
};
|
|
115
|
+
if (options.body && typeof options.body === 'object') {
|
|
116
|
+
fetchOptions.body = JSON.stringify(options.body);
|
|
117
|
+
} else if (options.body) {
|
|
118
|
+
fetchOptions.body = options.body;
|
|
119
|
+
}
|
|
120
|
+
const response = await fetch(url, fetchOptions);
|
|
121
|
+
clearTimeout(timeoutId);
|
|
122
|
+
if (!response.ok) {
|
|
123
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
124
|
+
}
|
|
125
|
+
const contentType = response.headers.get('content-type');
|
|
126
|
+
if (contentType && contentType.includes('application/json')) {
|
|
127
|
+
return await response.json();
|
|
128
|
+
} else {
|
|
129
|
+
return await response.text();
|
|
130
|
+
}
|
|
131
|
+
} catch (error) {
|
|
132
|
+
clearTimeout(timeoutId);
|
|
133
|
+
if (error.name === 'AbortError') {
|
|
134
|
+
throw new Error(`Request timeout after ${options.timeout}ms`);
|
|
135
|
+
}
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get service URL using service discovery
|
|
142
|
+
* @param {string} serviceName - Name of the service
|
|
143
|
+
* @returns {Promise<string|null>} Service URL or null if not found
|
|
144
|
+
*/
|
|
145
|
+
async getServiceUrl(serviceName) {
|
|
146
|
+
// Check cache first
|
|
147
|
+
const cached = this.serviceCache.get(serviceName);
|
|
148
|
+
if (cached && Date.now() - cached.timestamp < this.cacheTimeout) {
|
|
149
|
+
return cached.url;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Use service discovery
|
|
153
|
+
const url = await this.options.serviceDiscovery(serviceName);
|
|
154
|
+
if (url) {
|
|
155
|
+
this.serviceCache.set(serviceName, {
|
|
156
|
+
url: url,
|
|
157
|
+
timestamp: Date.now()
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return url;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Default service discovery implementation
|
|
165
|
+
* Uses environment variables for service URLs
|
|
166
|
+
* @param {string} serviceName - Name of the service
|
|
167
|
+
* @returns {Promise<string|null>} Service URL
|
|
168
|
+
*/
|
|
169
|
+
async defaultServiceDiscovery(serviceName) {
|
|
170
|
+
// Try environment-based discovery
|
|
171
|
+
const envKey = `SERVICE_URL_${serviceName.toUpperCase().replace(/-/g, '_')}`;
|
|
172
|
+
const envUrl = process.env[envKey];
|
|
173
|
+
if (envUrl) {
|
|
174
|
+
return envUrl;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Try development localhost patterns
|
|
178
|
+
if (process.env.NODE_ENV === 'development') {
|
|
179
|
+
const localhostUrl = `http://localhost:8787`; // Default Cloudflare Workers dev port
|
|
180
|
+
return localhostUrl;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Try production Cloudflare Workers pattern
|
|
184
|
+
const productionUrl = `https://${serviceName}.your-domain.workers.dev`;
|
|
185
|
+
return productionUrl;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Health check for a service
|
|
190
|
+
* @param {string} serviceName - Name of the service
|
|
191
|
+
* @returns {Promise<boolean>} True if service is healthy
|
|
192
|
+
*/
|
|
193
|
+
async healthCheck(serviceName) {
|
|
194
|
+
try {
|
|
195
|
+
await this.call(serviceName, '/health', {
|
|
196
|
+
timeout: 5000,
|
|
197
|
+
retries: 0
|
|
198
|
+
});
|
|
199
|
+
return true;
|
|
200
|
+
} catch (error) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Get circuit breaker status
|
|
207
|
+
* @param {string} serviceName - Name of the service
|
|
208
|
+
* @returns {Object} Circuit breaker status
|
|
209
|
+
*/
|
|
210
|
+
getCircuitBreakerStatus(serviceName) {
|
|
211
|
+
if (!this.circuitBreaker) {
|
|
212
|
+
return {
|
|
213
|
+
enabled: false
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
return this.circuitBreaker.getStatus(serviceName);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Clear service URL cache
|
|
221
|
+
* @param {string} serviceName - Specific service to clear (optional)
|
|
222
|
+
*/
|
|
223
|
+
clearCache(serviceName = null) {
|
|
224
|
+
if (serviceName) {
|
|
225
|
+
this.serviceCache.delete(serviceName);
|
|
226
|
+
} else {
|
|
227
|
+
this.serviceCache.clear();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Sleep utility
|
|
233
|
+
* @param {number} ms - Milliseconds to sleep
|
|
234
|
+
* @returns {Promise<void>}
|
|
235
|
+
*/
|
|
236
|
+
sleep(ms) {
|
|
237
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Circuit Breaker Implementation
|
|
3
|
+
* Prevents cascading failures in distributed systems
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Circuit Breaker for resilient service communication
|
|
8
|
+
*/
|
|
9
|
+
export class CircuitBreaker {
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
this.options = {
|
|
12
|
+
failureThreshold: options.failureThreshold || 5,
|
|
13
|
+
recoveryTimeout: options.recoveryTimeout || 60000,
|
|
14
|
+
monitoringPeriod: options.monitoringPeriod || 10000,
|
|
15
|
+
...options
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// State: 'closed', 'open', 'half-open'
|
|
19
|
+
this.state = 'closed';
|
|
20
|
+
|
|
21
|
+
// Failure tracking
|
|
22
|
+
this.failureCount = 0;
|
|
23
|
+
this.lastFailureTime = null;
|
|
24
|
+
this.nextAttemptTime = null;
|
|
25
|
+
|
|
26
|
+
// Success tracking for half-open state
|
|
27
|
+
this.successCount = 0;
|
|
28
|
+
this.requiredSuccessCount = options.requiredSuccessCount || 3;
|
|
29
|
+
|
|
30
|
+
// Service-specific tracking
|
|
31
|
+
this.services = new Map();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Check if an operation can be executed
|
|
36
|
+
* @param {string} serviceName - Name of the service
|
|
37
|
+
* @returns {boolean} True if operation can proceed
|
|
38
|
+
*/
|
|
39
|
+
canExecute(serviceName) {
|
|
40
|
+
const serviceState = this.getServiceState(serviceName);
|
|
41
|
+
switch (serviceState.state) {
|
|
42
|
+
case 'closed':
|
|
43
|
+
return true;
|
|
44
|
+
case 'open':
|
|
45
|
+
if (Date.now() >= serviceState.nextAttemptTime) {
|
|
46
|
+
// Transition to half-open
|
|
47
|
+
this.setServiceState(serviceName, 'half-open');
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
case 'half-open':
|
|
52
|
+
return true;
|
|
53
|
+
default:
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Record a successful operation
|
|
60
|
+
* @param {string} serviceName - Name of the service
|
|
61
|
+
*/
|
|
62
|
+
recordSuccess(serviceName) {
|
|
63
|
+
const serviceState = this.getServiceState(serviceName);
|
|
64
|
+
switch (serviceState.state) {
|
|
65
|
+
case 'half-open':
|
|
66
|
+
serviceState.successCount++;
|
|
67
|
+
if (serviceState.successCount >= this.requiredSuccessCount) {
|
|
68
|
+
// Transition back to closed
|
|
69
|
+
this.setServiceState(serviceName, 'closed');
|
|
70
|
+
}
|
|
71
|
+
break;
|
|
72
|
+
case 'closed':
|
|
73
|
+
// Reset failure count on success
|
|
74
|
+
serviceState.failureCount = 0;
|
|
75
|
+
serviceState.lastFailureTime = null;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Record a failed operation
|
|
82
|
+
* @param {string} serviceName - Name of the service
|
|
83
|
+
*/
|
|
84
|
+
recordFailure(serviceName) {
|
|
85
|
+
const serviceState = this.getServiceState(serviceName);
|
|
86
|
+
serviceState.failureCount++;
|
|
87
|
+
serviceState.lastFailureTime = Date.now();
|
|
88
|
+
if (serviceState.failureCount >= this.options.failureThreshold) {
|
|
89
|
+
// Transition to open
|
|
90
|
+
this.setServiceState(serviceName, 'open');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get the current state for a service
|
|
96
|
+
* @param {string} serviceName - Name of the service
|
|
97
|
+
* @returns {Object} Service state information
|
|
98
|
+
*/
|
|
99
|
+
getStatus(serviceName) {
|
|
100
|
+
const serviceState = this.getServiceState(serviceName);
|
|
101
|
+
return {
|
|
102
|
+
service: serviceName,
|
|
103
|
+
state: serviceState.state,
|
|
104
|
+
failureCount: serviceState.failureCount,
|
|
105
|
+
lastFailureTime: serviceState.lastFailureTime,
|
|
106
|
+
nextAttemptTime: serviceState.nextAttemptTime,
|
|
107
|
+
successCount: serviceState.successCount,
|
|
108
|
+
canExecute: this.canExecute(serviceName)
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get service state (internal)
|
|
114
|
+
* @param {string} serviceName - Name of the service
|
|
115
|
+
* @returns {Object} Service state
|
|
116
|
+
*/
|
|
117
|
+
getServiceState(serviceName) {
|
|
118
|
+
if (!this.services.has(serviceName)) {
|
|
119
|
+
this.services.set(serviceName, {
|
|
120
|
+
state: 'closed',
|
|
121
|
+
failureCount: 0,
|
|
122
|
+
lastFailureTime: null,
|
|
123
|
+
nextAttemptTime: null,
|
|
124
|
+
successCount: 0
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return this.services.get(serviceName);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Set service state (internal)
|
|
132
|
+
* @param {string} serviceName - Name of the service
|
|
133
|
+
* @param {string} state - New state
|
|
134
|
+
*/
|
|
135
|
+
setServiceState(serviceName, state) {
|
|
136
|
+
const serviceState = this.getServiceState(serviceName);
|
|
137
|
+
serviceState.state = state;
|
|
138
|
+
if (state === 'open') {
|
|
139
|
+
serviceState.nextAttemptTime = Date.now() + this.options.recoveryTimeout;
|
|
140
|
+
serviceState.successCount = 0;
|
|
141
|
+
} else if (state === 'half-open') {
|
|
142
|
+
serviceState.successCount = 0;
|
|
143
|
+
} else if (state === 'closed') {
|
|
144
|
+
serviceState.failureCount = 0;
|
|
145
|
+
serviceState.lastFailureTime = null;
|
|
146
|
+
serviceState.nextAttemptTime = null;
|
|
147
|
+
serviceState.successCount = 0;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Manually reset a service's circuit breaker
|
|
153
|
+
* @param {string} serviceName - Name of the service
|
|
154
|
+
*/
|
|
155
|
+
reset(serviceName) {
|
|
156
|
+
this.setServiceState(serviceName, 'closed');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Force a service into open state
|
|
161
|
+
* @param {string} serviceName - Name of the service
|
|
162
|
+
*/
|
|
163
|
+
trip(serviceName) {
|
|
164
|
+
this.setServiceState(serviceName, 'open');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Get all service statuses
|
|
169
|
+
* @returns {Object} All service statuses
|
|
170
|
+
*/
|
|
171
|
+
getAllStatuses() {
|
|
172
|
+
const statuses = {};
|
|
173
|
+
for (const [serviceName] of this.services) {
|
|
174
|
+
statuses[serviceName] = this.getStatus(serviceName);
|
|
175
|
+
}
|
|
176
|
+
return statuses;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Clean up old service states (optional maintenance)
|
|
181
|
+
*/
|
|
182
|
+
cleanup() {
|
|
183
|
+
const now = Date.now();
|
|
184
|
+
const maxAge = this.options.monitoringPeriod * 10; // 10 monitoring periods
|
|
185
|
+
|
|
186
|
+
for (const [serviceName, serviceState] of this.services.entries()) {
|
|
187
|
+
if (serviceState.lastFailureTime && now - serviceState.lastFailureTime > maxAge && serviceState.state === 'closed') {
|
|
188
|
+
this.services.delete(serviceName);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment Variable Validator
|
|
3
|
+
* Validates required environment variables at runtime
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Environment Variable Validation
|
|
8
|
+
*/
|
|
9
|
+
export class EnvironmentValidator {
|
|
10
|
+
/**
|
|
11
|
+
* Common required environment variables for Cloudflare services
|
|
12
|
+
*/
|
|
13
|
+
static COMMON_REQUIRED_VARS = ['CLOUDFLARE_ACCOUNT_ID', 'CLOUDFLARE_API_TOKEN'];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Validate that all required environment variables are set
|
|
17
|
+
* @param {string[]} requiredVars - Array of required environment variable names
|
|
18
|
+
* @param {Object} env - Environment object (defaults to process.env)
|
|
19
|
+
* @throws {Error} Throws descriptive error if any required vars are missing
|
|
20
|
+
*/
|
|
21
|
+
static validateRequired(requiredVars, env = process.env) {
|
|
22
|
+
const missing = [];
|
|
23
|
+
const empty = [];
|
|
24
|
+
for (const varName of requiredVars) {
|
|
25
|
+
const value = env[varName];
|
|
26
|
+
if (value === undefined || value === null) {
|
|
27
|
+
missing.push(varName);
|
|
28
|
+
} else if (typeof value === 'string' && value.trim() === '') {
|
|
29
|
+
empty.push(varName);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (missing.length > 0 || empty.length > 0) {
|
|
33
|
+
const errors = [];
|
|
34
|
+
if (missing.length > 0) {
|
|
35
|
+
errors.push(`Missing required environment variables: ${missing.join(', ')}`);
|
|
36
|
+
}
|
|
37
|
+
if (empty.length > 0) {
|
|
38
|
+
errors.push(`Empty required environment variables: ${empty.join(', ')}`);
|
|
39
|
+
}
|
|
40
|
+
const errorMessage = `Environment validation failed:\n${errors.join('\n')}\n\nPlease set these variables in your wrangler.toml or deployment environment.`;
|
|
41
|
+
throw new Error(errorMessage);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Validate Cloudflare-specific environment variables
|
|
47
|
+
* @param {Object} env - Environment object (defaults to process.env)
|
|
48
|
+
* @throws {Error} Throws error if Cloudflare vars are missing
|
|
49
|
+
*/
|
|
50
|
+
static validateCloudflareVars(env = process.env) {
|
|
51
|
+
const cloudflareVars = ['CLOUDFLARE_ACCOUNT_ID', 'CLOUDFLARE_API_TOKEN'];
|
|
52
|
+
this.validateRequired(cloudflareVars, env);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Validate service-specific environment variables
|
|
57
|
+
* @param {string[]} serviceVars - Service-specific required variables
|
|
58
|
+
* @param {Object} env - Environment object (defaults to process.env)
|
|
59
|
+
* @throws {Error} Throws error if service vars are missing
|
|
60
|
+
*/
|
|
61
|
+
static validateServiceVars(serviceVars, env = process.env) {
|
|
62
|
+
this.validateRequired(serviceVars, env);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Validate all environment variables for a service
|
|
67
|
+
* @param {Object} config - Service configuration with required vars
|
|
68
|
+
* @param {Object} env - Environment object (defaults to process.env)
|
|
69
|
+
* @throws {Error} Throws error if any vars are missing
|
|
70
|
+
*/
|
|
71
|
+
static validateServiceConfig(config, env = process.env) {
|
|
72
|
+
const requiredVars = [];
|
|
73
|
+
|
|
74
|
+
// Add common Cloudflare vars
|
|
75
|
+
requiredVars.push(...this.COMMON_REQUIRED_VARS);
|
|
76
|
+
|
|
77
|
+
// Add service-specific vars from config
|
|
78
|
+
if (config.requiredEnvironmentVars) {
|
|
79
|
+
requiredVars.push(...config.requiredEnvironmentVars);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Add feature-specific vars
|
|
83
|
+
if (config.features) {
|
|
84
|
+
if (config.features.d1) {
|
|
85
|
+
// D1 might require additional vars, but usually handled by wrangler
|
|
86
|
+
}
|
|
87
|
+
if (config.features.kv) {
|
|
88
|
+
// KV vars are usually handled by wrangler binding
|
|
89
|
+
}
|
|
90
|
+
if (config.features.r2) {
|
|
91
|
+
// R2 vars are usually handled by wrangler binding
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
this.validateRequired(requiredVars, env);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get a summary of environment variable status
|
|
99
|
+
* @param {string[]} vars - Variables to check
|
|
100
|
+
* @param {Object} env - Environment object (defaults to process.env)
|
|
101
|
+
* @returns {Object} Status summary
|
|
102
|
+
*/
|
|
103
|
+
static getStatus(vars, env = process.env) {
|
|
104
|
+
const status = {
|
|
105
|
+
total: vars.length,
|
|
106
|
+
set: 0,
|
|
107
|
+
missing: [],
|
|
108
|
+
empty: []
|
|
109
|
+
};
|
|
110
|
+
for (const varName of vars) {
|
|
111
|
+
const value = env[varName];
|
|
112
|
+
if (value === undefined || value === null) {
|
|
113
|
+
status.missing.push(varName);
|
|
114
|
+
} else if (typeof value === 'string' && value.trim() === '') {
|
|
115
|
+
status.empty.push(varName);
|
|
116
|
+
} else {
|
|
117
|
+
status.set++;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
status.valid = status.missing.length === 0 && status.empty.length === 0;
|
|
121
|
+
return status;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Log environment variable validation results
|
|
126
|
+
* @param {string[]} vars - Variables that were checked
|
|
127
|
+
* @param {Object} status - Status from getStatus()
|
|
128
|
+
*/
|
|
129
|
+
static logValidationResults(vars, status) {
|
|
130
|
+
console.log('\nš Environment Variable Validation');
|
|
131
|
+
console.log('='.repeat(40));
|
|
132
|
+
if (status.valid) {
|
|
133
|
+
console.log(`ā
PASSED - All ${status.total} variables are set`);
|
|
134
|
+
} else {
|
|
135
|
+
console.log(`ā ISSUES - ${status.missing.length + status.empty.length} problems found`);
|
|
136
|
+
if (status.missing.length > 0) {
|
|
137
|
+
console.log('\nš MISSING VARIABLES:');
|
|
138
|
+
status.missing.forEach(variable => console.log(` - ${variable}`));
|
|
139
|
+
}
|
|
140
|
+
if (status.empty.length > 0) {
|
|
141
|
+
console.log('\nš EMPTY VARIABLES:');
|
|
142
|
+
status.empty.forEach(variable => console.log(` - ${variable}`));
|
|
143
|
+
}
|
|
144
|
+
console.log('\nš” Set these in your wrangler.toml [vars] section or deployment environment');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|