@buenojs/bueno 0.8.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/.env.example +109 -0
- package/.github/workflows/ci.yml +31 -0
- package/LICENSE +21 -0
- package/README.md +892 -0
- package/architecture.md +652 -0
- package/bun.lock +70 -0
- package/dist/cli/index.js +3233 -0
- package/dist/index.js +9014 -0
- package/package.json +77 -0
- package/src/cache/index.ts +795 -0
- package/src/cli/ARCHITECTURE.md +837 -0
- package/src/cli/bin.ts +10 -0
- package/src/cli/commands/build.ts +425 -0
- package/src/cli/commands/dev.ts +248 -0
- package/src/cli/commands/generate.ts +541 -0
- package/src/cli/commands/help.ts +55 -0
- package/src/cli/commands/index.ts +112 -0
- package/src/cli/commands/migration.ts +355 -0
- package/src/cli/commands/new.ts +804 -0
- package/src/cli/commands/start.ts +208 -0
- package/src/cli/core/args.ts +283 -0
- package/src/cli/core/console.ts +349 -0
- package/src/cli/core/index.ts +60 -0
- package/src/cli/core/prompt.ts +424 -0
- package/src/cli/core/spinner.ts +265 -0
- package/src/cli/index.ts +135 -0
- package/src/cli/templates/deploy.ts +295 -0
- package/src/cli/templates/docker.ts +307 -0
- package/src/cli/templates/index.ts +24 -0
- package/src/cli/utils/fs.ts +428 -0
- package/src/cli/utils/index.ts +8 -0
- package/src/cli/utils/strings.ts +197 -0
- package/src/config/env.ts +408 -0
- package/src/config/index.ts +506 -0
- package/src/config/loader.ts +329 -0
- package/src/config/merge.ts +285 -0
- package/src/config/types.ts +320 -0
- package/src/config/validation.ts +441 -0
- package/src/container/forward-ref.ts +143 -0
- package/src/container/index.ts +386 -0
- package/src/context/index.ts +360 -0
- package/src/database/index.ts +1142 -0
- package/src/database/migrations/index.ts +371 -0
- package/src/database/schema/index.ts +619 -0
- package/src/frontend/api-routes.ts +640 -0
- package/src/frontend/bundler.ts +643 -0
- package/src/frontend/console-client.ts +419 -0
- package/src/frontend/console-stream.ts +587 -0
- package/src/frontend/dev-server.ts +846 -0
- package/src/frontend/file-router.ts +611 -0
- package/src/frontend/frameworks/index.ts +106 -0
- package/src/frontend/frameworks/react.ts +85 -0
- package/src/frontend/frameworks/solid.ts +104 -0
- package/src/frontend/frameworks/svelte.ts +110 -0
- package/src/frontend/frameworks/vue.ts +92 -0
- package/src/frontend/hmr-client.ts +663 -0
- package/src/frontend/hmr.ts +728 -0
- package/src/frontend/index.ts +342 -0
- package/src/frontend/islands.ts +552 -0
- package/src/frontend/isr.ts +555 -0
- package/src/frontend/layout.ts +475 -0
- package/src/frontend/ssr/react.ts +446 -0
- package/src/frontend/ssr/solid.ts +523 -0
- package/src/frontend/ssr/svelte.ts +546 -0
- package/src/frontend/ssr/vue.ts +504 -0
- package/src/frontend/ssr.ts +699 -0
- package/src/frontend/types.ts +2274 -0
- package/src/health/index.ts +604 -0
- package/src/index.ts +410 -0
- package/src/lock/index.ts +587 -0
- package/src/logger/index.ts +444 -0
- package/src/logger/transports/index.ts +969 -0
- package/src/metrics/index.ts +494 -0
- package/src/middleware/built-in.ts +360 -0
- package/src/middleware/index.ts +94 -0
- package/src/modules/filters.ts +458 -0
- package/src/modules/guards.ts +405 -0
- package/src/modules/index.ts +1256 -0
- package/src/modules/interceptors.ts +574 -0
- package/src/modules/lazy.ts +418 -0
- package/src/modules/lifecycle.ts +478 -0
- package/src/modules/metadata.ts +90 -0
- package/src/modules/pipes.ts +626 -0
- package/src/router/index.ts +339 -0
- package/src/router/linear.ts +371 -0
- package/src/router/regex.ts +292 -0
- package/src/router/tree.ts +562 -0
- package/src/rpc/index.ts +1263 -0
- package/src/security/index.ts +436 -0
- package/src/ssg/index.ts +631 -0
- package/src/storage/index.ts +456 -0
- package/src/telemetry/index.ts +1097 -0
- package/src/testing/index.ts +1586 -0
- package/src/types/index.ts +236 -0
- package/src/types/optional-deps.d.ts +219 -0
- package/src/validation/index.ts +276 -0
- package/src/websocket/index.ts +1004 -0
- package/tests/integration/cli.test.ts +1016 -0
- package/tests/integration/fullstack.test.ts +234 -0
- package/tests/unit/cache.test.ts +174 -0
- package/tests/unit/cli-commands.test.ts +892 -0
- package/tests/unit/cli.test.ts +1258 -0
- package/tests/unit/container.test.ts +279 -0
- package/tests/unit/context.test.ts +221 -0
- package/tests/unit/database.test.ts +183 -0
- package/tests/unit/linear-router.test.ts +280 -0
- package/tests/unit/lock.test.ts +336 -0
- package/tests/unit/middleware.test.ts +184 -0
- package/tests/unit/modules.test.ts +142 -0
- package/tests/unit/pubsub.test.ts +257 -0
- package/tests/unit/regex-router.test.ts +265 -0
- package/tests/unit/router.test.ts +373 -0
- package/tests/unit/rpc.test.ts +1248 -0
- package/tests/unit/security.test.ts +174 -0
- package/tests/unit/telemetry.test.ts +371 -0
- package/tests/unit/test-cache.test.ts +110 -0
- package/tests/unit/test-database.test.ts +282 -0
- package/tests/unit/tree-router.test.ts +325 -0
- package/tests/unit/validation.test.ts +794 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health Check System
|
|
3
|
+
*
|
|
4
|
+
* Provides health check endpoints for production monitoring:
|
|
5
|
+
* - /health (liveness probe) - Returns 200 if server is running
|
|
6
|
+
* - /ready (readiness probe) - Returns 200 only if all checks pass
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Context } from "../context";
|
|
10
|
+
import type { Middleware } from "../middleware";
|
|
11
|
+
|
|
12
|
+
// ============= Types =============
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Health check status
|
|
16
|
+
*/
|
|
17
|
+
export type HealthStatus = "healthy" | "unhealthy" | "degraded";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Individual check result
|
|
21
|
+
*/
|
|
22
|
+
export interface CheckResult {
|
|
23
|
+
status: HealthStatus;
|
|
24
|
+
latency?: number;
|
|
25
|
+
message?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Overall health check result
|
|
30
|
+
*/
|
|
31
|
+
export interface HealthCheckResult {
|
|
32
|
+
status: HealthStatus;
|
|
33
|
+
timestamp: string;
|
|
34
|
+
version?: string;
|
|
35
|
+
uptime?: number;
|
|
36
|
+
checks?: Record<string, CheckResult>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Health check function type
|
|
41
|
+
*/
|
|
42
|
+
export type HealthCheckFn = () => Promise<CheckResult> | CheckResult;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Options for individual health checks
|
|
46
|
+
*/
|
|
47
|
+
export interface CheckOptions {
|
|
48
|
+
/** Whether this check is critical for readiness (default: true) */
|
|
49
|
+
critical?: boolean;
|
|
50
|
+
/** Timeout in milliseconds (default: 5000) */
|
|
51
|
+
timeout?: number;
|
|
52
|
+
/** Description of the check */
|
|
53
|
+
description?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Registered health check entry
|
|
58
|
+
*/
|
|
59
|
+
interface RegisteredCheck {
|
|
60
|
+
name: string;
|
|
61
|
+
fn: HealthCheckFn;
|
|
62
|
+
options: Required<CheckOptions>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Options for health middleware
|
|
67
|
+
*/
|
|
68
|
+
export interface HealthMiddlewareOptions {
|
|
69
|
+
/** Path for liveness probe (default: /health) */
|
|
70
|
+
healthPath?: string;
|
|
71
|
+
/** Path for readiness probe (default: /ready) */
|
|
72
|
+
readyPath?: string;
|
|
73
|
+
/** Whether to expose metrics in response (default: true) */
|
|
74
|
+
exposeMetrics?: boolean;
|
|
75
|
+
/** Initial health checks to register */
|
|
76
|
+
checks?: Record<string, HealthCheckFn | { fn: HealthCheckFn; options?: CheckOptions }>;
|
|
77
|
+
/** Custom version string (default: from package.json) */
|
|
78
|
+
version?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Database interface for built-in checker
|
|
83
|
+
*/
|
|
84
|
+
export interface DatabaseLike {
|
|
85
|
+
query?(sql: string): Promise<unknown>;
|
|
86
|
+
execute?(sql: string): Promise<unknown>;
|
|
87
|
+
healthCheck?(): Promise<boolean>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Cache interface for built-in checker
|
|
92
|
+
*/
|
|
93
|
+
export interface CacheLike {
|
|
94
|
+
get?(key: string): Promise<unknown>;
|
|
95
|
+
set?(key: string, value: unknown): Promise<unknown>;
|
|
96
|
+
ping?(): Promise<unknown>;
|
|
97
|
+
healthCheck?(): Promise<boolean>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ============= HealthCheckManager Class =============
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Manages health checks for the application
|
|
104
|
+
*/
|
|
105
|
+
export class HealthCheckManager {
|
|
106
|
+
private checks: Map<string, RegisteredCheck> = new Map();
|
|
107
|
+
private startTime = Date.now();
|
|
108
|
+
private version: string;
|
|
109
|
+
|
|
110
|
+
constructor(version?: string) {
|
|
111
|
+
this.version = version ?? "0.1.0";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Register a health check
|
|
116
|
+
*/
|
|
117
|
+
registerCheck(
|
|
118
|
+
name: string,
|
|
119
|
+
checkFn: HealthCheckFn,
|
|
120
|
+
options: CheckOptions = {},
|
|
121
|
+
): this {
|
|
122
|
+
this.checks.set(name, {
|
|
123
|
+
name,
|
|
124
|
+
fn: checkFn,
|
|
125
|
+
options: {
|
|
126
|
+
critical: options.critical ?? true,
|
|
127
|
+
timeout: options.timeout ?? 5000,
|
|
128
|
+
description: options.description ?? "",
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
return this;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Remove a health check
|
|
136
|
+
*/
|
|
137
|
+
removeCheck(name: string): boolean {
|
|
138
|
+
return this.checks.delete(name);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get all registered check names
|
|
143
|
+
*/
|
|
144
|
+
getCheckNames(): string[] {
|
|
145
|
+
return Array.from(this.checks.keys());
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Check if a check is registered
|
|
150
|
+
*/
|
|
151
|
+
hasCheck(name: string): boolean {
|
|
152
|
+
return this.checks.has(name);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Run a single check with timeout
|
|
157
|
+
*/
|
|
158
|
+
private async runSingleCheck(check: RegisteredCheck): Promise<CheckResult> {
|
|
159
|
+
const start = Date.now();
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
// Run check with timeout
|
|
163
|
+
const result = await Promise.race([
|
|
164
|
+
check.fn(),
|
|
165
|
+
new Promise<never>((_, reject) =>
|
|
166
|
+
setTimeout(
|
|
167
|
+
() => reject(new Error(`Check timed out after ${check.options.timeout}ms`)),
|
|
168
|
+
check.options.timeout,
|
|
169
|
+
),
|
|
170
|
+
),
|
|
171
|
+
]);
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
...result,
|
|
175
|
+
latency: Date.now() - start,
|
|
176
|
+
};
|
|
177
|
+
} catch (error) {
|
|
178
|
+
return {
|
|
179
|
+
status: "unhealthy",
|
|
180
|
+
latency: Date.now() - start,
|
|
181
|
+
message: error instanceof Error ? error.message : "Check failed",
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Execute all registered checks
|
|
188
|
+
*/
|
|
189
|
+
async runChecks(): Promise<Record<string, CheckResult>> {
|
|
190
|
+
const results: Record<string, CheckResult> = {};
|
|
191
|
+
|
|
192
|
+
// Run all checks in parallel
|
|
193
|
+
const entries = Array.from(this.checks.values());
|
|
194
|
+
const outcomes = await Promise.all(
|
|
195
|
+
entries.map(async (check) => ({
|
|
196
|
+
name: check.name,
|
|
197
|
+
result: await this.runSingleCheck(check),
|
|
198
|
+
critical: check.options.critical,
|
|
199
|
+
})),
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
for (const { name, result } of outcomes) {
|
|
203
|
+
results[name] = result;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return results;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Get liveness status (lightweight - no dependency checks)
|
|
211
|
+
*/
|
|
212
|
+
getHealth(): HealthCheckResult {
|
|
213
|
+
return {
|
|
214
|
+
status: "healthy",
|
|
215
|
+
timestamp: new Date().toISOString(),
|
|
216
|
+
version: this.version,
|
|
217
|
+
uptime: Math.floor((Date.now() - this.startTime) / 1000),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Get readiness status (runs all checks)
|
|
223
|
+
*/
|
|
224
|
+
async getReadiness(): Promise<HealthCheckResult> {
|
|
225
|
+
const checks = await this.runChecks();
|
|
226
|
+
|
|
227
|
+
// Determine overall status
|
|
228
|
+
let status: HealthStatus = "healthy";
|
|
229
|
+
let hasUnhealthyCritical = false;
|
|
230
|
+
let hasUnhealthyNonCritical = false;
|
|
231
|
+
let hasDegraded = false;
|
|
232
|
+
|
|
233
|
+
const entries = Array.from(this.checks.values());
|
|
234
|
+
|
|
235
|
+
for (const entry of entries) {
|
|
236
|
+
const checkResult = checks[entry.name];
|
|
237
|
+
if (checkResult) {
|
|
238
|
+
// Critical checks affect overall status
|
|
239
|
+
if (entry.options.critical && checkResult.status === "unhealthy") {
|
|
240
|
+
hasUnhealthyCritical = true;
|
|
241
|
+
}
|
|
242
|
+
// Non-critical unhealthy checks degrade status
|
|
243
|
+
if (!entry.options.critical && checkResult.status === "unhealthy") {
|
|
244
|
+
hasUnhealthyNonCritical = true;
|
|
245
|
+
}
|
|
246
|
+
// Any degraded check degrades overall status
|
|
247
|
+
if (checkResult.status === "degraded") {
|
|
248
|
+
hasDegraded = true;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Determine final status based on flags
|
|
254
|
+
if (hasUnhealthyCritical) {
|
|
255
|
+
status = "unhealthy";
|
|
256
|
+
} else if (hasUnhealthyNonCritical || hasDegraded) {
|
|
257
|
+
status = "degraded";
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
status,
|
|
262
|
+
timestamp: new Date().toISOString(),
|
|
263
|
+
version: this.version,
|
|
264
|
+
uptime: Math.floor((Date.now() - this.startTime) / 1000),
|
|
265
|
+
checks,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Get uptime in seconds
|
|
271
|
+
*/
|
|
272
|
+
getUptime(): number {
|
|
273
|
+
return Math.floor((Date.now() - this.startTime) / 1000);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Reset start time (useful for testing)
|
|
278
|
+
*/
|
|
279
|
+
resetStartTime(): void {
|
|
280
|
+
this.startTime = Date.now();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ============= Middleware Factory =============
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Create health check middleware
|
|
288
|
+
*/
|
|
289
|
+
export function createHealthMiddleware(
|
|
290
|
+
options: HealthMiddlewareOptions = {},
|
|
291
|
+
): { middleware: Middleware; manager: HealthCheckManager } {
|
|
292
|
+
const {
|
|
293
|
+
healthPath = "/health",
|
|
294
|
+
readyPath = "/ready",
|
|
295
|
+
exposeMetrics = true,
|
|
296
|
+
checks = {},
|
|
297
|
+
version,
|
|
298
|
+
} = options;
|
|
299
|
+
|
|
300
|
+
const manager = new HealthCheckManager(version);
|
|
301
|
+
|
|
302
|
+
// Register initial checks
|
|
303
|
+
for (const [name, check] of Object.entries(checks)) {
|
|
304
|
+
if (typeof check === "function") {
|
|
305
|
+
manager.registerCheck(name, check);
|
|
306
|
+
} else {
|
|
307
|
+
manager.registerCheck(name, check.fn, check.options);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const middleware: Middleware = async (
|
|
312
|
+
context: Context,
|
|
313
|
+
next: () => Promise<Response>,
|
|
314
|
+
): Promise<Response> => {
|
|
315
|
+
const path = context.path;
|
|
316
|
+
|
|
317
|
+
// Handle liveness probe
|
|
318
|
+
if (path === healthPath && context.method === "GET") {
|
|
319
|
+
const health = manager.getHealth();
|
|
320
|
+
return context.json(health);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Handle readiness probe
|
|
324
|
+
if (path === readyPath && context.method === "GET") {
|
|
325
|
+
const readiness = await manager.getReadiness();
|
|
326
|
+
|
|
327
|
+
// Set appropriate status code
|
|
328
|
+
if (readiness.status === "unhealthy") {
|
|
329
|
+
context.status(503);
|
|
330
|
+
} else if (readiness.status === "degraded") {
|
|
331
|
+
context.status(200); // Still serve traffic but with warning
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Optionally strip metrics
|
|
335
|
+
const response = exposeMetrics
|
|
336
|
+
? readiness
|
|
337
|
+
: { status: readiness.status, timestamp: readiness.timestamp };
|
|
338
|
+
|
|
339
|
+
return context.json(response);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Continue to next middleware/handler
|
|
343
|
+
return next();
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
return { middleware, manager };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ============= Built-in Checkers =============
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Create a database connectivity check
|
|
353
|
+
*/
|
|
354
|
+
export function createDatabaseCheck(
|
|
355
|
+
db: DatabaseLike,
|
|
356
|
+
options: { query?: string; timeout?: number } = {},
|
|
357
|
+
): HealthCheckFn {
|
|
358
|
+
const { query = "SELECT 1" } = options;
|
|
359
|
+
|
|
360
|
+
return async (): Promise<CheckResult> => {
|
|
361
|
+
const start = Date.now();
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
// Use custom healthCheck method if available
|
|
365
|
+
if (typeof db.healthCheck === "function") {
|
|
366
|
+
const isHealthy = await db.healthCheck();
|
|
367
|
+
return {
|
|
368
|
+
status: isHealthy ? "healthy" : "unhealthy",
|
|
369
|
+
latency: Date.now() - start,
|
|
370
|
+
message: isHealthy ? "Database connection OK" : "Database health check failed",
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Try to execute a simple query
|
|
375
|
+
if (typeof db.query === "function") {
|
|
376
|
+
await db.query(query);
|
|
377
|
+
return {
|
|
378
|
+
status: "healthy",
|
|
379
|
+
latency: Date.now() - start,
|
|
380
|
+
message: "Database connection OK",
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (typeof db.execute === "function") {
|
|
385
|
+
await db.execute(query);
|
|
386
|
+
return {
|
|
387
|
+
status: "healthy",
|
|
388
|
+
latency: Date.now() - start,
|
|
389
|
+
message: "Database connection OK",
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return {
|
|
394
|
+
status: "degraded",
|
|
395
|
+
latency: Date.now() - start,
|
|
396
|
+
message: "No compatible database method found",
|
|
397
|
+
};
|
|
398
|
+
} catch (error) {
|
|
399
|
+
return {
|
|
400
|
+
status: "unhealthy",
|
|
401
|
+
latency: Date.now() - start,
|
|
402
|
+
message: error instanceof Error ? error.message : "Database check failed",
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Create a cache connectivity check
|
|
410
|
+
*/
|
|
411
|
+
export function createCacheCheck(
|
|
412
|
+
cache: CacheLike,
|
|
413
|
+
options: { testKey?: string; timeout?: number } = {},
|
|
414
|
+
): HealthCheckFn {
|
|
415
|
+
const { testKey = "__health_check__" } = options;
|
|
416
|
+
|
|
417
|
+
return async (): Promise<CheckResult> => {
|
|
418
|
+
const start = Date.now();
|
|
419
|
+
|
|
420
|
+
try {
|
|
421
|
+
// Use custom healthCheck method if available
|
|
422
|
+
if (typeof cache.healthCheck === "function") {
|
|
423
|
+
const isHealthy = await cache.healthCheck();
|
|
424
|
+
return {
|
|
425
|
+
status: isHealthy ? "healthy" : "unhealthy",
|
|
426
|
+
latency: Date.now() - start,
|
|
427
|
+
message: isHealthy ? "Cache connection OK" : "Cache health check failed",
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Use ping method if available (Redis-like)
|
|
432
|
+
if (typeof cache.ping === "function") {
|
|
433
|
+
await cache.ping();
|
|
434
|
+
return {
|
|
435
|
+
status: "healthy",
|
|
436
|
+
latency: Date.now() - start,
|
|
437
|
+
message: "Cache ping OK",
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Try set and get operations
|
|
442
|
+
if (typeof cache.set === "function" && typeof cache.get === "function") {
|
|
443
|
+
const testValue = Date.now().toString();
|
|
444
|
+
await cache.set(testKey, testValue);
|
|
445
|
+
const retrieved = await cache.get(testKey);
|
|
446
|
+
|
|
447
|
+
if (retrieved === testValue || retrieved?.toString() === testValue) {
|
|
448
|
+
return {
|
|
449
|
+
status: "healthy",
|
|
450
|
+
latency: Date.now() - start,
|
|
451
|
+
message: "Cache read/write OK",
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return {
|
|
456
|
+
status: "degraded",
|
|
457
|
+
latency: Date.now() - start,
|
|
458
|
+
message: "Cache read/write mismatch",
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return {
|
|
463
|
+
status: "degraded",
|
|
464
|
+
latency: Date.now() - start,
|
|
465
|
+
message: "No compatible cache method found",
|
|
466
|
+
};
|
|
467
|
+
} catch (error) {
|
|
468
|
+
return {
|
|
469
|
+
status: "unhealthy",
|
|
470
|
+
latency: Date.now() - start,
|
|
471
|
+
message: error instanceof Error ? error.message : "Cache check failed",
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Create a custom health check with timeout
|
|
479
|
+
*/
|
|
480
|
+
export function createCustomCheck(
|
|
481
|
+
checkFn: () => Promise<boolean> | boolean,
|
|
482
|
+
options: { message?: string; timeout?: number } = {},
|
|
483
|
+
): HealthCheckFn {
|
|
484
|
+
const { message = "Custom check" } = options;
|
|
485
|
+
|
|
486
|
+
return async (): Promise<CheckResult> => {
|
|
487
|
+
const start = Date.now();
|
|
488
|
+
|
|
489
|
+
try {
|
|
490
|
+
const result = await checkFn();
|
|
491
|
+
return {
|
|
492
|
+
status: result ? "healthy" : "unhealthy",
|
|
493
|
+
latency: Date.now() - start,
|
|
494
|
+
message: result ? `${message} OK` : `${message} failed`,
|
|
495
|
+
};
|
|
496
|
+
} catch (error) {
|
|
497
|
+
return {
|
|
498
|
+
status: "unhealthy",
|
|
499
|
+
latency: Date.now() - start,
|
|
500
|
+
message: error instanceof Error ? error.message : `${message} failed`,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Create a TCP port check
|
|
508
|
+
*/
|
|
509
|
+
export function createTCPCheck(
|
|
510
|
+
host: string,
|
|
511
|
+
port: number,
|
|
512
|
+
options: { timeout?: number } = {},
|
|
513
|
+
): HealthCheckFn {
|
|
514
|
+
const { timeout = 5000 } = options;
|
|
515
|
+
|
|
516
|
+
return async (): Promise<CheckResult> => {
|
|
517
|
+
const start = Date.now();
|
|
518
|
+
|
|
519
|
+
try {
|
|
520
|
+
// Use Bun's connect API
|
|
521
|
+
const socket = await Bun.connect({
|
|
522
|
+
hostname: host,
|
|
523
|
+
port,
|
|
524
|
+
socket: {
|
|
525
|
+
data() {},
|
|
526
|
+
error() {},
|
|
527
|
+
},
|
|
528
|
+
});
|
|
529
|
+
socket.end();
|
|
530
|
+
|
|
531
|
+
return {
|
|
532
|
+
status: "healthy",
|
|
533
|
+
latency: Date.now() - start,
|
|
534
|
+
message: `TCP ${host}:${port} reachable`,
|
|
535
|
+
};
|
|
536
|
+
} catch (error) {
|
|
537
|
+
return {
|
|
538
|
+
status: "unhealthy",
|
|
539
|
+
latency: Date.now() - start,
|
|
540
|
+
message: `TCP ${host}:${port} unreachable: ${error instanceof Error ? error.message : "unknown error"}`,
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Create an HTTP endpoint check
|
|
548
|
+
*/
|
|
549
|
+
export function createHTTPCheck(
|
|
550
|
+
url: string,
|
|
551
|
+
options: {
|
|
552
|
+
expectedStatus?: number;
|
|
553
|
+
timeout?: number;
|
|
554
|
+
headers?: Record<string, string>;
|
|
555
|
+
} = {},
|
|
556
|
+
): HealthCheckFn {
|
|
557
|
+
const { expectedStatus = 200, timeout = 5000, headers = {} } = options;
|
|
558
|
+
|
|
559
|
+
return async (): Promise<CheckResult> => {
|
|
560
|
+
const start = Date.now();
|
|
561
|
+
|
|
562
|
+
try {
|
|
563
|
+
const controller = new AbortController();
|
|
564
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
565
|
+
|
|
566
|
+
const response = await fetch(url, {
|
|
567
|
+
method: "GET",
|
|
568
|
+
headers,
|
|
569
|
+
signal: controller.signal,
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
clearTimeout(timeoutId);
|
|
573
|
+
|
|
574
|
+
if (response.status === expectedStatus) {
|
|
575
|
+
return {
|
|
576
|
+
status: "healthy",
|
|
577
|
+
latency: Date.now() - start,
|
|
578
|
+
message: `HTTP ${url} returned ${response.status}`,
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return {
|
|
583
|
+
status: "unhealthy",
|
|
584
|
+
latency: Date.now() - start,
|
|
585
|
+
message: `HTTP ${url} returned ${response.status}, expected ${expectedStatus}`,
|
|
586
|
+
};
|
|
587
|
+
} catch (error) {
|
|
588
|
+
return {
|
|
589
|
+
status: "unhealthy",
|
|
590
|
+
latency: Date.now() - start,
|
|
591
|
+
message: `HTTP ${url} failed: ${error instanceof Error ? error.message : "unknown error"}`,
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// ============= Factory Functions =============
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Create a new health check manager
|
|
601
|
+
*/
|
|
602
|
+
export function createHealthManager(version?: string): HealthCheckManager {
|
|
603
|
+
return new HealthCheckManager(version);
|
|
604
|
+
}
|