/
mentoster
/
vk-stilus
Обзор
Документация
Войти
/
mentoster
/
vk-stilus
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
3
CI/CD
Аналитика
Безопасность
master
src/features/background/slideshow-controller.ts
216 строк
8 KB
mentoster
fix(background): restore audio track slideshow
28 июл 2026, 16:00
28 июл 2026, 16:00
46b2202
Код
Авторство
О чём код?
import { verifyInvariant } from '../../core/invariant.js'; import { err, ok } from '../../core/result.js'; import type { MediaResult, SlideshowScheduler } from './background-types.js'; const SLIDESHOW_CONTEXT = Object.freeze({ owner: 'SlideshowController' }); const MAX_SLIDESHOW_ITEMS = 50; const MIN_INTERVAL_MS = 1_000; const MAX_INTERVAL_MS = 3_600_000; /** @assertion-exception Pure slideshow error factory owns no lifecycle state. */ function slideshowError(code: string, message: string) { return Object.freeze({ code, message }); } /** @assertion-exception Pure item-list validator owns no lifecycle state. */ function validateItems(items: readonly string[]): MediaResult<readonly string[]> { const validEntries = Array.isArray(items) && items.every((item) => typeof item === 'string' && item); if (!validEntries) { return err(slideshowError('SLIDESHOW_ITEMS_INVALID', 'slideshow items must be non-empty strings')); } if (items.length > MAX_SLIDESHOW_ITEMS) { return err(slideshowError('SLIDESHOW_ITEM_LIMIT_EXCEEDED', 'slideshow supports at most 50 items')); } return ok(Object.freeze([...(items as readonly string[])])); } /** @assertion-exception Pure interval validator owns no lifecycle state. */ function validateInterval(intervalMs: number): MediaResult<number> { const integer = Number.isInteger(intervalMs); const bounded = intervalMs >= MIN_INTERVAL_MS && intervalMs <= MAX_INTERVAL_MS; return integer && bounded ? ok(intervalMs) : err(slideshowError('SLIDESHOW_INTERVAL_INVALID', 'slideshow interval is outside bounds')); } /** * @classdesc Owns one bounded background slideshow interval and immutable item list. * @responsibility Cycle at most 50 validated background references in deterministic or random order. * @nonresponsibility Resolve media URLs, render DOM, or react to audio-track changes. * @trustBoundary Validates item count, callback, scheduler, interval, and random source. * @lifecycle Empty -> configured -> running/stopped -> disposed. * @invariant At most one interval exists and current index stays inside the configured list. * @resourceBudget One interval and 50 immutable item references. * @sideEffects Invokes the injected change callback and scheduler. */ export class SlideshowController { readonly scheduler: SlideshowScheduler; readonly changeCallback: (value: string) => void; readonly random: () => number; items: readonly string[] = Object.freeze([]); intervalMs = 0; randomOrder = false; index = 0; intervalId: unknown | null = null; readonly tickHandler: () => void; constructor( options: Readonly<{ scheduler: SlideshowScheduler; onChange: (value: string) => void; random?: () => number; }>, ) { this.scheduler = options.scheduler; this.changeCallback = options.onChange; this.random = options.random ?? Math.random; this.tickHandler = this.tick.bind(this); const schedulerValid = typeof this.scheduler.setInterval === 'function'; const callbackValid = typeof this.changeCallback === 'function'; const schedulerInvariant = verifyInvariant( schedulerValid, 'slideshow.construct.scheduler', SLIDESHOW_CONTEXT, ); const callbackInvariant = verifyInvariant( callbackValid, 'slideshow.construct.callback', SLIDESHOW_CONTEXT, ); if (!schedulerInvariant.ok || !callbackInvariant.ok) { throw new TypeError('SlideshowController options are invalid'); } } configure( items: readonly string[], intervalMs: number, randomOrder: boolean, ): MediaResult<ReturnType<SlideshowController['snapshot']>> { const validatedItems = validateItems(items); if (!validatedItems.ok) return validatedItems; const validatedInterval = validateInterval(intervalMs); if (!validatedInterval.ok) return validatedInterval; const stopped = this.stop(); if (!stopped.ok) return stopped; this.items = validatedItems.value; this.intervalMs = validatedInterval.value; this.randomOrder = Boolean(randomOrder); this.index = 0; const listBounded = this.items.length <= MAX_SLIDESHOW_ITEMS; const indexZero = this.index === 0; const listInvariant = verifyInvariant( listBounded, 'slideshow.configure.items', SLIDESHOW_CONTEXT, ); const indexInvariant = verifyInvariant( indexZero, 'slideshow.configure.index', SLIDESHOW_CONTEXT, ); if (!listInvariant.ok) return err(listInvariant.error); return indexInvariant.ok ? ok(this.snapshot()) : err(indexInvariant.error); } start(): MediaResult<ReturnType<SlideshowController['snapshot']>> { if (this.intervalId !== null) return ok(this.snapshot()); if (this.items.length < 2) { return err(slideshowError('SLIDESHOW_ITEMS_INSUFFICIENT', 'slideshow needs at least two items')); } this.intervalId = this.scheduler.setInterval(this.tickHandler, this.intervalMs); const intervalOwned = this.intervalId !== null; const itemsReady = this.items.length >= 2; const intervalInvariant = verifyInvariant( intervalOwned, 'slideshow.start.interval', SLIDESHOW_CONTEXT, ); const itemsInvariant = verifyInvariant(itemsReady, 'slideshow.start.items', SLIDESHOW_CONTEXT); if (!intervalInvariant.ok) return err(intervalInvariant.error); return itemsInvariant.ok ? ok(this.snapshot()) : err(itemsInvariant.error); } /** @assertion-exception Random-index calculation is bounded by configured item count. */ nextIndex(): number { if (!this.randomOrder) return (this.index + 1) % this.items.length; const offset = 1 + Math.floor(this.random() * Math.max(1, this.items.length - 1)); return (this.index + offset) % this.items.length; } /** @assertion-exception Previous-index calculation is bounded by configured item count. */ previousIndex(): number { if (!this.randomOrder) return (this.index + this.items.length - 1) % this.items.length; const span = Math.max(1, Math.ceil(this.items.length / 3)); const offset = 1 + Math.floor(this.random() * span); return (this.index + this.items.length - offset) % this.items.length; } /** @assertion-exception Direction callback relies on configure lifecycle and bounded item list. */ previous(): void { if (this.items.length < 2) return; this.index = this.previousIndex(); const value = this.items[this.index]; if (value) this.changeCallback(value); } /** @assertion-exception Timer callback relies on configure/start lifecycle checks. */ tick(): void { if (this.items.length < 2) return; this.index = this.nextIndex(); const value = this.items[this.index]; if (value) this.changeCallback(value); } stop(): MediaResult<ReturnType<SlideshowController['snapshot']>> { if (this.intervalId !== null) this.scheduler.clearInterval(this.intervalId); this.intervalId = null; const intervalEmpty = this.intervalId === null; const indexBounded = this.items.length === 0 || this.index < this.items.length; const intervalInvariant = verifyInvariant( intervalEmpty, 'slideshow.stop.interval', SLIDESHOW_CONTEXT, ); const indexInvariant = verifyInvariant( indexBounded, 'slideshow.stop.index', SLIDESHOW_CONTEXT, ); if (!intervalInvariant.ok) return err(intervalInvariant.error); return indexInvariant.ok ? ok(this.snapshot()) : err(indexInvariant.error); } dispose(): MediaResult<void> { const stopped = this.stop(); if (!stopped.ok) return stopped; this.items = Object.freeze([]); this.intervalMs = 0; this.index = 0; const itemsEmpty = this.items.length === 0; const intervalEmpty = this.intervalId === null; const itemsInvariant = verifyInvariant( itemsEmpty, 'slideshow.dispose.items', SLIDESHOW_CONTEXT, ); const intervalInvariant = verifyInvariant( intervalEmpty, 'slideshow.dispose.interval', SLIDESHOW_CONTEXT, ); if (!itemsInvariant.ok) return err(itemsInvariant.error); return intervalInvariant.ok ? ok(undefined) : err(intervalInvariant.error); } /** @assertion-exception Pure immutable diagnostic snapshot owns no lifecycle transition. */ snapshot() { return Object.freeze({ itemCount: this.items.length, intervalMs: this.intervalMs, randomOrder: this.randomOrder, index: this.index, running: this.intervalId !== null, }); } }