feat: registerShutdownHandlers — SIGTERM/SIGINT stops heartbeat + unregisters worker

This commit is contained in:
Janpeter Visser 2026-05-01 14:56:44 +02:00
parent 994f28f103
commit 512b06cd75
2 changed files with 88 additions and 0 deletions

28
src/presence/shutdown.ts Normal file
View file

@ -0,0 +1,28 @@
import { unregisterWorker } from './worker.js'
export function registerShutdownHandlers(opts: {
userId: string
tokenId: string
stopHeartbeat: () => void
}): void {
let shuttingDown = false
const handleShutdown = async (signal: string) => {
if (shuttingDown) return
shuttingDown = true
console.log(`[scrum4me-mcp] ${signal} received — cleaning up worker presence`)
opts.stopHeartbeat()
try {
await unregisterWorker({ userId: opts.userId, tokenId: opts.tokenId })
} catch {
// best-effort
}
process.exit(0)
}
process.on('SIGTERM', () => { void handleShutdown('SIGTERM') })
process.on('SIGINT', () => { void handleShutdown('SIGINT') })
}

60
src/presence/worker.ts Normal file
View file

@ -0,0 +1,60 @@
import { Client } from 'pg'
import { prisma } from '../prisma.js'
export async function pgNotify(payload: Record<string, unknown>): Promise<void> {
const pg = new Client({ connectionString: process.env.DATABASE_URL })
await pg.connect()
await pg.query('SELECT pg_notify($1, $2)', ['scrum4me_changes', JSON.stringify(payload)])
await pg.end()
}
export async function registerWorker(opts: {
userId: string
tokenId: string
productId?: string | null
notify?: (payload: Record<string, unknown>) => Promise<void>
}): Promise<void> {
await prisma.claudeWorker.upsert({
where: { token_id: opts.tokenId },
create: {
user_id: opts.userId,
token_id: opts.tokenId,
product_id: opts.productId ?? null,
},
update: {
last_seen_at: new Date(),
product_id: opts.productId ?? null,
},
})
const notify = opts.notify ?? pgNotify
try {
await notify({
type: 'worker_connected',
user_id: opts.userId,
token_id: opts.tokenId,
product_id: opts.productId ?? null,
})
} catch {
// non-fatal
}
}
export async function unregisterWorker(opts: {
userId: string
tokenId: string
notify?: (payload: Record<string, unknown>) => Promise<void>
}): Promise<void> {
await prisma.claudeWorker.deleteMany({ where: { token_id: opts.tokenId } }).catch(() => {})
const notify = opts.notify ?? pgNotify
try {
await notify({
type: 'worker_disconnected',
user_id: opts.userId,
token_id: opts.tokenId,
})
} catch {
// non-fatal
}
}