/** * QUIXZOOM Replay Engine * * Spelar upp händelsekedjor i efterhand för debugging och utveckling. * * Exempel: * 08:13:01 Zoomer börjar filma * 08:13:07 Street Lamp observeras * 08:13:08 Identity Engine hittar objekt * 08:13:08 Evidence läggs till * 08:13:09 AI ser kunskapsgap * 08:13:10 Mission Generator skapar uppdrag * 08:13:11 Telefonen får instruktion: "Gå två meter närmare." * * Användning: * - Felsökning: Spela upp exakt vad som hände * - Regressionstest: Kör om samma events med ny kod * - Algoritm-jämförelse: Jämför resultat mellan versioner * - Demo: Visa hur systemet fungerar i realtid */ class ReplayEngine { constructor(options = {}) { this.speed = options.speed || 1.0; // 1.0 = realtid, 2.0 = dubbel hastighet this.events = []; this.handlers = new Map(); this.state = 'idle'; // idle, playing, paused, stopped this.currentIndex = 0; this.startTime = null; this.pauseTime = null; this.results = []; } /** * ============================================================ * REGISTRERA HANDLERS * ============================================================ */ registerHandler(eventType, handler) { this.handlers.set(eventType, handler); console.log(`[REPLAY] Registered handler for: ${eventType}`); } /** * ============================================================ * LADDA EVENTS * ============================================================ */ loadEvents(events) { // Sortera efter timestamp this.events = events.sort((a, b) => a.timestamp - b.timestamp); this.currentIndex = 0; console.log(`[REPLAY] Loaded ${events.length} events`); } loadFromEventStore(eventStore, filter = {}) { // Ladda från Event Store med filter const events = eventStore.getEvents(filter); this.loadEvents(events); } /** * ============================================================ * SPELA UPP * ============================================================ */ async play() { if (this.state === 'playing') { console.log('[REPLAY] Already playing'); return; } if (this.state === 'paused') { // Återuppta const pausedDuration = Date.now() - this.pauseTime; this.startTime += pausedDuration; this.state = 'playing'; console.log('[REPLAY] Resumed'); } else { // Ny uppspelning this.startTime = Date.now(); this.state = 'playing'; this.currentIndex = 0; this.results = []; console.log('[REPLAY] Started'); } // Spela upp events while (this.currentIndex < this.events.length && this.state === 'playing') { const event = this.events[this.currentIndex]; const eventTime = this.getEventTime(event); // Vänta tills det är dags const now = Date.now(); const elapsed = (now - this.startTime) * this.speed; const waitTime = eventTime - elapsed; if (waitTime > 0) { await this.sleep(waitTime / this.speed); } if (this.state !== 'playing') break; // Kör event await this.executeEvent(event); this.currentIndex++; } if (this.currentIndex >= this.events.length) { this.state = 'completed'; console.log('[REPLAY] Completed'); } } pause() { if (this.state === 'playing') { this.state = 'paused'; this.pauseTime = Date.now(); console.log('[REPLAY] Paused'); } } stop() { this.state = 'stopped'; console.log('[REPLAY] Stopped'); } /** * ============================================================ * EXECUTE EVENT * ============================================================ */ async executeEvent(event) { const handler = this.handlers.get(event.type); const context = { event, index: this.currentIndex, total: this.events.length, elapsed: Date.now() - this.startTime, results: this.results, }; console.log(`[REPLAY] [${this.formatTime(event.timestamp)}] ${event.type}`); if (handler) { try { const result = await handler(event.payload, context); this.results.push({ eventId: event.id, type: event.type, timestamp: event.timestamp, result, success: true, }); return result; } catch (error) { console.error(`[REPLAY] Error executing ${event.type}:`, error.message); this.results.push({ eventId: event.id, type: event.type, timestamp: event.timestamp, error: error.message, success: false, }); throw error; } } else { console.log(`[REPLAY] No handler for: ${event.type}`); this.results.push({ eventId: event.id, type: event.type, timestamp: event.timestamp, result: null, success: true, }); } } /** * ============================================================ * JÄMFÖR VERSIONER * ============================================================ */ async compareVersions(events, versionAHandlers, versionBHandlers) { console.log('[REPLAY] Comparing versions...'); // Kör version A const engineA = new ReplayEngine({ speed: 100 }); // Snabb uppspelning for (const [type, handler] of versionAHandlers) { engineA.registerHandler(type, handler); } engineA.loadEvents(events); await engineA.play(); // Kör version B const engineB = new ReplayEngine({ speed: 100 }); for (const [type, handler] of versionBHandlers) { engineB.registerHandler(type, handler); } engineB.loadEvents(events); await engineB.play(); // Jämför resultat const comparison = this.compareResults(engineA.results, engineB.results); return { versionA: { results: engineA.results, stats: engineA.getStats() }, versionB: { results: engineB.results, stats: engineB.getStats() }, comparison, }; } compareResults(resultsA, resultsB) { const differences = []; const minLength = Math.min(resultsA.length, resultsB.length); for (let i = 0; i < minLength; i++) { const a = resultsA[i]; const b = resultsB[i]; if (JSON.stringify(a.result) !== JSON.stringify(b.result)) { differences.push({ index: i, eventId: a.eventId, type: a.type, versionA: a.result, versionB: b.result, }); } } return { totalEvents: minLength, differences: differences.length, differenceRate: (differences.length / minLength * 100).toFixed(2) + '%', details: differences, }; } /** * ============================================================ * EXPORT/IMPORT * ============================================================ */ exportSession() { return { events: this.events, results: this.results, stats: this.getStats(), exportedAt: new Date().toISOString(), }; } importSession(session) { this.loadEvents(session.events); this.results = session.results || []; console.log(`[REPLAY] Imported session with ${session.events.length} events`); } /** * ============================================================ * STATISTIK * ============================================================ */ getStats() { const typeCounts = {}; for (const event of this.events) { typeCounts[event.type] = (typeCounts[event.type] || 0) + 1; } const successCount = this.results.filter(r => r.success).length; const errorCount = this.results.filter(r => !r.success).length; return { totalEvents: this.events.length, playedEvents: this.currentIndex, remainingEvents: this.events.length - this.currentIndex, typeCounts, successCount, errorCount, state: this.state, }; } /** * ============================================================ * HJÄLPMETODER * ============================================================ */ getEventTime(event) { // Relativ tid från första eventet if (!this.firstEventTime) { this.firstEventTime = this.events[0]?.timestamp || Date.now(); } return event.timestamp - this.firstEventTime; } formatTime(timestamp) { const date = new Date(timestamp); return date.toISOString().split('T')[1].split('.')[0]; } sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } } // Exportera module.exports = ReplayEngine; // Demo if (require.main === module) { const engine = new ReplayEngine({ speed: 10 }); // 10x hastighet för demo console.log('=== REPLAY ENGINE DEMO ===\n'); // Registrera handlers engine.registerHandler('zoomer_started', async (payload) => { console.log(` → Zoomer ${payload.zoomerId} started recording`); return { status: 'recording' }; }); engine.registerHandler('observation_created', async (payload) => { console.log(` → Object ${payload.objectType} observed`); return { objectId: payload.objectId, confidence: 0.92 }; }); engine.registerHandler('identity_matched', async (payload) => { console.log(` → Matched to ${payload.objectId} (${(payload.confidence * 100).toFixed(0)}% confidence)`); return { matched: true, objectId: payload.objectId }; }); engine.registerHandler('evidence_added', async (payload) => { console.log(` → Evidence added: ${payload.evidenceType}`); return { evidenceId: payload.evidenceId }; }); engine.registerHandler('knowledge_gap_detected', async (payload) => { console.log(` → Knowledge gap: ${payload.gapType}`); return { gapId: payload.gapId, priority: payload.priority }; }); engine.registerHandler('mission_created', async (payload) => { console.log(` → Mission created: ${payload.missionType}`); return { missionId: payload.missionId }; }); engine.registerHandler('instruction_sent', async (payload) => { console.log(` → Instruction: "${payload.instruction}"`); return { sent: true }; }); // Skapa scenario const baseTime = Date.now(); const events = [ { id: 'evt_001', type: 'zoomer_started', timestamp: baseTime, payload: { zoomerId: 'zoomer_00001' } }, { id: 'evt_002', type: 'observation_created', timestamp: baseTime + 6000, payload: { objectType: 'street_lamp', objectId: 'obj_001' } }, { id: 'evt_003', type: 'identity_matched', timestamp: baseTime + 7000, payload: { objectId: 'street_lamp_000001', confidence: 0.94 } }, { id: 'evt_004', type: 'evidence_added', timestamp: baseTime + 7000, payload: { evidenceType: 'image', evidenceId: 'ev_001' } }, { id: 'evt_005', type: 'knowledge_gap_detected', timestamp: baseTime + 8000, payload: { gapType: 'angle', gapId: 'gap_001', priority: 'medium' } }, { id: 'evt_006', type: 'mission_created', timestamp: baseTime + 9000, payload: { missionType: 'verify_angle', missionId: 'mission_001' } }, { id: 'evt_007', type: 'instruction_sent', timestamp: baseTime + 10000, payload: { instruction: 'Gå två meter närmare' } }, ]; // Ladda och spela engine.loadEvents(events); console.log('Starting playback...\n'); engine.play().then(() => { console.log('\n=== PLAYBACK COMPLETE ==='); console.log('\nStats:', engine.getStats()); console.log('\nResults:'); for (const result of engine.results) { console.log(` ${result.type}: ${result.success ? '✓' : '✗'} ${JSON.stringify(result.result)}`); } }); }