Перейти к основному содержимому

Signal

Описание

Модуль Signal обеспечивает механизм событийного взаимодействия между различными частями приложения. Позволяет декуплировать отправителей и получателей событий, поддерживает как синхронные, так и асинхронные слушатели, обеспечивая завершение всех асинхронных операций до окончания вызова dispatch.

Назначение

  • Решаемые проблемы:
    • Упрощает передачу данных и событий между компонентами без жёсткой связи.
    • Гарантированная обработка асинхронных операций перед завершением рассылки события.
    • Возможность одноразовой подписки на событие.
  • Ключевые кейсы использования:
    • Внутренний шина событий в мелкомодульных системах.
    • Передача уведомлений и команд между UI-компонентами.
    • Организация одноразовых хуков (одиночных реакций на событие).
  • Преимущества и ограничения:
      • Поддержка async/await для слушателей.
      • Гарантированное ожидание всех промисов.
      • Простая отписка через Disposable.
    • – Нет встроенной обработки ошибок слушателей (исключения не перехватываются).
    • – Отсутствует приоритетизация или категория событий.

Архитектура

  • Основные классы и интерфейсы:
    • Signal<T> — главный класс для создания и работы с сигналом.
    • ISignal<T> — интерфейс с методами subscribe, once, unsubscribe, dispatch.
    • Disposable — интерфейс для управления жизненным циклом подписки.
    • Utils — вспомогательные утилиты (например, генерация UUID).
  • Взаимодействие с другими модулями:
    • Использует Utils.uuid() для генерации уникального идентификатора.
    • Имплементирует ISignal из модуля моделей (./models).

Использование

Основные сценарии

  • Подписка на события:
    const sig = new Signal<number>('MyEvent');
    const disposable = sig.subscribe(data => console.log(data));
  • Одноразовая подписка:
    sig.once(async data => { /* ... */ });
  • Диспатч события:
    await sig.dispatch(42);
  • Отписка:
    disposable.dispose();
  • Лучшие практики:
    • Всегда сохранять Disposable для своевременной отписки.
    • Не создавать бесконечные цепочки асинхронных слушателей без контроля ошибок.

Примеры

Простые примеры

const signal = new Signal<string>('Greeting');
signal.subscribe(msg => console.log('Получено:', msg));
signal.dispatch('Привет!');

Продвинутые примеры

const signal = new Signal<{ id: number, text: string }>('Message');
// Асинхронная подписка и одноразовая подписка
signal.subscribe(async payload => {
await saveToDB(payload);
});
signal.once(({ id }) => console.log(`Первое сообщение с id=${id}`));

await signal.dispatch({ id: 1, text: 'Hello' });
// После этого одноразовый слушатель отписан
await signal.dispatch({ id: 2, text: 'World' });

API

Классы и интерфейсы

  • Signal<T>

    • constructor(name?: string) — создаёт сигнал с именем (по умолчанию "Signal").
    • get name(): string | undefined — возвращает имя сигнала.
    • get uuid(): string — уникальный идентификатор сигнала.
    • subscribe(callback): Disposable — подписаться на событие.
    • once(callback): Disposable — одноразовая подписка.
    • unsubscribe(callback): void — отписка по функции.
    • dispatch(data): Promise<void> — отправка события и ожидание всех слушателей.
  • Disposable

    • dispose(): void — метод для отписки.
  • ISignal<T>

    • Интерфейс, описывающий сигнатуру всех публичных методов Signal<T>.

Конфигурация

  • Параметры конструктора:
    • name (string) — пользовательское имя сигнала.

Интеграция с ECS

Основное применение сигналов в фреймворке - это связывание Групп Систем с определёнными событиями через SignalsController. Это позволяет запускать выполнение систем в нужные моменты.

Пример использования с SignalsController

import { SignalsController } from '@shared/signals';
import { OnUpdateSignal } from '@flow/lifecycle';

// Создаём группы систем
class PhysicsGroup extends SystemGroup {
// ...
}

class RenderGroup extends SystemGroup {
// ...
}

// Создаём конфигурацию связей
const signalBindings = [
{
signal: OnUpdateSignal, // Сигнал жизненного цикла
groups: [PhysicsGroup] // Группы для выполнения
},
{
signal: OnRenderSignal,
groups: [RenderGroup]
}
];

// Регистрируем связи в контроллере
const signalsController = new SignalsController(executionController);
signalsController.listen(signalBindings);

// Теперь при отправке сигнала будут выполняться привязанные группы
await OnUpdateSignal.dispatch({ deltaTime: 16.67 });
// Выполнятся все системы в PhysicsGroup

Особенности работы с SignalsController

  • Порядок выполнения:

    • Группы выполняются в порядке их регистрации в конфигурации
    • Системы внутри группы выполняются согласно их приоритету
  • Асинхронность:

    • Все системы в группе выполняются асинхронно
    • SignalsController дожидается завершения всех систем
  • Управление выполнением:

    • Можно приостанавливать и возобновлять выполнение групп
    • Поддерживается отвязка групп от сигналов

Встроенные сигналы

Фреймворк предоставляет набор встроенных сигналов для типовых событий:

  • OnStartSignal - запуск приложения
  • OnUpdateSignal - обновление каждый кадр
  • OnPauseSignal - приостановка
  • OnResumeSignal - возобновление
  • OnStopSignal - остановка

Частые вопросы

  • Что происходит при ошибке в слушателе? Исключения в синхронных слушателях приведут к падению dispatch, асинхронные — к отклонению промиса.
  • Как отписаться от одноразового слушателя? После первого вызова слушатель автоматически удаляется, но можно вызвать dispose().
  • Можно ли использовать один и тот же callback несколько раз? Да, каждый вызов subscribe создаёт новую запись в списке слушателей.