Phase 3 scaffold: chat engine (WebSocket, FCM, pricing, timer, extension, history)
- Backend: WebSocket plugin, chat/pricing/timer/extension/closure/notification services - Client app: ChatBloc, pricing dialog, chat screen with message status, extension/goodbye flow, history - Mitra app: MitraChatBloc, ExtensionBloc, chat screen, extension accept/reject, history - Control center: free trial, extension timeout, early end config toggles - DB migration: chat_messages, session_closures, session_extensions, customer_transactions tables Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
100
backend/src/services/session-timer.service.js
Normal file
100
backend/src/services/session-timer.service.js
Normal file
@@ -0,0 +1,100 @@
|
||||
import { getDb } from '../db/client.js'
|
||||
import { publish } from '../plugins/valkey.js'
|
||||
import { sendToSessionParticipant } from '../plugins/websocket.js'
|
||||
|
||||
const sql = getDb()
|
||||
|
||||
// Active session timers: sessionId → { warningTimeout, expiryTimeout }
|
||||
const sessionTimers = new Map()
|
||||
|
||||
export const startSessionTimer = (sessionId, expiresAt) => {
|
||||
const now = Date.now()
|
||||
const expiresMs = new Date(expiresAt).getTime()
|
||||
const warningMs = expiresMs - 60_000 // 1 minute before expiry
|
||||
|
||||
// Clear any existing timers
|
||||
clearSessionTimer(sessionId)
|
||||
|
||||
const timers = {}
|
||||
|
||||
// Warning timer (1 min before expiry)
|
||||
if (warningMs > now) {
|
||||
timers.warningTimeout = setTimeout(() => {
|
||||
onSessionWarning(sessionId)
|
||||
}, warningMs - now)
|
||||
}
|
||||
|
||||
// Expiry timer
|
||||
if (expiresMs > now) {
|
||||
timers.expiryTimeout = setTimeout(() => {
|
||||
onSessionExpired(sessionId)
|
||||
}, expiresMs - now)
|
||||
} else {
|
||||
// Already expired
|
||||
onSessionExpired(sessionId)
|
||||
return
|
||||
}
|
||||
|
||||
sessionTimers.set(sessionId, timers)
|
||||
}
|
||||
|
||||
export const clearSessionTimer = (sessionId) => {
|
||||
const timers = sessionTimers.get(sessionId)
|
||||
if (timers) {
|
||||
if (timers.warningTimeout) clearTimeout(timers.warningTimeout)
|
||||
if (timers.expiryTimeout) clearTimeout(timers.expiryTimeout)
|
||||
sessionTimers.delete(sessionId)
|
||||
}
|
||||
}
|
||||
|
||||
export const extendSessionTimer = async (sessionId, additionalMinutes) => {
|
||||
const [session] = await sql`
|
||||
UPDATE chat_sessions
|
||||
SET expires_at = expires_at + ${additionalMinutes + ' minutes'}::interval,
|
||||
extended_minutes = extended_minutes + ${additionalMinutes}
|
||||
WHERE id = ${sessionId}
|
||||
RETURNING id, expires_at
|
||||
`
|
||||
if (session) {
|
||||
startSessionTimer(sessionId, session.expires_at)
|
||||
}
|
||||
return session
|
||||
}
|
||||
|
||||
const onSessionWarning = (sessionId) => {
|
||||
const data = { type: 'session_timer', remaining_seconds: 60, session_id: sessionId }
|
||||
sendToSessionParticipant(sessionId, 'customer', data)
|
||||
sendToSessionParticipant(sessionId, 'mitra', data)
|
||||
}
|
||||
|
||||
const onSessionExpired = async (sessionId) => {
|
||||
clearSessionTimer(sessionId)
|
||||
|
||||
// Check session is still active
|
||||
const [session] = await sql`
|
||||
SELECT id, status FROM chat_sessions WHERE id = ${sessionId} AND status = 'active'
|
||||
`
|
||||
if (!session) return
|
||||
|
||||
// Notify both parties
|
||||
const data = { type: 'session_expired', session_id: sessionId }
|
||||
sendToSessionParticipant(sessionId, 'customer', data)
|
||||
sendToSessionParticipant(sessionId, 'mitra', data)
|
||||
|
||||
// Also publish via Valkey for any listeners
|
||||
await publish(`session:${sessionId}:status`, data)
|
||||
}
|
||||
|
||||
// Restore timers for active sessions on server restart
|
||||
export const restoreActiveTimers = async () => {
|
||||
const activeSessions = await sql`
|
||||
SELECT id, expires_at FROM chat_sessions
|
||||
WHERE status = 'active' AND expires_at IS NOT NULL AND expires_at > NOW()
|
||||
`
|
||||
for (const session of activeSessions) {
|
||||
startSessionTimer(session.id, session.expires_at)
|
||||
}
|
||||
if (activeSessions.length > 0) {
|
||||
console.log(`Restored ${activeSessions.length} session timer(s)`)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user