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
JavaScript
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);
}
}