Skip to main content
Glama
event-inbox.repo.ts4.86 kB
/** * EVENT INBOX REPOSITORY * * Manages the event queue for "autonomous" NPC actions. * Events are pushed by internal systems and polled by the frontend. */ import Database from 'better-sqlite3'; export type EventType = | 'npc_action' | 'combat_update' | 'world_change' | 'quest_update' | 'time_passage' | 'environmental' | 'system'; export type SourceType = 'npc' | 'combat' | 'world' | 'system' | 'scheduler'; export interface GameEvent { id?: number; eventType: EventType; payload: Record<string, any>; sourceType?: SourceType; sourceId?: string; priority?: number; createdAt?: string; consumedAt?: string | null; expiresAt?: string | null; } interface EventRow { id: number; event_type: string; payload: string; source_type: string | null; source_id: string | null; priority: number; created_at: string; consumed_at: string | null; expires_at: string | null; } export class EventInboxRepository { constructor(private db: Database.Database) {} /** * Push an event to the inbox */ push(event: Omit<GameEvent, 'id' | 'createdAt' | 'consumedAt'>): number { const stmt = this.db.prepare(` INSERT INTO event_inbox (event_type, payload, source_type, source_id, priority, expires_at) VALUES (?, ?, ?, ?, ?, ?) `); const result = stmt.run( event.eventType, JSON.stringify(event.payload), event.sourceType || null, event.sourceId || null, event.priority || 0, event.expiresAt || null ); return result.lastInsertRowid as number; } /** * Poll for unread events, ordered by priority then time */ poll(limit: number = 20): GameEvent[] { const now = new Date().toISOString(); // Get unconsumed, non-expired events const stmt = this.db.prepare(` SELECT * FROM event_inbox WHERE consumed_at IS NULL AND (expires_at IS NULL OR expires_at > ?) ORDER BY priority DESC, created_at ASC LIMIT ? `); const rows = stmt.all(now, limit) as EventRow[]; return rows.map(row => this.rowToEvent(row)); } /** * Mark events as consumed (read) */ markConsumed(ids: number[]): number { if (ids.length === 0) return 0; const now = new Date().toISOString(); const placeholders = ids.map(() => '?').join(','); const stmt = this.db.prepare(` UPDATE event_inbox SET consumed_at = ? WHERE id IN (${placeholders}) `); const result = stmt.run(now, ...ids); return result.changes; } /** * Poll and immediately mark as consumed (atomic) */ pollAndConsume(limit: number = 20): GameEvent[] { const events = this.poll(limit); const ids = events.map(e => e.id!).filter(Boolean); if (ids.length > 0) { this.markConsumed(ids); } return events; } /** * Get recent event history (including consumed) */ getHistory(options: { limit?: number; eventType?: EventType; sourceType?: SourceType; includeConsumed?: boolean; } = {}): GameEvent[] { const { limit = 50, eventType, sourceType, includeConsumed = true } = options; let query = 'SELECT * FROM event_inbox WHERE 1=1'; const params: any[] = []; if (!includeConsumed) { query += ' AND consumed_at IS NULL'; } if (eventType) { query += ' AND event_type = ?'; params.push(eventType); } if (sourceType) { query += ' AND source_type = ?'; params.push(sourceType); } query += ' ORDER BY created_at DESC LIMIT ?'; params.push(limit); const stmt = this.db.prepare(query); const rows = stmt.all(...params) as EventRow[]; return rows.map(row => this.rowToEvent(row)); } /** * Clean up old consumed events */ cleanup(olderThanDays: number = 7): number { const cutoff = new Date(); cutoff.setDate(cutoff.getDate() - olderThanDays); const stmt = this.db.prepare(` DELETE FROM event_inbox WHERE consumed_at IS NOT NULL AND created_at < ? `); const result = stmt.run(cutoff.toISOString()); return result.changes; } /** * Get count of pending (unconsumed) events */ getPendingCount(): number { const stmt = this.db.prepare(` SELECT COUNT(*) as count FROM event_inbox WHERE consumed_at IS NULL `); const row = stmt.get() as { count: number }; return row.count; } private rowToEvent(row: EventRow): GameEvent { return { id: row.id, eventType: row.event_type as EventType, payload: JSON.parse(row.payload), sourceType: row.source_type as SourceType | undefined, sourceId: row.source_id || undefined, priority: row.priority, createdAt: row.created_at, consumedAt: row.consumed_at, expiresAt: row.expires_at }; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Mnehmos/rpg-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server