import { performance } from 'node:perf_hooks' import type { Command, State } from '../src/types/screen.js' export function parseCommand(value: unknown): Command { if (!value || typeof value !== 'object') throw new Error('Некорректная команда') const c = value as Record if ( typeof c.id !== 'string' || !/^[a-zA-Z0-9-]{1,80}$/.test(c.id) || typeof c.action !== 'string' || !['play', 'pause', 'stop', 'seek'].includes(c.action) || typeof c.media !== 'string' || !['audio', 'video'].includes(c.media) || typeof c.position !== 'number' || !Number.isFinite(c.position) || c.position < 0 || c.position > 60 ) { throw new Error('Некорректная команда') } return { id: c.id, action: c.action as Command['action'], media: c.media as Command['media'], position: c.position, } } export class Controller { state: State = { revision: 0, command: null, status: 'idle', error: '', samples: [] } private seen = new Set() private startedAt = 0 constructor(private publish: (state: State) => void) {} command(input: unknown) { const command = parseCommand(input) if (this.seen.has(command.id)) return this.seen.add(command.id) // shortcut: помним 4096 команд стенда; долговременная дедупликация — SCREEN-004. if (this.seen.size > 4096) this.seen.delete(this.seen.values().next().value!) this.startedAt = performance.now() this.state = { ...this.state, revision: this.state.revision + 1, command, status: 'pending', error: '', } this.publish(this.state) } report(input: unknown) { if (!input || typeof input !== 'object') throw new Error('Некорректный отчёт') const r = input as Record if ( typeof r.status !== 'string' || !['playing', 'paused', 'stopped', 'ended', 'error'].includes(r.status) || typeof r.id !== 'string' || (r.error !== undefined && typeof r.error !== 'string') ) throw new Error('Некорректный отчёт') if ( r.id !== this.state.command?.id || (this.state.status !== 'pending' && r.status !== 'ended' && r.status !== 'error') ) return const samples = r.status === 'playing' && this.state.command.action === 'play' ? [ ...this.state.samples.slice(-399), { media: this.state.command.media, milliseconds: performance.now() - this.startedAt, }, ] : this.state.samples this.state = { ...this.state, revision: this.state.revision + 1, status: r.status as State['status'], error: String(r.error ?? '').slice(0, 500), samples, } this.publish(this.state) } }