/** * Player-scoped ads controller wiring. */ import { noopLogger, type AviationLogger } from '../logger'; import type { AviationStore } from '../store'; import type { AdsController } from '../specs/AdsController.nitro'; interface AdsWiring { setController(controller: AdsController): void; clearController(): void; getController(): AdsController | undefined; releaseController(): void; resetController(): void; } export function createAdsWiring( store: AviationStore, log: AviationLogger = noopLogger ): AdsWiring { let adsController: AdsController | null = null; let adsControllerGeneration = 0; function nextGeneration(): number { adsControllerGeneration++; return adsControllerGeneration; } function isCurrent(controller: AdsController, generation: number): boolean { return ( adsController === controller && adsControllerGeneration === generation ); } function releaseController(): void { const controller = adsController; nextGeneration(); adsController = null; if (controller) { try { controller.release(); } catch (e) { log.warn('teardown', 'ads release failed', { error: e }); } } } function resetController(): void { nextGeneration(); adsController = null; store.clearAdState(); } return { setController(controller): void { if (adsController && adsController !== controller) { releaseController(); } adsController = controller; const generation = nextGeneration(); controller.onAdBreakStarted((info) => { if (!isCurrent(controller, generation)) return; log.debug('ads', 'ad break started', { breakIndex: info.breakIndex }); store.setAdPlaying(info); }); controller.onAdBreakEnded((_info) => { if (!isCurrent(controller, generation)) return; log.debug('ads', 'ad break ended'); store.setAdBreakEnded(); }); controller.onAdStarted((info) => { if (!isCurrent(controller, generation)) return; log.debug('ads', 'ad started'); store.setCurrentAd(info); }); controller.onAdCompleted((_info) => { if (!isCurrent(controller, generation)) return; log.debug('ads', 'ad completed'); store.setCurrentAd(undefined); }); controller.onAdProgress((info) => { if (!isCurrent(controller, generation)) return; store.setCurrentAd(info); }); controller.onAdSkipped((_info) => { if (!isCurrent(controller, generation)) return; store.setCurrentAd(undefined); }); controller.onAllAdsCompleted(() => { if (!isCurrent(controller, generation)) return; store.clearAdState(); }); controller.onAdError((_adError) => { if (!isCurrent(controller, generation)) return; log.warn('ads', 'ad error'); store.clearAdState(); }); }, clearController(): void { releaseController(); resetController(); }, getController(): AdsController | undefined { return adsController ?? undefined; }, releaseController, resetController, }; }