🪻 distributed transcription service thistle.dunkirk.sh

feat: add graceful shutdown handlers

Implements graceful shutdown on SIGTERM/SIGINT that:
- Stops accepting new requests via server.stop()
- Closes all active SSE streams (sync will reconnect on restart)
- Stops transcription service streams to Murmur
- Clears cleanup intervals (sessions, sync, files)
- Closes database connections cleanly

Prevents database corruption and ensures clean shutdown.

💘 Generated with Crush

Assisted-by: Claude Sonnet 4.5 via Crush <crush@charm.land>

dunkirk.sh 4dc7250a 191fdc63

verified
Changed files
+81 -3
src
+63 -3
src/index.ts
···
);
// Clean up expired sessions every hour
-
setInterval(cleanupExpiredSessions, 60 * 60 * 1000);
+
const sessionCleanupInterval = setInterval(
+
cleanupExpiredSessions,
+
60 * 60 * 1000,
+
);
// Helper function to sync user subscriptions from Polar
async function syncUserSubscriptionsFromPolar(
···
}
// Periodic sync every 5 minutes as backup (SSE handles real-time updates)
-
setInterval(
+
const syncInterval = setInterval(
async () => {
try {
await whisperService.syncWithWhisper();
···
);
// Clean up stale files daily
-
setInterval(() => whisperService.cleanupStaleFiles(), 24 * 60 * 60 * 1000);
+
const fileCleanupInterval = setInterval(
+
() => whisperService.cleanupStaleFiles(),
+
24 * 60 * 60 * 1000,
+
);
const server = Bun.serve({
port: process.env.PORT ? Number.parseInt(process.env.PORT, 10) : 3000,
···
// Event-driven SSE stream with reconnection support
const stream = new ReadableStream({
async start(controller) {
+
// Track this stream for graceful shutdown
+
activeSSEStreams.add(controller);
+
const encoder = new TextEncoder();
let isClosed = false;
let lastEventId = Math.floor(Date.now() / 1000);
···
current?.status === "failed"
) {
isClosed = true;
+
activeSSEStreams.delete(controller);
controller.close();
return;
···
isClosed = true;
clearInterval(heartbeatInterval);
transcriptionEvents.off(transcriptionId, updateHandler);
+
activeSSEStreams.delete(controller);
controller.close();
};
···
isClosed = true;
clearInterval(heartbeatInterval);
transcriptionEvents.off(transcriptionId, updateHandler);
+
activeSSEStreams.delete(controller);
};
},
});
···
},
});
console.log(`🪻 Thistle running at http://localhost:${server.port}`);
+
+
// Track active SSE streams for graceful shutdown
+
const activeSSEStreams = new Set<ReadableStreamDefaultController>();
+
+
// Graceful shutdown handler
+
let isShuttingDown = false;
+
+
async function shutdown(signal: string) {
+
if (isShuttingDown) return;
+
isShuttingDown = true;
+
+
console.log(`\n${signal} received, starting graceful shutdown...`);
+
+
// 1. Stop accepting new requests
+
console.log("[Shutdown] Closing server...");
+
server.stop();
+
+
// 2. Close all active SSE streams (safe to kill - sync will handle reconnection)
+
console.log(`[Shutdown] Closing ${activeSSEStreams.size} active SSE streams...`);
+
for (const controller of activeSSEStreams) {
+
try {
+
controller.close();
+
} catch {
+
// Already closed
+
}
+
}
+
activeSSEStreams.clear();
+
+
// 3. Stop transcription service (closes streams to Murmur)
+
whisperService.stop();
+
+
// 4. Stop cleanup intervals
+
console.log("[Shutdown] Stopping cleanup intervals...");
+
clearInterval(sessionCleanupInterval);
+
clearInterval(syncInterval);
+
clearInterval(fileCleanupInterval);
+
+
// 5. Close database connections
+
console.log("[Shutdown] Closing database...");
+
db.close();
+
+
console.log("[Shutdown] Complete");
+
process.exit(0);
+
}
+
+
// Register shutdown handlers
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
+
process.on("SIGINT", () => shutdown("SIGINT"));
+18
src/lib/transcription.ts
···
console.error("[Cleanup] Failed:", error);
}
}
+
+
stop(): void {
+
console.log("[Transcription] Closing active streams...");
+
// Close all active SSE streams to Murmur
+
for (const [transcriptionId, stream] of this.activeStreams.entries()) {
+
try {
+
stream.close();
+
this.streamLocks.delete(transcriptionId);
+
} catch (error) {
+
console.error(
+
`[Transcription] Error closing stream ${transcriptionId}:`,
+
error,
+
);
+
}
+
}
+
this.activeStreams.clear();
+
console.log("[Transcription] All streams closed");
+
}
}