# Внешние события

Внешние события позволяют передать результат фоновой работы текущему состоянию
Источник `SmEventSource<E>` не знает о сессии, окне приложения и контроллере представления
Вызов `publish(event)` сообщает о событии подпискам, а `StateManager` ставит обработку в очередь обычных задач

## Источник и подписка

Источник создаётся в прикладном модуле и передаётся первым аргументом в `subscribeEvents`
Например, в EAM источник объявлен так:

```kotlin
object EamNotificationEvents {
    val received = SmEventSource<EamPushBatch>()
}
```

Фоновая служба сообщает о полученных уведомлениях через `EamNotificationEvents.received.publish(batch)`
Публиковать события можно с любого потока

Контроллер представления объявляет подписку один раз в `onInit` через `getSts()`:

```kotlin
override fun onInit(state: HomeState) {
    getSts().subscribeEvents(EamNotificationEvents.received, "receiveHomeNotifications") { current, batch ->
        current.dbs.getPkg(EamNotificationsPkg::class).acceptBatch(current.notificationOwner, batch)
    }.onHandled {
        // Необязательная реакция интерфейса после успешной обработки
        // Здесь можно поставить следующую задачу или переход через getSts()
    }
}
```

Обработчик получает текущее состояние и данные события
В нём выполняются запросы и изменения БД через сессию состояния
Обновление интерфейса обычно происходит в `afterEnter` контроллера, поэтому `onHandled` не обязателен

## Порядок обработки

- Обработчик выполняется как `postSharedTask`, без изменения стека состояний
- БД и файлы меняются на рабочем потоке `StateManager`, фиксацией и откатом владеет `StateEventProcessor`
- `afterEnter` и `afterEnterGui` выполняются по правилам обычной задачи
- `onHandled` вызывается на главном потоке после успешного завершения задачи, обновления контроллера и фиксации изменений
  Он не предназначен для работы с БД
- Несколько событий ставят несколько задач в общую очередь, обработчики не выполняются параллельно
  Можно объявить несколько подписок на разные источники и задать каждой свой обработчик

## Жизненный цикл подписки

События принимает только текущее состояние открытого `StateManager`, когда окно находится в состоянии `RESUMED`
Состояния под ним не реагируют, в сценарных тестах без интерфейса окно не требуется
Перед выполнением задачи и вызовом `onHandled` проверяется, что подписка ещё открыта и принадлежит текущему состоянию
Устаревшие задачи отсеиваются

Подписка принадлежит состоянию: при возврате назад она снова работает, при закрытии состояния удаляется
`subscribeEvents` возвращает `SmEventSubscription<E>`, её `close()` позволяет отписаться раньше
При восстановлении из резервной копии прежние подписки закрываются, новый контроллер объявляет их в своём `onInit`

Не регистрируйте подписку в `afterEnter`: этот метод повторяется после каждой задачи
Приложению не нужно вручную отписываться и подписываться при каждом входе на экран

## Что источник не гарантирует

Источник не сохраняет события для повторной доставки
События без активного подписчика, в фоне, после закрытия `StateManager` или во время блокирующего перехода могут быть пропущены
Если результат нельзя потерять, он должен оставаться у источника данных и запрашиваться при возвращении
Например, EAM повторно получает неподтверждённые уведомления с сервера

`StateManager` проверяет принадлежность подписки состоянию, но не принадлежность данных учётной записи
Проверку пользователя выполняет прикладной обработчик, в EAM за неё отвечает `EamNotificationsPkg.acceptBatch`

## Фоновая работа и ресурсы экрана

[Фоновая служба](095_Фоновая_служба.md) использует собственную сессию
Передавать её в обработчик подписки или напрямую вызывать обработчик на потоке службы нельзя

Объекты периодической проверки экрана принадлежат контроллеру, а не представлению Compose
В `onInit` ресурс привязывается к состоянию через `state.registerClosable(resource)`
`StateManager` закрывает его при удалении состояния, восстановлении из резервной копии и завершении работы
Жизненный цикл окна доступен через `activityLifecycle`

См также: [состояние](040_Состояние.md), [контроллер представления](050_Контроллер_представления.md), [сессия](060_Сессия.md)
