UNPKG

rock-mod

Version:

Rock-Mod is a powerful framework designed for creating and managing mods for Grand Theft Auto (GTA) games.

134 lines (133 loc) 6.09 kB
const RENDER_EVENT = "rm::render"; const RENDER_LISTENER_WARN_THRESHOLD_MS = 10; function getCCMPCallbackProfiler() { return globalThis; } function profileLabel(event) { return `rm internal "${event}"`; } function captureRegistrationSite() { var _a; const stack = String((_a = new Error().stack) !== null && _a !== void 0 ? _a : ""); const lines = stack .split("\n") .map((line) => line.trim()) .filter(Boolean); for (const line of lines) { if (line === "Error" || line.includes("captureRegistrationSite") || line.includes("CCMPInProcessEmitter") || line.includes("getCCMPCallbackProfiler") || line.includes("profileLabel")) { continue; } return line.replace(/^at\s+/, ""); } return "<unknown>"; } /** * Тонкий внутренний event bus. * * RageMP даёт `mp.events.call(name, ...args)` для локальной шины поверх своего * движка событий. У CCMP нативной внутренней шины нет — её роль выполняет этот * класс. Используется только из `CCMPEventsManager` для `onInternal` / * `offInternal` / `emitInternal` и из `CCMPEventsBridge` для синтезированных * `rm::*` событий. * * Поддерживает "sticky" events (см. `emitSticky`) для one-shot lifecycle- * сигналов (например `rm::playerReady`), которые могут эмититься до того, * как поздние подписчики из DI-цепочки геймода успели зарегистрироваться. * Sticky-кэш гарантирует, что любой `on()`-вызов после `emitSticky` получит * последнее значение synchronously. */ export class CCMPInProcessEmitter { constructor() { this._listeners = new Map(); /** Кэш аргументов для sticky-events. См. `emitSticky` / `on`. */ this._stickyCache = new Map(); } on(event, listener) { var _a, _b; let bucket = this._listeners.get(event); if (!bucket) { bucket = new Set(); this._listeners.set(event, bucket); } bucket.add(listener); (_b = (_a = getCCMPCallbackProfiler()).__ccmp_registerCallbackProfile) === null || _b === void 0 ? void 0 : _b.call(_a, profileLabel(event), listener, captureRegistrationSite()); // Sticky replay: если событие уже было эмитнуто как sticky — сразу // воспроизводим последнее значение новому подписчику. Это решает гонку // "CCMPEventsBridge эмитит rm::playerReady до того, как геймод-адаптер // успел подписаться" — типично для async DI-bootstrap'а. const sticky = this._stickyCache.get(event); if (sticky !== undefined) { try { this._callListener(event, listener, sticky); } catch (error) { console.error(`[CCMPInProcessEmitter] sticky replay "${event}" failed for new subscriber:`, error); } } } off(event, listener) { const bucket = this._listeners.get(event); if (!bucket) { return; } if (listener) { bucket.delete(listener); if (bucket.size === 0) { this._listeners.delete(event); } return; } this._listeners.delete(event); } emit(event, ...args) { const bucket = this._listeners.get(event); if (!bucket || bucket.size === 0) { return; } // Копия на случай, если хендлер изменит реестр во время итерации. for (const listener of [...bucket]) { try { this._callListener(event, listener, args); } catch (error) { console.error(`[CCMPInProcessEmitter] listener "${event}" failed:`, error); } } } /** * Как `emit`, но дополнительно кэширует args, чтобы любой будущий * `on(event, listener)` сразу получил воспроизведение последнего значения. * * Использовать только для one-shot lifecycle-событий (`rm::playerReady` и * т.п.), где "позднее" подписавшийся обработчик всё равно должен получить * сигнал. Для регулярных событий (`rm::playerConnected` на каждого нового * игрока) sticky-кэш приведёт к нежелательному поведению — используйте * обычный `emit`. * * Кэш переписывается на каждый последующий вызов с тем же event'ом, так * что подписчики получат последнее значение, не цепочку историй. */ emitSticky(event, ...args) { this._stickyCache.set(event, args); this.emit(event, ...args); } /** * Удаляет sticky-кэш для события. Полезно на disconnect/reset, чтобы * новые подписчики не получили устаревший local-player-stub. */ clearSticky(event) { this._stickyCache.delete(event); } _callListener(event, listener, args) { const profiler = getCCMPCallbackProfiler().__ccmp_profileCallback; if (profiler) { profiler(profileLabel(event), listener, args, event === RENDER_EVENT ? RENDER_LISTENER_WARN_THRESHOLD_MS : undefined); return; } listener(...args); } }