This commit is contained in:
seb
2026-08-07 03:51:54 +02:00
parent 57fd3b4837
commit 2767568f5a
2 changed files with 30 additions and 8 deletions

View File

@@ -8,6 +8,7 @@ import { loggerMiddleware } from './middleware/logger.js';
import env from '../config/env.js';
import { authMiddleware } from './middleware/auth.js';
import { ensureConnection } from '../db/store.js';
import { closeAll as closeAllSse } from './sse.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -133,8 +134,12 @@ export async function startApi() {
export async function stopApi() {
if (!server) return;
// Long-lived SSE (and keep-alive) sockets otherwise block server.close()
// forever — node --watch hangs on "Waiting for graceful termination...".
closeAllSse();
await new Promise((resolve, reject) => {
server.close((err) => (err ? reject(err) : resolve()));
server.closeAllConnections();
});
server = undefined;
}

View File

@@ -3,7 +3,7 @@
* after a successful mutation so other tabs/users can refetch.
*/
/** @typedef {{ res: import('express').Response, companyId: number, userId: number }} SseClient */
/** @typedef {{ res: import('express').Response, companyId: number, userId: number, keepAlive: NodeJS.Timeout }} SseClient */
/** @type {Set<SseClient>} */
const clients = new Set();
@@ -18,16 +18,19 @@ const KEEP_ALIVE_MS = 25_000;
*/
export function subscribe(res, { companyId, userId }) {
/** @type {SseClient} */
const client = { res, companyId, userId };
const client = {
res,
companyId,
userId,
keepAlive: setInterval(() => {
if (res.writableEnded) return;
res.write(': keepalive\n\n');
}, KEEP_ALIVE_MS),
};
clients.add(client);
const keepAlive = setInterval(() => {
if (res.writableEnded) return;
res.write(': keepalive\n\n');
}, KEEP_ALIVE_MS);
const unsubscribe = () => {
clearInterval(keepAlive);
clearInterval(client.keepAlive);
clients.delete(client);
};
@@ -35,6 +38,20 @@ export function subscribe(res, { companyId, userId }) {
return unsubscribe;
}
/**
* End every open SSE stream so `server.close()` can finish (e.g. node --watch).
* EventSource clients reconnect to the new process.
*/
export function closeAll() {
for (const client of [...clients]) {
clearInterval(client.keepAlive);
clients.delete(client);
if (!client.res.writableEnded) {
client.res.end();
}
}
}
/**
* Broadcast a named signal to subscribers of the given company.
* @param {string} signal - SSE event name (e.g. `category`)