@cleocode/lafs-protocol 1.0.0 → 1.2.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/README.md +7 -3
- package/dist/schemas/v1/envelope.schema.json +6 -0
- package/dist/src/a2a/bridge.d.ts +129 -0
- package/dist/src/a2a/bridge.js +173 -0
- package/dist/src/a2a/index.d.ts +36 -0
- package/dist/src/a2a/index.js +36 -0
- package/dist/src/circuit-breaker/index.d.ts +121 -0
- package/dist/src/circuit-breaker/index.js +249 -0
- package/dist/src/flagSemantics.d.ts +2 -0
- package/dist/src/flagSemantics.js +7 -6
- package/dist/src/health/index.d.ts +105 -0
- package/dist/src/health/index.js +211 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +6 -0
- package/dist/src/shutdown/index.d.ts +69 -0
- package/dist/src/shutdown/index.js +160 -0
- package/dist/src/types.d.ts +4 -0
- package/lafs.md +187 -0
- package/package.json +3 -2
- package/schemas/v1/envelope.schema.json +6 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LAFS Circuit Breaker Module
|
|
3
|
+
*
|
|
4
|
+
* Provides circuit breaker pattern for resilient service calls
|
|
5
|
+
*/
|
|
6
|
+
export class CircuitBreakerError extends Error {
|
|
7
|
+
constructor(message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = 'CircuitBreakerError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Circuit breaker for protecting against cascading failures
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* import { CircuitBreaker } from '@cleocode/lafs-protocol/circuit-breaker';
|
|
18
|
+
*
|
|
19
|
+
* const breaker = new CircuitBreaker({
|
|
20
|
+
* name: 'external-api',
|
|
21
|
+
* failureThreshold: 5,
|
|
22
|
+
* resetTimeout: 30000
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* try {
|
|
26
|
+
* const result = await breaker.execute(async () => {
|
|
27
|
+
* return await externalApi.call();
|
|
28
|
+
* });
|
|
29
|
+
* } catch (error) {
|
|
30
|
+
* if (error instanceof CircuitBreakerError) {
|
|
31
|
+
* console.log('Circuit breaker is open');
|
|
32
|
+
* }
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export class CircuitBreaker {
|
|
37
|
+
config;
|
|
38
|
+
state = 'CLOSED';
|
|
39
|
+
failures = 0;
|
|
40
|
+
successes = 0;
|
|
41
|
+
lastFailureTime;
|
|
42
|
+
consecutiveSuccesses = 0;
|
|
43
|
+
totalCalls = 0;
|
|
44
|
+
halfOpenCalls = 0;
|
|
45
|
+
resetTimer;
|
|
46
|
+
constructor(config) {
|
|
47
|
+
this.config = config;
|
|
48
|
+
this.config = {
|
|
49
|
+
failureThreshold: 5,
|
|
50
|
+
resetTimeout: 30000,
|
|
51
|
+
halfOpenMaxCalls: 3,
|
|
52
|
+
successThreshold: 2,
|
|
53
|
+
...config
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Execute a function with circuit breaker protection
|
|
58
|
+
*/
|
|
59
|
+
async execute(fn) {
|
|
60
|
+
this.totalCalls++;
|
|
61
|
+
if (this.state === 'OPEN') {
|
|
62
|
+
if (this.shouldAttemptReset()) {
|
|
63
|
+
this.transitionTo('HALF_OPEN');
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
throw new CircuitBreakerError(`Circuit breaker '${this.config.name}' is OPEN`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (this.state === 'HALF_OPEN') {
|
|
70
|
+
if (this.halfOpenCalls >= (this.config.halfOpenMaxCalls || 3)) {
|
|
71
|
+
throw new CircuitBreakerError(`Circuit breaker '${this.config.name}' is HALF_OPEN (max calls reached)`);
|
|
72
|
+
}
|
|
73
|
+
this.halfOpenCalls++;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const result = await fn();
|
|
77
|
+
this.onSuccess();
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
this.onFailure();
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Get current circuit breaker state
|
|
87
|
+
*/
|
|
88
|
+
getState() {
|
|
89
|
+
return this.state;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Get circuit breaker metrics
|
|
93
|
+
*/
|
|
94
|
+
getMetrics() {
|
|
95
|
+
return {
|
|
96
|
+
state: this.state,
|
|
97
|
+
failures: this.failures,
|
|
98
|
+
successes: this.successes,
|
|
99
|
+
lastFailureTime: this.lastFailureTime,
|
|
100
|
+
consecutiveSuccesses: this.consecutiveSuccesses,
|
|
101
|
+
totalCalls: this.totalCalls
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Manually open the circuit breaker
|
|
106
|
+
*/
|
|
107
|
+
forceOpen() {
|
|
108
|
+
this.transitionTo('OPEN');
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Manually close the circuit breaker
|
|
112
|
+
*/
|
|
113
|
+
forceClose() {
|
|
114
|
+
this.transitionTo('CLOSED');
|
|
115
|
+
this.reset();
|
|
116
|
+
}
|
|
117
|
+
onSuccess() {
|
|
118
|
+
this.successes++;
|
|
119
|
+
this.consecutiveSuccesses++;
|
|
120
|
+
if (this.state === 'HALF_OPEN') {
|
|
121
|
+
if (this.consecutiveSuccesses >= (this.config.successThreshold || 2)) {
|
|
122
|
+
this.transitionTo('CLOSED');
|
|
123
|
+
this.reset();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
onFailure() {
|
|
128
|
+
this.failures++;
|
|
129
|
+
this.consecutiveSuccesses = 0;
|
|
130
|
+
this.lastFailureTime = new Date();
|
|
131
|
+
if (this.state === 'HALF_OPEN') {
|
|
132
|
+
this.transitionTo('OPEN');
|
|
133
|
+
this.scheduleReset();
|
|
134
|
+
}
|
|
135
|
+
else if (this.state === 'CLOSED') {
|
|
136
|
+
if (this.failures >= (this.config.failureThreshold || 5)) {
|
|
137
|
+
this.transitionTo('OPEN');
|
|
138
|
+
this.scheduleReset();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
transitionTo(newState) {
|
|
143
|
+
console.log(`Circuit breaker '${this.config.name}': ${this.state} -> ${newState}`);
|
|
144
|
+
this.state = newState;
|
|
145
|
+
if (newState === 'HALF_OPEN') {
|
|
146
|
+
this.halfOpenCalls = 0;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
shouldAttemptReset() {
|
|
150
|
+
if (!this.lastFailureTime)
|
|
151
|
+
return true;
|
|
152
|
+
const elapsed = Date.now() - this.lastFailureTime.getTime();
|
|
153
|
+
return elapsed >= (this.config.resetTimeout || 30000);
|
|
154
|
+
}
|
|
155
|
+
scheduleReset() {
|
|
156
|
+
if (this.resetTimer) {
|
|
157
|
+
clearTimeout(this.resetTimer);
|
|
158
|
+
}
|
|
159
|
+
this.resetTimer = setTimeout(() => {
|
|
160
|
+
if (this.state === 'OPEN') {
|
|
161
|
+
this.transitionTo('HALF_OPEN');
|
|
162
|
+
}
|
|
163
|
+
}, this.config.resetTimeout || 30000);
|
|
164
|
+
}
|
|
165
|
+
reset() {
|
|
166
|
+
this.failures = 0;
|
|
167
|
+
this.consecutiveSuccesses = 0;
|
|
168
|
+
this.halfOpenCalls = 0;
|
|
169
|
+
if (this.resetTimer) {
|
|
170
|
+
clearTimeout(this.resetTimer);
|
|
171
|
+
this.resetTimer = undefined;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Circuit breaker registry for managing multiple breakers
|
|
177
|
+
*
|
|
178
|
+
* @example
|
|
179
|
+
* ```typescript
|
|
180
|
+
* const registry = new CircuitBreakerRegistry();
|
|
181
|
+
*
|
|
182
|
+
* registry.add('payment-api', {
|
|
183
|
+
* failureThreshold: 3,
|
|
184
|
+
* resetTimeout: 60000
|
|
185
|
+
* });
|
|
186
|
+
*
|
|
187
|
+
* const paymentBreaker = registry.get('payment-api');
|
|
188
|
+
* ```
|
|
189
|
+
*/
|
|
190
|
+
export class CircuitBreakerRegistry {
|
|
191
|
+
breakers = new Map();
|
|
192
|
+
add(name, config) {
|
|
193
|
+
const breaker = new CircuitBreaker({ ...config, name });
|
|
194
|
+
this.breakers.set(name, breaker);
|
|
195
|
+
return breaker;
|
|
196
|
+
}
|
|
197
|
+
get(name) {
|
|
198
|
+
return this.breakers.get(name);
|
|
199
|
+
}
|
|
200
|
+
getOrCreate(name, config) {
|
|
201
|
+
let breaker = this.breakers.get(name);
|
|
202
|
+
if (!breaker) {
|
|
203
|
+
breaker = this.add(name, config);
|
|
204
|
+
}
|
|
205
|
+
return breaker;
|
|
206
|
+
}
|
|
207
|
+
getAllMetrics() {
|
|
208
|
+
const metrics = {};
|
|
209
|
+
this.breakers.forEach((breaker, name) => {
|
|
210
|
+
metrics[name] = breaker.getMetrics();
|
|
211
|
+
});
|
|
212
|
+
return metrics;
|
|
213
|
+
}
|
|
214
|
+
resetAll() {
|
|
215
|
+
this.breakers.forEach(breaker => breaker.forceClose());
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Create a circuit breaker middleware for Express
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* ```typescript
|
|
223
|
+
* app.use('/external-api', circuitBreakerMiddleware({
|
|
224
|
+
* name: 'external-api',
|
|
225
|
+
* failureThreshold: 5
|
|
226
|
+
* }));
|
|
227
|
+
* ```
|
|
228
|
+
*/
|
|
229
|
+
export function circuitBreakerMiddleware(config) {
|
|
230
|
+
const breaker = new CircuitBreaker(config);
|
|
231
|
+
return async (req, res, next) => {
|
|
232
|
+
try {
|
|
233
|
+
await breaker.execute(async () => {
|
|
234
|
+
next();
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
if (error instanceof CircuitBreakerError) {
|
|
239
|
+
res.status(503).json({
|
|
240
|
+
error: 'Service temporarily unavailable',
|
|
241
|
+
reason: 'Circuit breaker is open'
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
throw error;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
}
|
|
@@ -2,6 +2,8 @@ import type { FlagInput } from "./types.js";
|
|
|
2
2
|
export interface FlagResolution {
|
|
3
3
|
format: "json" | "human";
|
|
4
4
|
source: "flag" | "project" | "user" | "default";
|
|
5
|
+
/** When true, suppress non-essential output for scripting */
|
|
6
|
+
quiet: boolean;
|
|
5
7
|
}
|
|
6
8
|
export declare class LAFSFlagError extends Error {
|
|
7
9
|
code: string;
|
|
@@ -10,20 +10,21 @@ export function resolveOutputFormat(input) {
|
|
|
10
10
|
if (input.humanFlag && input.jsonFlag) {
|
|
11
11
|
throw new LAFSFlagError("E_FORMAT_CONFLICT", "Cannot combine --human and --json in the same invocation.");
|
|
12
12
|
}
|
|
13
|
+
const quiet = input.quiet ?? false;
|
|
13
14
|
if (input.requestedFormat) {
|
|
14
|
-
return { format: input.requestedFormat, source: "flag" };
|
|
15
|
+
return { format: input.requestedFormat, source: "flag", quiet };
|
|
15
16
|
}
|
|
16
17
|
if (input.humanFlag) {
|
|
17
|
-
return { format: "human", source: "flag" };
|
|
18
|
+
return { format: "human", source: "flag", quiet };
|
|
18
19
|
}
|
|
19
20
|
if (input.jsonFlag) {
|
|
20
|
-
return { format: "json", source: "flag" };
|
|
21
|
+
return { format: "json", source: "flag", quiet };
|
|
21
22
|
}
|
|
22
23
|
if (input.projectDefault) {
|
|
23
|
-
return { format: input.projectDefault, source: "project" };
|
|
24
|
+
return { format: input.projectDefault, source: "project", quiet };
|
|
24
25
|
}
|
|
25
26
|
if (input.userDefault) {
|
|
26
|
-
return { format: input.userDefault, source: "user" };
|
|
27
|
+
return { format: input.userDefault, source: "user", quiet };
|
|
27
28
|
}
|
|
28
|
-
return { format: "json", source: "default" };
|
|
29
|
+
return { format: "json", source: "default", quiet };
|
|
29
30
|
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LAFS Health Check Module
|
|
3
|
+
*
|
|
4
|
+
* Provides health check endpoints for monitoring and orchestration
|
|
5
|
+
*/
|
|
6
|
+
export interface HealthCheckConfig {
|
|
7
|
+
path?: string;
|
|
8
|
+
checks?: HealthCheckFunction[];
|
|
9
|
+
}
|
|
10
|
+
export type HealthCheckFunction = () => Promise<HealthCheckResult> | HealthCheckResult;
|
|
11
|
+
export interface HealthCheckResult {
|
|
12
|
+
name: string;
|
|
13
|
+
status: 'ok' | 'warning' | 'error';
|
|
14
|
+
message?: string;
|
|
15
|
+
duration?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface HealthStatus {
|
|
18
|
+
status: 'healthy' | 'degraded' | 'unhealthy';
|
|
19
|
+
timestamp: string;
|
|
20
|
+
version: string;
|
|
21
|
+
uptime: number;
|
|
22
|
+
checks: HealthCheckResult[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Health check middleware for Express applications
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* import express from 'express';
|
|
30
|
+
* import { healthCheck } from '@cleocode/lafs-protocol/health';
|
|
31
|
+
*
|
|
32
|
+
* const app = express();
|
|
33
|
+
*
|
|
34
|
+
* // Basic health check
|
|
35
|
+
* app.use('/health', healthCheck());
|
|
36
|
+
*
|
|
37
|
+
* // Custom health checks
|
|
38
|
+
* app.use('/health', healthCheck({
|
|
39
|
+
* checks: [
|
|
40
|
+
* async () => ({
|
|
41
|
+
* name: 'database',
|
|
42
|
+
* status: await checkDatabase() ? 'ok' : 'error'
|
|
43
|
+
* })
|
|
44
|
+
* ]
|
|
45
|
+
* }));
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export declare function healthCheck(config?: HealthCheckConfig): (req: any, res: any) => Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Create a database health check
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```typescript
|
|
54
|
+
* const dbCheck = createDatabaseHealthCheck({
|
|
55
|
+
* checkConnection: async () => await db.ping()
|
|
56
|
+
* });
|
|
57
|
+
*
|
|
58
|
+
* app.use('/health', healthCheck({
|
|
59
|
+
* checks: [dbCheck]
|
|
60
|
+
* }));
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export declare function createDatabaseHealthCheck(config: {
|
|
64
|
+
checkConnection: () => Promise<boolean>;
|
|
65
|
+
name?: string;
|
|
66
|
+
}): HealthCheckFunction;
|
|
67
|
+
/**
|
|
68
|
+
* Create an external service health check
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```typescript
|
|
72
|
+
* const apiCheck = createExternalServiceHealthCheck({
|
|
73
|
+
* name: 'payment-api',
|
|
74
|
+
* url: 'https://api.payment.com/health',
|
|
75
|
+
* timeout: 5000
|
|
76
|
+
* });
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export declare function createExternalServiceHealthCheck(config: {
|
|
80
|
+
name: string;
|
|
81
|
+
url: string;
|
|
82
|
+
timeout?: number;
|
|
83
|
+
}): HealthCheckFunction;
|
|
84
|
+
/**
|
|
85
|
+
* Liveness probe - basic check that service is running
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```typescript
|
|
89
|
+
* app.get('/health/live', livenessProbe());
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export declare function livenessProbe(): (req: any, res: any) => void;
|
|
93
|
+
/**
|
|
94
|
+
* Readiness probe - check that service is ready to accept traffic
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```typescript
|
|
98
|
+
* app.get('/health/ready', readinessProbe({
|
|
99
|
+
* checks: [dbCheck, cacheCheck]
|
|
100
|
+
* }));
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
export declare function readinessProbe(config?: {
|
|
104
|
+
checks?: HealthCheckFunction[];
|
|
105
|
+
}): (req: any, res: any) => Promise<void>;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LAFS Health Check Module
|
|
3
|
+
*
|
|
4
|
+
* Provides health check endpoints for monitoring and orchestration
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Health check middleware for Express applications
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import express from 'express';
|
|
12
|
+
* import { healthCheck } from '@cleocode/lafs-protocol/health';
|
|
13
|
+
*
|
|
14
|
+
* const app = express();
|
|
15
|
+
*
|
|
16
|
+
* // Basic health check
|
|
17
|
+
* app.use('/health', healthCheck());
|
|
18
|
+
*
|
|
19
|
+
* // Custom health checks
|
|
20
|
+
* app.use('/health', healthCheck({
|
|
21
|
+
* checks: [
|
|
22
|
+
* async () => ({
|
|
23
|
+
* name: 'database',
|
|
24
|
+
* status: await checkDatabase() ? 'ok' : 'error'
|
|
25
|
+
* })
|
|
26
|
+
* ]
|
|
27
|
+
* }));
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export function healthCheck(config = {}) {
|
|
31
|
+
const { path = '/health', checks = [] } = config;
|
|
32
|
+
const startTime = Date.now();
|
|
33
|
+
return async (req, res) => {
|
|
34
|
+
const timestamp = new Date().toISOString();
|
|
35
|
+
const checkResults = [];
|
|
36
|
+
// Run all health checks
|
|
37
|
+
for (const check of checks) {
|
|
38
|
+
const start = Date.now();
|
|
39
|
+
try {
|
|
40
|
+
const result = await check();
|
|
41
|
+
result.duration = Date.now() - start;
|
|
42
|
+
checkResults.push(result);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
checkResults.push({
|
|
46
|
+
name: 'unknown',
|
|
47
|
+
status: 'error',
|
|
48
|
+
message: error instanceof Error ? error.message : 'Check failed',
|
|
49
|
+
duration: Date.now() - start
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Add default checks
|
|
54
|
+
checkResults.push({
|
|
55
|
+
name: 'envelopeValidation',
|
|
56
|
+
status: 'ok'
|
|
57
|
+
});
|
|
58
|
+
checkResults.push({
|
|
59
|
+
name: 'tokenBudgets',
|
|
60
|
+
status: 'ok'
|
|
61
|
+
});
|
|
62
|
+
// Determine overall status
|
|
63
|
+
const hasErrors = checkResults.some(c => c.status === 'error');
|
|
64
|
+
const hasWarnings = checkResults.some(c => c.status === 'warning');
|
|
65
|
+
const status = hasErrors
|
|
66
|
+
? 'unhealthy'
|
|
67
|
+
: hasWarnings
|
|
68
|
+
? 'degraded'
|
|
69
|
+
: 'healthy';
|
|
70
|
+
const health = {
|
|
71
|
+
status,
|
|
72
|
+
timestamp,
|
|
73
|
+
version: process.env.npm_package_version || '1.1.0',
|
|
74
|
+
uptime: Math.floor((Date.now() - startTime) / 1000),
|
|
75
|
+
checks: checkResults
|
|
76
|
+
};
|
|
77
|
+
const statusCode = status === 'healthy' ? 200 : status === 'degraded' ? 200 : 503;
|
|
78
|
+
res.status(statusCode).json(health);
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Create a database health check
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```typescript
|
|
86
|
+
* const dbCheck = createDatabaseHealthCheck({
|
|
87
|
+
* checkConnection: async () => await db.ping()
|
|
88
|
+
* });
|
|
89
|
+
*
|
|
90
|
+
* app.use('/health', healthCheck({
|
|
91
|
+
* checks: [dbCheck]
|
|
92
|
+
* }));
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export function createDatabaseHealthCheck(config) {
|
|
96
|
+
return async () => {
|
|
97
|
+
try {
|
|
98
|
+
const isConnected = await config.checkConnection();
|
|
99
|
+
return {
|
|
100
|
+
name: config.name || 'database',
|
|
101
|
+
status: isConnected ? 'ok' : 'error',
|
|
102
|
+
message: isConnected ? 'Connected' : 'Connection failed'
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
return {
|
|
107
|
+
name: config.name || 'database',
|
|
108
|
+
status: 'error',
|
|
109
|
+
message: error instanceof Error ? error.message : 'Database check failed'
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Create an external service health check
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```typescript
|
|
119
|
+
* const apiCheck = createExternalServiceHealthCheck({
|
|
120
|
+
* name: 'payment-api',
|
|
121
|
+
* url: 'https://api.payment.com/health',
|
|
122
|
+
* timeout: 5000
|
|
123
|
+
* });
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
export function createExternalServiceHealthCheck(config) {
|
|
127
|
+
return async () => {
|
|
128
|
+
const start = Date.now();
|
|
129
|
+
try {
|
|
130
|
+
const controller = new AbortController();
|
|
131
|
+
const timeout = setTimeout(() => controller.abort(), config.timeout || 5000);
|
|
132
|
+
const response = await fetch(config.url, {
|
|
133
|
+
signal: controller.signal
|
|
134
|
+
});
|
|
135
|
+
clearTimeout(timeout);
|
|
136
|
+
return {
|
|
137
|
+
name: config.name,
|
|
138
|
+
status: response.ok ? 'ok' : 'error',
|
|
139
|
+
message: response.ok ? 'Service healthy' : `HTTP ${response.status}`,
|
|
140
|
+
duration: Date.now() - start
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
return {
|
|
145
|
+
name: config.name,
|
|
146
|
+
status: 'error',
|
|
147
|
+
message: error instanceof Error ? error.message : 'Service unreachable',
|
|
148
|
+
duration: Date.now() - start
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Liveness probe - basic check that service is running
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```typescript
|
|
158
|
+
* app.get('/health/live', livenessProbe());
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
export function livenessProbe() {
|
|
162
|
+
return (req, res) => {
|
|
163
|
+
res.status(200).json({
|
|
164
|
+
status: 'alive',
|
|
165
|
+
timestamp: new Date().toISOString()
|
|
166
|
+
});
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Readiness probe - check that service is ready to accept traffic
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* ```typescript
|
|
174
|
+
* app.get('/health/ready', readinessProbe({
|
|
175
|
+
* checks: [dbCheck, cacheCheck]
|
|
176
|
+
* }));
|
|
177
|
+
* ```
|
|
178
|
+
*/
|
|
179
|
+
export function readinessProbe(config = {}) {
|
|
180
|
+
return async (req, res) => {
|
|
181
|
+
const checkResults = [];
|
|
182
|
+
if (config.checks) {
|
|
183
|
+
for (const check of config.checks) {
|
|
184
|
+
try {
|
|
185
|
+
const result = await check();
|
|
186
|
+
checkResults.push(result);
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
checkResults.push({
|
|
190
|
+
name: 'unknown',
|
|
191
|
+
status: 'error',
|
|
192
|
+
message: error instanceof Error ? error.message : 'Check failed'
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const hasErrors = checkResults.some(c => c.status === 'error');
|
|
198
|
+
if (hasErrors) {
|
|
199
|
+
res.status(503).json({
|
|
200
|
+
status: 'not ready',
|
|
201
|
+
checks: checkResults
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
res.status(200).json({
|
|
206
|
+
status: 'ready',
|
|
207
|
+
checks: checkResults
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -7,3 +7,7 @@ export * from "./tokenEstimator.js";
|
|
|
7
7
|
export * from "./budgetEnforcement.js";
|
|
8
8
|
export * from "./mcpAdapter.js";
|
|
9
9
|
export * from "./discovery.js";
|
|
10
|
+
export * from "./health/index.js";
|
|
11
|
+
export * from "./shutdown/index.js";
|
|
12
|
+
export * from "./circuit-breaker/index.js";
|
|
13
|
+
export * from "./a2a/index.js";
|
package/dist/src/index.js
CHANGED
|
@@ -7,3 +7,9 @@ export * from "./tokenEstimator.js";
|
|
|
7
7
|
export * from "./budgetEnforcement.js";
|
|
8
8
|
export * from "./mcpAdapter.js";
|
|
9
9
|
export * from "./discovery.js";
|
|
10
|
+
// Operations & Reliability
|
|
11
|
+
export * from "./health/index.js";
|
|
12
|
+
export * from "./shutdown/index.js";
|
|
13
|
+
export * from "./circuit-breaker/index.js";
|
|
14
|
+
// A2A Integration
|
|
15
|
+
export * from "./a2a/index.js";
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LAFS Graceful Shutdown Module
|
|
3
|
+
*
|
|
4
|
+
* Handles graceful shutdown of LAFS servers
|
|
5
|
+
*/
|
|
6
|
+
import { Server } from 'http';
|
|
7
|
+
export interface GracefulShutdownConfig {
|
|
8
|
+
timeout?: number;
|
|
9
|
+
signals?: NodeJS.Signals[];
|
|
10
|
+
onShutdown?: () => Promise<void> | void;
|
|
11
|
+
onClose?: () => Promise<void> | void;
|
|
12
|
+
}
|
|
13
|
+
export interface ShutdownState {
|
|
14
|
+
isShuttingDown: boolean;
|
|
15
|
+
activeConnections: number;
|
|
16
|
+
shutdownStartTime?: Date;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Enable graceful shutdown for an HTTP server
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* import express from 'express';
|
|
24
|
+
* import { gracefulShutdown } from '@cleocode/lafs-protocol/shutdown';
|
|
25
|
+
*
|
|
26
|
+
* const app = express();
|
|
27
|
+
* const server = app.listen(3000);
|
|
28
|
+
*
|
|
29
|
+
* gracefulShutdown(server, {
|
|
30
|
+
* timeout: 30000,
|
|
31
|
+
* signals: ['SIGTERM', 'SIGINT'],
|
|
32
|
+
* onShutdown: async () => {
|
|
33
|
+
* console.log('Shutting down...');
|
|
34
|
+
* await db.close();
|
|
35
|
+
* }
|
|
36
|
+
* });
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export declare function gracefulShutdown(server: Server, config?: GracefulShutdownConfig): void;
|
|
40
|
+
/**
|
|
41
|
+
* Check if server is shutting down
|
|
42
|
+
*/
|
|
43
|
+
export declare function isShuttingDown(): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Get shutdown state
|
|
46
|
+
*/
|
|
47
|
+
export declare function getShutdownState(): ShutdownState;
|
|
48
|
+
/**
|
|
49
|
+
* Force immediate shutdown (emergency use only)
|
|
50
|
+
*/
|
|
51
|
+
export declare function forceShutdown(exitCode?: number): void;
|
|
52
|
+
/**
|
|
53
|
+
* Middleware to reject requests during shutdown
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```typescript
|
|
57
|
+
* app.use(shutdownMiddleware());
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export declare function shutdownMiddleware(): (req: any, res: any, next: any) => void;
|
|
61
|
+
/**
|
|
62
|
+
* Wait for shutdown to complete
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```typescript
|
|
66
|
+
* await waitForShutdown();
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export declare function waitForShutdown(): Promise<void>;
|