feat: presence layer — registerWorker, startHeartbeat, registerShutdownHandlers
This commit is contained in:
parent
966a7484c5
commit
1d6652b7c8
3 changed files with 104 additions and 0 deletions
23
src/presence/heartbeat.ts
Normal file
23
src/presence/heartbeat.ts
Normal 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
20
src/presence/shutdown.ts
Normal 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
61
src/presence/worker.ts
Normal 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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue