feat: presence layer — registerWorker, startHeartbeat, registerShutdownHandlers

This commit is contained in:
Janpeter Visser 2026-05-01 14:31:39 +02:00
parent 966a7484c5
commit 1d6652b7c8
3 changed files with 104 additions and 0 deletions

23
src/presence/heartbeat.ts Normal file
View file

@ -0,0 +1,23 @@
import { prisma } from '../prisma.js'
export function startHeartbeat(opts: {
tokenId: string
intervalMs?: number
}): { stop: () => void } {
const timer = setInterval(async () => {
try {
const result = await prisma.claudeWorker.updateMany({
where: { token_id: opts.tokenId },
data: { last_seen_at: new Date() },
})
if (result.count === 0) {
console.error('[scrum4me-mcp] Heartbeat: worker record not found — token may be revoked. Stopping.')
clearInterval(timer)
}
} catch {
// non-fatal
}
}, opts.intervalMs ?? 5_000)
return { stop: () => clearInterval(timer) }
}

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

@ -0,0 +1,20 @@
import { unregisterWorker } from './worker.js'
export function registerShutdownHandlers(opts: {
userId: string
tokenId: string
stopHeartbeat: () => void
}): void {
let exiting = false
const shutdown = async () => {
if (exiting) return
exiting = true
opts.stopHeartbeat()
await unregisterWorker({ userId: opts.userId, tokenId: opts.tokenId })
process.exit(0)
}
process.on('SIGTERM', () => void shutdown())
process.on('SIGINT', () => void shutdown())
}

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

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