fluxy-bot 0.4.15 → 0.4.16
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/package.json +1 -1
- package/supervisor/backend.ts +1 -103
- package/supervisor/index.ts +20 -39
package/package.json
CHANGED
package/supervisor/backend.ts
CHANGED
|
@@ -1,24 +1,11 @@
|
|
|
1
1
|
import { spawn, type ChildProcess } from 'child_process';
|
|
2
|
-
import fs from 'fs';
|
|
3
2
|
import path from 'path';
|
|
4
3
|
import { PKG_DIR } from '../shared/paths.js';
|
|
5
4
|
import { log } from '../shared/logger.js';
|
|
6
5
|
|
|
7
6
|
let child: ChildProcess | null = null;
|
|
8
7
|
let restarts = 0;
|
|
9
|
-
let lastSpawnTime = 0;
|
|
10
|
-
let pendingRestartTimer: ReturnType<typeof setTimeout> | null = null;
|
|
11
8
|
const MAX_RESTARTS = 3;
|
|
12
|
-
const STABLE_THRESHOLD_MS = 30_000; // 30s uptime = reset counter
|
|
13
|
-
|
|
14
|
-
// File watcher state
|
|
15
|
-
let watcher: fs.FSWatcher | null = null;
|
|
16
|
-
let restartInProgress = false;
|
|
17
|
-
let restartInProgressTimer: ReturnType<typeof setTimeout> | null = null;
|
|
18
|
-
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
19
|
-
|
|
20
|
-
const IGNORED_EXTENSIONS = new Set(['.db', '.sqlite', '.db-journal', '.db-wal']);
|
|
21
|
-
const IGNORED_DIRS = new Set(['node_modules', '.git']);
|
|
22
9
|
|
|
23
10
|
export function getBackendPort(basePort: number): number {
|
|
24
11
|
return basePort + 4;
|
|
@@ -33,8 +20,6 @@ export function spawnBackend(port: number): ChildProcess {
|
|
|
33
20
|
env: { ...process.env, BACKEND_PORT: String(port) },
|
|
34
21
|
});
|
|
35
22
|
|
|
36
|
-
lastSpawnTime = Date.now();
|
|
37
|
-
|
|
38
23
|
child.stdout?.on('data', (d) => {
|
|
39
24
|
process.stdout.write(d);
|
|
40
25
|
});
|
|
@@ -46,20 +31,10 @@ export function spawnBackend(port: number): ChildProcess {
|
|
|
46
31
|
child.on('exit', (code) => {
|
|
47
32
|
if (code !== 0 && code !== null) {
|
|
48
33
|
log.warn(`Backend crashed (code ${code})`);
|
|
49
|
-
|
|
50
|
-
// Auto-reset counter if the process was stable (alive >30s)
|
|
51
|
-
const uptime = Date.now() - lastSpawnTime;
|
|
52
|
-
if (uptime > STABLE_THRESHOLD_MS) {
|
|
53
|
-
restarts = 0;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
34
|
if (restarts < MAX_RESTARTS) {
|
|
57
35
|
restarts++;
|
|
58
36
|
log.info(`Restarting backend (${restarts}/${MAX_RESTARTS})...`);
|
|
59
|
-
|
|
60
|
-
pendingRestartTimer = null;
|
|
61
|
-
spawnBackend(port);
|
|
62
|
-
}, 1000);
|
|
37
|
+
setTimeout(() => spawnBackend(port), 1000);
|
|
63
38
|
} else {
|
|
64
39
|
log.error('Backend failed too many times. Use Fluxy chat to debug.');
|
|
65
40
|
}
|
|
@@ -71,11 +46,6 @@ export function spawnBackend(port: number): ChildProcess {
|
|
|
71
46
|
}
|
|
72
47
|
|
|
73
48
|
export function stopBackend(): void {
|
|
74
|
-
// Clear any pending crash-recovery restart
|
|
75
|
-
if (pendingRestartTimer) {
|
|
76
|
-
clearTimeout(pendingRestartTimer);
|
|
77
|
-
pendingRestartTimer = null;
|
|
78
|
-
}
|
|
79
49
|
child?.kill();
|
|
80
50
|
child = null;
|
|
81
51
|
}
|
|
@@ -87,75 +57,3 @@ export function isBackendAlive(): boolean {
|
|
|
87
57
|
export function resetBackendRestarts(): void {
|
|
88
58
|
restarts = 0;
|
|
89
59
|
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Mark that a restart is already in progress (e.g. from Fluxy agent bot:done).
|
|
93
|
-
* The file watcher will skip triggering a restart for the next 2 seconds.
|
|
94
|
-
*/
|
|
95
|
-
export function markRestartInProgress(): void {
|
|
96
|
-
restartInProgress = true;
|
|
97
|
-
if (restartInProgressTimer) clearTimeout(restartInProgressTimer);
|
|
98
|
-
restartInProgressTimer = setTimeout(() => {
|
|
99
|
-
restartInProgress = false;
|
|
100
|
-
restartInProgressTimer = null;
|
|
101
|
-
}, 2000);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Watch workspace/backend/ for file changes and auto-restart.
|
|
106
|
-
* Returns a cleanup function.
|
|
107
|
-
*/
|
|
108
|
-
export function startBackendWatcher(port: number, onRestart?: () => void): void {
|
|
109
|
-
const watchDir = path.join(PKG_DIR, 'workspace', 'backend');
|
|
110
|
-
|
|
111
|
-
try {
|
|
112
|
-
watcher = fs.watch(watchDir, { recursive: true }, (_event, filename) => {
|
|
113
|
-
if (!filename) return;
|
|
114
|
-
|
|
115
|
-
// Filter out ignored files
|
|
116
|
-
const ext = path.extname(filename);
|
|
117
|
-
if (IGNORED_EXTENSIONS.has(ext)) return;
|
|
118
|
-
|
|
119
|
-
// Filter out ignored directories
|
|
120
|
-
const parts = filename.split(path.sep);
|
|
121
|
-
if (parts.some((p) => IGNORED_DIRS.has(p))) return;
|
|
122
|
-
|
|
123
|
-
// Debounce rapid saves (500ms)
|
|
124
|
-
if (debounceTimer) clearTimeout(debounceTimer);
|
|
125
|
-
debounceTimer = setTimeout(() => {
|
|
126
|
-
debounceTimer = null;
|
|
127
|
-
|
|
128
|
-
// Skip if a restart was already triggered (e.g. by Fluxy agent)
|
|
129
|
-
if (restartInProgress) {
|
|
130
|
-
log.info(`[watcher] File changed (${filename}) — restart already in progress, skipping`);
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
log.info(`[watcher] File changed: ${filename} — restarting backend`);
|
|
135
|
-
resetBackendRestarts();
|
|
136
|
-
stopBackend();
|
|
137
|
-
spawnBackend(port);
|
|
138
|
-
onRestart?.();
|
|
139
|
-
}, 500);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
log.ok(`Watching workspace/backend/ for changes`);
|
|
143
|
-
} catch (err) {
|
|
144
|
-
log.warn(`File watcher failed: ${err instanceof Error ? err.message : err}`);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
export function stopBackendWatcher(): void {
|
|
149
|
-
if (watcher) {
|
|
150
|
-
watcher.close();
|
|
151
|
-
watcher = null;
|
|
152
|
-
}
|
|
153
|
-
if (debounceTimer) {
|
|
154
|
-
clearTimeout(debounceTimer);
|
|
155
|
-
debounceTimer = null;
|
|
156
|
-
}
|
|
157
|
-
if (restartInProgressTimer) {
|
|
158
|
-
clearTimeout(restartInProgressTimer);
|
|
159
|
-
restartInProgressTimer = null;
|
|
160
|
-
}
|
|
161
|
-
}
|
package/supervisor/index.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { PKG_DIR } from '../shared/paths.js';
|
|
|
10
10
|
import { log } from '../shared/logger.js';
|
|
11
11
|
import { startTunnel, stopTunnel, isTunnelAlive, restartTunnel } from './tunnel.js';
|
|
12
12
|
import { spawnWorker, stopWorker, getWorkerPort, isWorkerAlive } from './worker.js';
|
|
13
|
-
import { spawnBackend, stopBackend, getBackendPort,
|
|
13
|
+
import { spawnBackend, stopBackend, getBackendPort, isBackendAlive, resetBackendRestarts } from './backend.js';
|
|
14
14
|
import { updateTunnelUrl, startHeartbeat, stopHeartbeat, disconnect } from '../shared/relay.js';
|
|
15
15
|
import { startFluxyAgentQuery, stopFluxyAgentQuery, clearFluxySession } from './fluxy-agent.js';
|
|
16
16
|
import { ensureFileDirs, saveAttachment, type SavedFile } from './file-saver.js';
|
|
@@ -162,44 +162,30 @@ export async function startSupervisor() {
|
|
|
162
162
|
return;
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
// App API routes → proxy to user's backend server
|
|
165
|
+
// App API routes → proxy to user's backend server
|
|
166
166
|
if (req.url?.startsWith('/app/api')) {
|
|
167
167
|
const backendPath = req.url.replace(/^\/app\/api/, '') || '/';
|
|
168
168
|
console.log(`[supervisor] → backend :${backendPort} | ${req.method} ${backendPath}`);
|
|
169
|
+
if (!isBackendAlive()) {
|
|
170
|
+
console.log('[supervisor] Backend down — returning 503');
|
|
171
|
+
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
172
|
+
res.end(JSON.stringify({ error: 'Backend is starting...' }));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
169
175
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
{ host: '127.0.0.1', port: backendPort, path: backendPath, method: req.method, headers: req.headers },
|
|
182
|
-
(proxyRes) => {
|
|
183
|
-
res.writeHead(proxyRes.statusCode!, proxyRes.headers);
|
|
184
|
-
proxyRes.pipe(res);
|
|
185
|
-
},
|
|
186
|
-
);
|
|
187
|
-
proxy.on('error', (e: NodeJS.ErrnoException) => {
|
|
188
|
-
if (e.code === 'ECONNREFUSED' && attempt < MAX_RETRIES) {
|
|
189
|
-
attempt++;
|
|
190
|
-
console.log(`[supervisor] Backend not ready, retry ${attempt}/${MAX_RETRIES}...`);
|
|
191
|
-
setTimeout(tryProxy, RETRY_DELAY);
|
|
192
|
-
} else {
|
|
193
|
-
console.error(`[supervisor] Backend proxy error: ${req.url}`, e.message);
|
|
194
|
-
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
195
|
-
res.end(JSON.stringify({ error: 'Backend unavailable' }));
|
|
196
|
-
}
|
|
197
|
-
});
|
|
198
|
-
proxy.end(body);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
tryProxy();
|
|
176
|
+
const proxy = http.request(
|
|
177
|
+
{ host: '127.0.0.1', port: backendPort, path: backendPath, method: req.method, headers: req.headers },
|
|
178
|
+
(proxyRes) => {
|
|
179
|
+
res.writeHead(proxyRes.statusCode!, proxyRes.headers);
|
|
180
|
+
proxyRes.pipe(res);
|
|
181
|
+
},
|
|
182
|
+
);
|
|
183
|
+
proxy.on('error', (e) => {
|
|
184
|
+
console.error(`[supervisor] Backend proxy error: ${req.url}`, e.message);
|
|
185
|
+
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
186
|
+
res.end(JSON.stringify({ error: 'Backend unavailable' }));
|
|
202
187
|
});
|
|
188
|
+
req.pipe(proxy);
|
|
203
189
|
return;
|
|
204
190
|
}
|
|
205
191
|
|
|
@@ -437,7 +423,6 @@ export async function startSupervisor() {
|
|
|
437
423
|
if (eventData.usedFileTools) {
|
|
438
424
|
console.log('[supervisor] File tools used — Vite HMR will apply changes automatically');
|
|
439
425
|
console.log('[supervisor] Restarting backend...');
|
|
440
|
-
markRestartInProgress(); // prevent file watcher from double-restarting
|
|
441
426
|
resetBackendRestarts();
|
|
442
427
|
stopBackend();
|
|
443
428
|
spawnBackend(backendPort);
|
|
@@ -579,9 +564,6 @@ export async function startSupervisor() {
|
|
|
579
564
|
// Spawn worker + backend
|
|
580
565
|
spawnWorker(workerPort);
|
|
581
566
|
spawnBackend(backendPort);
|
|
582
|
-
startBackendWatcher(backendPort, () => {
|
|
583
|
-
broadcastFluxy('app:hmr-update');
|
|
584
|
-
});
|
|
585
567
|
|
|
586
568
|
// Tunnel
|
|
587
569
|
let tunnelUrl: string | null = null;
|
|
@@ -661,7 +643,6 @@ export async function startSupervisor() {
|
|
|
661
643
|
delete latestConfig.tunnelUrl;
|
|
662
644
|
saveConfig(latestConfig);
|
|
663
645
|
stopWorker();
|
|
664
|
-
stopBackendWatcher();
|
|
665
646
|
stopBackend();
|
|
666
647
|
stopTunnel();
|
|
667
648
|
console.log('[supervisor] Stopping Vite dev servers...');
|