up
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
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<string, unknown>
|
||||
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<string>()
|
||||
private startedAt = 0
|
||||
private origin: 'computer' | 'phone' = 'computer'
|
||||
constructor(private publish: (state: State) => void) {}
|
||||
command(input: unknown, origin: 'computer' | 'phone' = 'computer') {
|
||||
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.origin = origin
|
||||
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<string, unknown>
|
||||
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,
|
||||
origin: this.origin,
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user