From 512b06cd75ef61150259f6d1ecf5b549a27a4817 Mon Sep 17 00:00:00 2001 From: janpeter visser Date: Fri, 1 May 2026 14:56:44 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20registerShutdownHandlers=20=E2=80=94=20?= =?UTF-8?q?SIGTERM/SIGINT=20stops=20heartbeat=20+=20unregisters=20worker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/presence/shutdown.ts | 28 +++++++++++++++++++ src/presence/worker.ts | 60 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 src/presence/shutdown.ts create mode 100644 src/presence/worker.ts diff --git a/src/presence/shutdown.ts b/src/presence/shutdown.ts new file mode 100644 index 0000000..88d3526 --- /dev/null +++ b/src/presence/shutdown.ts @@ -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') }) +} diff --git a/src/presence/worker.ts b/src/presence/worker.ts new file mode 100644 index 0000000..762f286 --- /dev/null +++ b/src/presence/worker.ts @@ -0,0 +1,60 @@ +import { Client } from 'pg' +import { prisma } from '../prisma.js' + +export async function pgNotify(payload: Record): Promise { + 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) => Promise +}): Promise { + 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) => Promise +}): Promise { + 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 + } +}