@imenam/self-register-proxy 1.0.46 → 1.0.48

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.
@@ -1,240 +1,240 @@
1
- const { Router } = require('express');
2
- const { requireLocalOnly, requireAuth } = require('../services/auth');
3
- const { registry, mutex } = require('../services/registry');
4
- const { getNextAvailablePort, releasePort, isLocalTarget, isPortFree, findRouteForPath } = require('../services/portManager');
5
- const { checkServerHealth, unregisterServer } = require('../services/healthCheck');
6
- const { INTERNAL_PORT_START, INTERNAL_PORT_END } = require('../config');
7
-
8
- const router = Router();
9
-
10
- /**
11
- * Logique commune d'enregistrement (auto et manuel)
12
- */
13
- /**
14
- * Normalise un nom de groupe : trim, et chaîne vide => undefined (non groupé).
15
- */
16
- function normalizeGroup(group) {
17
- if (typeof group !== 'string') return undefined;
18
- const trimmed = group.trim();
19
- return trimmed === '' ? undefined : trimmed;
20
- }
21
-
22
- async function registerRouteInternal({ path, name, port, target, healthCheck, group, autoAssignPort }) {
23
- const normalizedPath = path.startsWith('/') ? path : `/${path}`;
24
- const normalizedGroup = normalizeGroup(group);
25
-
26
- let finalTarget = target || 'http://localhost';
27
- if (!finalTarget.startsWith('http://') && !finalTarget.startsWith('https://')) {
28
- finalTarget = `http://${finalTarget}`;
29
- }
30
- if (finalTarget.endsWith('/')) {
31
- finalTarget = finalTarget.slice(0, -1);
32
- }
33
-
34
- await mutex.acquire();
35
-
36
- let assignedPort = null;
37
-
38
- try {
39
- if (registry.routes.has(normalizedPath)) {
40
- const existing = registry.routes.get(normalizedPath);
41
-
42
- mutex.release();
43
- const isAlive = await checkServerHealth(normalizedPath, existing);
44
- await mutex.acquire();
45
-
46
- if (!isAlive) {
47
- console.log(`[REGISTER] Path ${normalizedPath} was occupied by a dead server. Overwriting...`);
48
- registry.usedPorts.delete(existing.port);
49
- registry.routes.delete(normalizedPath);
50
- } else {
51
- throw new Error('Path already registered');
52
- }
53
- }
54
-
55
- if (!autoAssignPort) {
56
- if (!port) throw new Error('Port is required for manual registration');
57
- const manualPort = parseInt(port, 10);
58
-
59
- if (registry.usedPorts.has(manualPort)) {
60
- throw new Error(`Port ${manualPort} already in use`);
61
- }
62
- if (isLocalTarget(finalTarget) && !await isPortFree(manualPort)) {
63
- throw new Error(`Port ${manualPort} is already in use by another process`);
64
- }
65
- assignedPort = manualPort;
66
- registry.usedPorts.add(assignedPort);
67
- } else {
68
- assignedPort = await getNextAvailablePort(isLocalTarget(finalTarget));
69
- }
70
-
71
- registry.routes.set(normalizedPath, {
72
- port: assignedPort,
73
- target: finalTarget,
74
- name: name || normalizedPath,
75
- healthCheck: healthCheck !== undefined ? healthCheck === true || healthCheck === 'true' : true,
76
- group: normalizedGroup,
77
- registeredAt: new Date().toISOString()
78
- });
79
-
80
- console.log(`[REGISTER] ${normalizedPath} -> ${finalTarget}:${assignedPort} (${name || 'unnamed'}) [Health: ${healthCheck !== undefined ? healthCheck : 'default'}]${normalizedGroup ? ` [Group: ${normalizedGroup}]` : ''}`);
81
-
82
- return {
83
- success: true,
84
- path: normalizedPath,
85
- port: assignedPort,
86
- target: finalTarget,
87
- name: name || normalizedPath,
88
- healthCheck: healthCheck !== undefined ? healthCheck === true || healthCheck === 'true' : true,
89
- group: normalizedGroup,
90
- message: `Route registered. Target: ${finalTarget}:${assignedPort}`
91
- };
92
-
93
- } catch (error) {
94
- if (assignedPort !== null) {
95
- if (!registry.routes.has(normalizedPath) || registry.routes.get(normalizedPath).port !== assignedPort) {
96
- releasePort(assignedPort);
97
- }
98
- }
99
- throw error;
100
- } finally {
101
- mutex.release();
102
- }
103
- }
104
-
105
- /**
106
- * POST /proxy/register
107
- * Enregistrement automatique (rétro-compatibilité)
108
- */
109
- router.post('/proxy/register', requireLocalOnly, async (req, res) => {
110
- const { path, name, target, healthCheck, group } = req.body;
111
-
112
- if (!path) return res.status(400).json({ success: false, error: 'Path is required' });
113
-
114
- try {
115
- const result = await registerRouteInternal({ path, name, target, healthCheck, group, autoAssignPort: true });
116
- res.json(result);
117
- } catch (error) {
118
- console.error('[REGISTER AUTO ERROR]', error.message);
119
- const status = error.message.includes('already') ? 409 : 500;
120
- res.status(status).json({ success: false, error: error.message });
121
- }
122
- });
123
-
124
- /**
125
- * POST /proxy/register-manual
126
- * Enregistrement manuel via l'interface graphique (port obligatoire)
127
- */
128
- router.post('/proxy/register-manual', requireAuth, async (req, res) => {
129
- const { path, name, port, target, healthCheck, group } = req.body;
130
-
131
- if (!path) return res.status(400).json({ success: false, error: 'Path is required' });
132
- if (!port) return res.status(400).json({ success: false, error: 'Port is required for manual registration' });
133
-
134
- try {
135
- const result = await registerRouteInternal({ path, name, port, target, healthCheck, group, autoAssignPort: false });
136
- res.json(result);
137
- } catch (error) {
138
- console.error('[REGISTER MANUAL ERROR]', error.message);
139
- const status = error.message.includes('already') ? 409 : 500;
140
- res.status(status).json({ success: false, error: error.message });
141
- }
142
- });
143
-
144
- /**
145
- * DELETE /proxy/unregister
146
- * Supprime une route
147
- */
148
- router.delete('/proxy/unregister', requireLocalOnly, async (req, res) => {
149
- const { path } = req.body;
150
-
151
- if (!path) {
152
- return res.status(400).json({ success: false, error: 'Path is required' });
153
- }
154
-
155
- const normalizedPath = path.startsWith('/') ? path : `/${path}`;
156
-
157
- await mutex.acquire();
158
-
159
- try {
160
- if (!registry.routes.has(normalizedPath)) {
161
- return res.status(404).json({ success: false, error: 'Path not found' });
162
- }
163
-
164
- const route = registry.routes.get(normalizedPath);
165
- registry.routes.delete(normalizedPath);
166
- registry.usedPorts.delete(route.port);
167
-
168
- console.log(`[UNREGISTER] ${normalizedPath} (was on port ${route.port})`);
169
-
170
- res.json({ success: true, path: normalizedPath, freedPort: route.port });
171
-
172
- } catch (error) {
173
- console.error('[UNREGISTER ERROR]', error.message);
174
- res.status(500).json({ success: false, error: error.message });
175
- } finally {
176
- mutex.release();
177
- }
178
- });
179
-
180
- /**
181
- * GET /proxy/routes
182
- * Liste toutes les routes enregistrées
183
- */
184
- router.get('/proxy/routes', requireAuth, (req, res) => {
185
- const routes = [];
186
- registry.routes.forEach((value, path) => {
187
- routes.push({
188
- path,
189
- port: value.port,
190
- target: value.target || 'http://localhost',
191
- name: value.name,
192
- healthCheck: value.healthCheck,
193
- group: value.group || null,
194
- registeredAt: value.registeredAt,
195
- fullTarget: `${value.target || 'http://localhost'}:${value.port}`
196
- });
197
- });
198
-
199
- res.json({
200
- count: routes.length,
201
- routes,
202
- availablePorts: (INTERNAL_PORT_END - INTERNAL_PORT_START) - registry.usedPorts.size
203
- });
204
- });
205
-
206
- /**
207
- * POST /proxy/check
208
- * Déclenche manuellement un health check pour une route spécifique
209
- */
210
- router.post('/proxy/check', requireAuth, async (req, res) => {
211
- const { path } = req.body;
212
-
213
- if (!path) {
214
- return res.status(400).json({ success: false, error: 'Path is required' });
215
- }
216
-
217
- const normalizedPath = path.startsWith('/') ? path : `/${path}`;
218
- const route = registry.routes.get(normalizedPath);
219
-
220
- if (!route) {
221
- return res.status(404).json({ success: false, error: 'Path not found' });
222
- }
223
-
224
- console.log(`[MANUAL HEALTH CHECK] Checking ${normalizedPath}...`);
225
- const isHealthy = await checkServerHealth(normalizedPath, route);
226
-
227
- if (!isHealthy) {
228
- await unregisterServer(normalizedPath);
229
- return res.json({
230
- success: true,
231
- path: normalizedPath,
232
- healthy: false,
233
- message: 'Server was unhealthy and has been removed'
234
- });
235
- }
236
-
237
- res.json({ success: true, path: normalizedPath, healthy: true, message: 'Server is healthy' });
238
- });
239
-
240
- module.exports = router;
1
+ const { Router } = require('express');
2
+ const { requireLocalOnly, requireAuth } = require('../services/auth');
3
+ const { registry, mutex } = require('../services/registry');
4
+ const { getNextAvailablePort, releasePort, isLocalTarget, isPortFree, findRouteForPath } = require('../services/portManager');
5
+ const { checkServerHealth, unregisterServer } = require('../services/healthCheck');
6
+ const { INTERNAL_PORT_START, INTERNAL_PORT_END } = require('../config');
7
+
8
+ const router = Router();
9
+
10
+ /**
11
+ * Logique commune d'enregistrement (auto et manuel)
12
+ */
13
+ /**
14
+ * Normalise un nom de groupe : trim, et chaîne vide => undefined (non groupé).
15
+ */
16
+ function normalizeGroup(group) {
17
+ if (typeof group !== 'string') return undefined;
18
+ const trimmed = group.trim();
19
+ return trimmed === '' ? undefined : trimmed;
20
+ }
21
+
22
+ async function registerRouteInternal({ path, name, port, target, healthCheck, group, autoAssignPort }) {
23
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`;
24
+ const normalizedGroup = normalizeGroup(group);
25
+
26
+ let finalTarget = target || 'http://localhost';
27
+ if (!finalTarget.startsWith('http://') && !finalTarget.startsWith('https://')) {
28
+ finalTarget = `http://${finalTarget}`;
29
+ }
30
+ if (finalTarget.endsWith('/')) {
31
+ finalTarget = finalTarget.slice(0, -1);
32
+ }
33
+
34
+ await mutex.acquire();
35
+
36
+ let assignedPort = null;
37
+
38
+ try {
39
+ if (registry.routes.has(normalizedPath)) {
40
+ const existing = registry.routes.get(normalizedPath);
41
+
42
+ mutex.release();
43
+ const isAlive = await checkServerHealth(normalizedPath, existing);
44
+ await mutex.acquire();
45
+
46
+ if (!isAlive) {
47
+ console.log(`[REGISTER] Path ${normalizedPath} was occupied by a dead server. Overwriting...`);
48
+ registry.usedPorts.delete(existing.port);
49
+ registry.routes.delete(normalizedPath);
50
+ } else {
51
+ throw new Error('Path already registered');
52
+ }
53
+ }
54
+
55
+ if (!autoAssignPort) {
56
+ if (!port) throw new Error('Port is required for manual registration');
57
+ const manualPort = parseInt(port, 10);
58
+
59
+ if (registry.usedPorts.has(manualPort)) {
60
+ throw new Error(`Port ${manualPort} already in use`);
61
+ }
62
+ if (isLocalTarget(finalTarget) && !await isPortFree(manualPort)) {
63
+ throw new Error(`Port ${manualPort} is already in use by another process`);
64
+ }
65
+ assignedPort = manualPort;
66
+ registry.usedPorts.add(assignedPort);
67
+ } else {
68
+ assignedPort = await getNextAvailablePort(isLocalTarget(finalTarget));
69
+ }
70
+
71
+ registry.routes.set(normalizedPath, {
72
+ port: assignedPort,
73
+ target: finalTarget,
74
+ name: name || normalizedPath,
75
+ healthCheck: healthCheck !== undefined ? healthCheck === true || healthCheck === 'true' : true,
76
+ group: normalizedGroup,
77
+ registeredAt: new Date().toISOString()
78
+ });
79
+
80
+ console.log(`[REGISTER] ${normalizedPath} -> ${finalTarget}:${assignedPort} (${name || 'unnamed'}) [Health: ${healthCheck !== undefined ? healthCheck : 'default'}]${normalizedGroup ? ` [Group: ${normalizedGroup}]` : ''}`);
81
+
82
+ return {
83
+ success: true,
84
+ path: normalizedPath,
85
+ port: assignedPort,
86
+ target: finalTarget,
87
+ name: name || normalizedPath,
88
+ healthCheck: healthCheck !== undefined ? healthCheck === true || healthCheck === 'true' : true,
89
+ group: normalizedGroup,
90
+ message: `Route registered. Target: ${finalTarget}:${assignedPort}`
91
+ };
92
+
93
+ } catch (error) {
94
+ if (assignedPort !== null) {
95
+ if (!registry.routes.has(normalizedPath) || registry.routes.get(normalizedPath).port !== assignedPort) {
96
+ releasePort(assignedPort);
97
+ }
98
+ }
99
+ throw error;
100
+ } finally {
101
+ mutex.release();
102
+ }
103
+ }
104
+
105
+ /**
106
+ * POST /proxy/register
107
+ * Enregistrement automatique (rétro-compatibilité)
108
+ */
109
+ router.post('/proxy/register', requireLocalOnly, async (req, res) => {
110
+ const { path, name, target, healthCheck, group } = req.body;
111
+
112
+ if (!path) return res.status(400).json({ success: false, error: 'Path is required' });
113
+
114
+ try {
115
+ const result = await registerRouteInternal({ path, name, target, healthCheck, group, autoAssignPort: true });
116
+ res.json(result);
117
+ } catch (error) {
118
+ console.error('[REGISTER AUTO ERROR]', error.message);
119
+ const status = error.message.includes('already') ? 409 : 500;
120
+ res.status(status).json({ success: false, error: error.message });
121
+ }
122
+ });
123
+
124
+ /**
125
+ * POST /proxy/register-manual
126
+ * Enregistrement manuel via l'interface graphique (port obligatoire)
127
+ */
128
+ router.post('/proxy/register-manual', requireAuth, async (req, res) => {
129
+ const { path, name, port, target, healthCheck, group } = req.body;
130
+
131
+ if (!path) return res.status(400).json({ success: false, error: 'Path is required' });
132
+ if (!port) return res.status(400).json({ success: false, error: 'Port is required for manual registration' });
133
+
134
+ try {
135
+ const result = await registerRouteInternal({ path, name, port, target, healthCheck, group, autoAssignPort: false });
136
+ res.json(result);
137
+ } catch (error) {
138
+ console.error('[REGISTER MANUAL ERROR]', error.message);
139
+ const status = error.message.includes('already') ? 409 : 500;
140
+ res.status(status).json({ success: false, error: error.message });
141
+ }
142
+ });
143
+
144
+ /**
145
+ * DELETE /proxy/unregister
146
+ * Supprime une route
147
+ */
148
+ router.delete('/proxy/unregister', requireLocalOnly, async (req, res) => {
149
+ const { path } = req.body;
150
+
151
+ if (!path) {
152
+ return res.status(400).json({ success: false, error: 'Path is required' });
153
+ }
154
+
155
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`;
156
+
157
+ await mutex.acquire();
158
+
159
+ try {
160
+ if (!registry.routes.has(normalizedPath)) {
161
+ return res.status(404).json({ success: false, error: 'Path not found' });
162
+ }
163
+
164
+ const route = registry.routes.get(normalizedPath);
165
+ registry.routes.delete(normalizedPath);
166
+ registry.usedPorts.delete(route.port);
167
+
168
+ console.log(`[UNREGISTER] ${normalizedPath} (was on port ${route.port})`);
169
+
170
+ res.json({ success: true, path: normalizedPath, freedPort: route.port });
171
+
172
+ } catch (error) {
173
+ console.error('[UNREGISTER ERROR]', error.message);
174
+ res.status(500).json({ success: false, error: error.message });
175
+ } finally {
176
+ mutex.release();
177
+ }
178
+ });
179
+
180
+ /**
181
+ * GET /proxy/routes
182
+ * Liste toutes les routes enregistrées
183
+ */
184
+ router.get('/proxy/routes', requireAuth, (req, res) => {
185
+ const routes = [];
186
+ registry.routes.forEach((value, path) => {
187
+ routes.push({
188
+ path,
189
+ port: value.port,
190
+ target: value.target || 'http://localhost',
191
+ name: value.name,
192
+ healthCheck: value.healthCheck,
193
+ group: value.group || null,
194
+ registeredAt: value.registeredAt,
195
+ fullTarget: `${value.target || 'http://localhost'}:${value.port}`
196
+ });
197
+ });
198
+
199
+ res.json({
200
+ count: routes.length,
201
+ routes,
202
+ availablePorts: (INTERNAL_PORT_END - INTERNAL_PORT_START) - registry.usedPorts.size
203
+ });
204
+ });
205
+
206
+ /**
207
+ * POST /proxy/check
208
+ * Déclenche manuellement un health check pour une route spécifique
209
+ */
210
+ router.post('/proxy/check', requireAuth, async (req, res) => {
211
+ const { path } = req.body;
212
+
213
+ if (!path) {
214
+ return res.status(400).json({ success: false, error: 'Path is required' });
215
+ }
216
+
217
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`;
218
+ const route = registry.routes.get(normalizedPath);
219
+
220
+ if (!route) {
221
+ return res.status(404).json({ success: false, error: 'Path not found' });
222
+ }
223
+
224
+ console.log(`[MANUAL HEALTH CHECK] Checking ${normalizedPath}...`);
225
+ const isHealthy = await checkServerHealth(normalizedPath, route);
226
+
227
+ if (!isHealthy) {
228
+ await unregisterServer(normalizedPath);
229
+ return res.json({
230
+ success: true,
231
+ path: normalizedPath,
232
+ healthy: false,
233
+ message: 'Server was unhealthy and has been removed'
234
+ });
235
+ }
236
+
237
+ res.json({ success: true, path: normalizedPath, healthy: true, message: 'Server is healthy' });
238
+ });
239
+
240
+ module.exports = router;
@@ -1,95 +1,95 @@
1
- // NOTE: le timeout est appliqué globalement, pas par route individuelle
2
- const { HEALTH_CHECK_INTERVAL, HEALTH_CHECK_TIMEOUT, HEALTH_CHECK_GRACE_PERIOD } = require('../config');
3
- const { registry, mutex } = require('./registry');
4
-
5
- /**
6
- * Vérifie la santé d'un serveur enregistré
7
- */
8
- async function checkServerHealth(path, route) {
9
- const target = route.target || 'http://localhost';
10
- const healthUrl = `${target}:${route.port}/proxy/health`;
11
-
12
- try {
13
- const controller = new AbortController();
14
- const timeoutId = setTimeout(() => controller.abort(), HEALTH_CHECK_TIMEOUT);
15
- const response = await fetch(healthUrl, { method: 'GET', signal: controller.signal });
16
- clearTimeout(timeoutId);
17
- return response.status === 200;
18
- } catch {
19
- return false;
20
- }
21
- }
22
-
23
- /**
24
- * Désenregistre un serveur via le mutex
25
- */
26
- async function unregisterServer(path, reason = 'unhealthy') {
27
- await mutex.acquire();
28
- try {
29
- const route = registry.routes.get(path);
30
- if (route) {
31
- registry.routes.delete(path);
32
- registry.usedPorts.delete(route.port);
33
- console.log(`[UNREGISTER] ${path} (was on port ${route.port}) - reason: ${reason}`);
34
- }
35
- } finally {
36
- mutex.release();
37
- }
38
- }
39
-
40
- /**
41
- * Vérifie si un serveur est encore dans sa période de grâce
42
- */
43
- function isInGracePeriod(route) {
44
- const registeredAt = new Date(route.registeredAt).getTime();
45
- return (Date.now() - registeredAt) < HEALTH_CHECK_GRACE_PERIOD;
46
- }
47
-
48
- /**
49
- * Effectue un health check sur tous les serveurs enregistrés
50
- */
51
- async function performHealthChecks() {
52
- if (registry.routes.size === 0) return;
53
-
54
- const checks = [];
55
- let skippedCount = 0;
56
-
57
- for (const [path, route] of registry.routes.entries()) {
58
- if (route.healthCheck === false) continue;
59
- if (isInGracePeriod(route)) { skippedCount++; continue; }
60
- checks.push(
61
- checkServerHealth(path, route).then(isHealthy => ({ path, route, isHealthy }))
62
- );
63
- }
64
-
65
- if (checks.length === 0) {
66
- if (skippedCount > 0) {
67
- console.log(`[HEALTH CHECK] ${skippedCount} server(s) in grace period, skipping checks`);
68
- }
69
- return;
70
- }
71
-
72
- console.log(`[HEALTH CHECK] Checking ${checks.length} server(s)...${skippedCount > 0 ? ` (${skippedCount} in grace period)` : ''}`);
73
-
74
- const results = await Promise.all(checks);
75
-
76
- for (const { path, isHealthy } of results) {
77
- if (!isHealthy) await unregisterServer(path, 'failed health check');
78
- }
79
-
80
- const healthyCount = results.filter(r => r.isHealthy).length;
81
- const unhealthyCount = results.length - healthyCount;
82
- if (unhealthyCount > 0) {
83
- console.log(`[HEALTH CHECK] Complete: ${healthyCount} healthy, ${unhealthyCount} removed`);
84
- }
85
- }
86
-
87
- /**
88
- * Démarre le polling de health check
89
- */
90
- function startHealthCheckPolling() {
91
- console.log(`[HEALTH CHECK] Starting polling every ${HEALTH_CHECK_INTERVAL}ms`);
92
- setInterval(performHealthChecks, HEALTH_CHECK_INTERVAL);
93
- }
94
-
95
- module.exports = { checkServerHealth, unregisterServer, isInGracePeriod, performHealthChecks, startHealthCheckPolling };
1
+ // NOTE: le timeout est appliqué globalement, pas par route individuelle
2
+ const { HEALTH_CHECK_INTERVAL, HEALTH_CHECK_TIMEOUT, HEALTH_CHECK_GRACE_PERIOD } = require('../config');
3
+ const { registry, mutex } = require('./registry');
4
+
5
+ /**
6
+ * Vérifie la santé d'un serveur enregistré
7
+ */
8
+ async function checkServerHealth(path, route) {
9
+ const target = route.target || 'http://localhost';
10
+ const healthUrl = `${target}:${route.port}/proxy/health`;
11
+
12
+ try {
13
+ const controller = new AbortController();
14
+ const timeoutId = setTimeout(() => controller.abort(), HEALTH_CHECK_TIMEOUT);
15
+ const response = await fetch(healthUrl, { method: 'GET', signal: controller.signal });
16
+ clearTimeout(timeoutId);
17
+ return response.status === 200;
18
+ } catch {
19
+ return false;
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Désenregistre un serveur via le mutex
25
+ */
26
+ async function unregisterServer(path, reason = 'unhealthy') {
27
+ await mutex.acquire();
28
+ try {
29
+ const route = registry.routes.get(path);
30
+ if (route) {
31
+ registry.routes.delete(path);
32
+ registry.usedPorts.delete(route.port);
33
+ console.log(`[UNREGISTER] ${path} (was on port ${route.port}) - reason: ${reason}`);
34
+ }
35
+ } finally {
36
+ mutex.release();
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Vérifie si un serveur est encore dans sa période de grâce
42
+ */
43
+ function isInGracePeriod(route) {
44
+ const registeredAt = new Date(route.registeredAt).getTime();
45
+ return (Date.now() - registeredAt) < HEALTH_CHECK_GRACE_PERIOD;
46
+ }
47
+
48
+ /**
49
+ * Effectue un health check sur tous les serveurs enregistrés
50
+ */
51
+ async function performHealthChecks() {
52
+ if (registry.routes.size === 0) return;
53
+
54
+ const checks = [];
55
+ let skippedCount = 0;
56
+
57
+ for (const [path, route] of registry.routes.entries()) {
58
+ if (route.healthCheck === false) continue;
59
+ if (isInGracePeriod(route)) { skippedCount++; continue; }
60
+ checks.push(
61
+ checkServerHealth(path, route).then(isHealthy => ({ path, route, isHealthy }))
62
+ );
63
+ }
64
+
65
+ if (checks.length === 0) {
66
+ if (skippedCount > 0) {
67
+ console.log(`[HEALTH CHECK] ${skippedCount} server(s) in grace period, skipping checks`);
68
+ }
69
+ return;
70
+ }
71
+
72
+ console.log(`[HEALTH CHECK] Checking ${checks.length} server(s)...${skippedCount > 0 ? ` (${skippedCount} in grace period)` : ''}`);
73
+
74
+ const results = await Promise.all(checks);
75
+
76
+ for (const { path, isHealthy } of results) {
77
+ if (!isHealthy) await unregisterServer(path, 'failed health check');
78
+ }
79
+
80
+ const healthyCount = results.filter(r => r.isHealthy).length;
81
+ const unhealthyCount = results.length - healthyCount;
82
+ if (unhealthyCount > 0) {
83
+ console.log(`[HEALTH CHECK] Complete: ${healthyCount} healthy, ${unhealthyCount} removed`);
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Démarre le polling de health check
89
+ */
90
+ function startHealthCheckPolling() {
91
+ console.log(`[HEALTH CHECK] Starting polling every ${HEALTH_CHECK_INTERVAL}ms`);
92
+ setInterval(performHealthChecks, HEALTH_CHECK_INTERVAL);
93
+ }
94
+
95
+ module.exports = { checkServerHealth, unregisterServer, isInGracePeriod, performHealthChecks, startHealthCheckPolling };