# Middleware и Events

> Расширяйте Transport, Endpoint, Service и модули совместной работы универсальными Middleware и Events.

- Human documentation: [https://docs.univer.ai/ru-RU/server/collaboration/middleware-and-events](https://docs.univer.ai/ru-RU/server/collaboration/middleware-and-events)

- Agent Markdown: [https://docs.univer.ai/ru-RU/server/collaboration/middleware-and-events.md](https://docs.univer.ai/ru-RU/server/collaboration/middleware-and-events.md)

- Requested language: `ru-RU`

- Content language: `ru-RU`

- Documentation version: `1.2.0`

- Source: [collaboration/middleware-and-events.ru-RU.mdx](https://github.com/dream-num/documentation/blob/dev/content/server/collaboration/middleware-and-events.ru-RU.mdx)

---

Middleware и Events — общие точки расширения Collaboration SDK. Идентификация и авторизация подключаются через Middleware. Проверки и ограничения до операции выполняйте в Middleware, запись изменений и последующие действия после подтверждения состояния — через Events. Оба механизма поддерживают журналирование, трассировку, метрики и обновление производного состояния.

## Два момента расширения

|                                      | Middleware                                                | Event                                                           |
| ------------------------------------ | --------------------------------------------------------- | --------------------------------------------------------------- |
| Выполнение                           | Во время операции                                         | После подтверждения изменения состояния                         |
| Участие в операции или её отклонение | Да                                                        | Нет                                                             |
| Дополнение Context вызова            | Да                                                        | Читает Context и результат из Event                             |
| Ошибка обработчика                   | Может завершить текущую операцию ошибкой                  | Изолирована от уже определённого доменного результата           |
| Типичные задачи                      | Идентификация, авторизация, проверка, логи, замер времени | Производные индексы, кеши, метрики, уведомления внутри процесса |

Используйте Middleware, чтобы решить, можно ли продолжить операцию, и Event, чтобы наблюдать уже произошедшее изменение.

## Модель выполнения Middleware

Middleware одной Action образуют асинхронную цепочку в порядке регистрации. `await next()` входит в следующий Middleware; операция SDK выполняется в самой глубокой точке:

```ts
service.use("submitChangeset", async (context, next) => {
  const startedAt = performance.now();
  context.customData.startedAt = startedAt;

  try {
    await next();
  } finally {
    metrics.observe("collaboration.submit.duration", performance.now() - startedAt);
  }
});
```

Middleware может:

* читать запрос конкретной Action;
* делиться локальными данными вызова через `customData`;
* проверять вход или опрашивать системы приложения до `next()`;
* записывать длительность и результаты после возврата из `next()`;
* выбрасывать `CollabError` для ожидаемого отказа в совместной операции.

HTTP Middleware Transport может закончить ответ без `next()`. Middleware Service и Endpoint должны отклонять операцию явной ошибкой, а не молча пропускать `next()`.

## Middleware Transport

`transport.use()` регистрирует Middleware для обычных HTTP-запросов; они выполняются в порядке регистрации. Вызовите `await next()`, чтобы продолжить обработку, или завершите ответ напрямую. `transport.useUpgrade()` обрабатывает рукопожатия WebSocket.

Например, сначала измерьте длительность HTTP-запроса, затем выполните аутентификацию и задайте доверенный `userID`, после чего зарегистрируйте Collaboration Endpoint:

```ts
transport.use(async (context, next) => {
  const start = performance.now();
  context.response.once("finish", () => {
    recordHttpDuration({
      method: context.incomingMessage.method,
      route: context.route?.path ?? "unmatched",
      status: context.response.statusCode,
      durationMs: performance.now() - start,
    });
  });
  await next();
});

transport.use(async (context, next) => {
  const user = await authenticate(context.incomingMessage);
  if (!user) {
    context.response.statusCode = 401;
    context.response.end("Authentication required");
    return;
  }
  context.userID = user.userId;
  await next();
});

transport.register(collaborationEndpoint);
```

Приложение предоставляет `recordHttpDuration` и `authenticate`. Endpoint использует `userID`, заданный предыдущими Middleware. Middleware наблюдения регистрируется до аутентификации, поэтому отказы также учитываются.

`context.route?.path` — полный шаблон совпавшего маршрута, доступный, когда выполнение достигает Endpoint. Событие `finish` ответа позволяет измерить время до его завершения; отказы до сопоставления маршрута и неизвестные пути записываются как `unmatched`.

## Middleware Endpoint

Участвуют в операциях Session реального времени и Presence:

| Action            | Момент                                                    | Типичные задачи                              |
| ----------------- | --------------------------------------------------------- | -------------------------------------------- |
| `connect`         | После погашения билета и создания Session, до HELLO       | Политика подключения, имя и аватар участника |
| `joinUnit`        | До входа Session в Room Unit                              | Допуск в Room и видимость Unit               |
| `receivePresence` | После получения Presence от вошедшей Session, до рассылки | Проверка и фильтрация Presence               |
| `sendPresence`    | До отправки Presence каждому участнику                    | Фильтрация по получателю                     |

```ts
endpoint.use("receivePresence", async (context, next) => {
  if (JSON.stringify(context.payload).length > 8_000) {
    throw new CollabError("INVALID_REQUEST", "Presence payload is too large");
  }
  await next();
});
```

`session.userID` и `session.customData` Endpoint приходят из HTTP-запроса, выдавшего Session Ticket. Endpoint создаёт `memberID` для текущей Session WebSocket; после переподключения он меняется.

## Middleware Service

Участвуют в жизненном цикле эталонных данных совместной работы:

| Action            | Граница                                                       | Типичные задачи                                    |
| ----------------- | ------------------------------------------------------------- | -------------------------------------------------- |
| `readUnitData`    | До чтения снимков, блоков или изменений                       | Политика чтения, проверки арендатора, аудит        |
| `createUnit`      | До атомарного создания Unit                                   | Проверка типов, политика создания, бизнес-правила  |
| `deleteUnits`     | До записи пакета удаления                                     | Политика удаления и проверки каждой Unit           |
| `recoverUnits`    | До записи пакета восстановления                               | Политика восстановления и аудит                    |
| `submitChangeset` | До идемпотентности и OT логической отправки                   | Политика редактирования, общий размер, трассировка |
| `applyChangeset`  | До применения преобразованных изменений к состоянию-кандидату | Проверка на уровне мутаций                         |
| `commitChangeset` | До CAS ревизии и коммита Adapter                              | Финальные правила ревизии и метрики                |

```ts
service.use("createUnit", async (context, next) => {
  await creationPolicy.assertAllowed({
    userID: context.userID,
    unitID: context.request.snapshot.unitID,
    type: context.request.snapshot.type,
  });
  context.customData.creationPolicyChecked = true;
  await next();
});
```

`submitChangeset` выполняется один раз для логической отправки. Конкуренция ревизий может повторно запустить `applyChangeset` и `commitChangeset` с новым `attempt`. Эти Middleware должны быть безопасны при повторе и не должны напрямую отправлять необратимые внешние сообщения или выполнять однократные списания средств.

## Область customData

`customData` — доступный приложению для записи объект текущего вызова, подходящий для:

* ID арендатора, трассировки и логгеров запроса;
* повторно используемых результатов ACL в вызове;
* начала замера времени и меток аудита;
* бизнес-объектов одной цепочки Middleware.

Обычные HTTP-запросы, вызовы Service и Sessions WebSocket имеют разные жизненные циклы. `customData` не сохраняется, не отправляется браузеру и не журналируется автоматически. Долговременные данные остаются в хранилище приложения.

## Модель выполнения Events

Services регистрируют локальные для процесса обработчики через `on()`, возвращающий освобождаемую подписку:

```ts
const subscription = service.on("changesetCommitted", async (event) => {
  metrics.increment("collaboration.changeset.committed", {
    unitType: String(event.changeset.type),
  });
  localCache.delete(event.changeset.unitID);
});

subscription.dispose();
```

Обработчики одного Event выполняются последовательно в порядке регистрации с ожиданием завершения. Ошибки записываются и изолируются от уже определённого доменного результата. Например, сбой обработчика `changesetCommitted` не откатывает изменения, уже сохранённые Adapter.

Events подходят для локальных, восстанавливаемых или некритичных последующих действий. Они не разделяют транзакционную границу коммита базы и не гарантируют доставку внешней системе.

## Events основного Service

| Event                | Подтверждённый факт                        | Типичное применение                            |
| -------------------- | ------------------------------------------ | ---------------------------------------------- |
| `unitCreated`        | Unit и начальные данные созданы            | Локальный кеш каталога или производный индекс  |
| `changesetCommitted` | Изменения подтверждены, ревизия продвинута | Рассылка, метрики, производные индексы         |
| `unitsDeleted`       | Adapter подтвердил пакет удаления          | Очистка связанного состояния процесса          |
| `unitsRecovered`     | Adapter подтвердил пакет восстановления    | Восстановление производного состояния процесса |

Endpoint слушает `changesetCommitted` того же Service и рассылает подтверждённые изменения клиентам, вошедшим в Unit в текущем процессе, а при настроенном pub/sub-адаптере — на всех узлах. Он также предоставляет `memberLeftUnit` для явного выхода, разрыва соединения и освобождения Endpoint.

## Независимые точки расширения модулей

History, Thread Comment и Worktree не наследуют Middleware и Events основного Collaboration Service:

* History предоставляет Middleware чтения для `getHistoryList`, `listHistoryCreators` и `getHistoryChangesets`;
* Comment — добавления, ответа, правки, удаления, решения и списка, а также `commentCommitted`;
* Worktree — жизненного цикла и read/submit/commit черновиков, а также Events создания, статуса, слияния и коммита черновика.

Устанавливайте правила отдельно для каждого включённого модуля. Логи, проверки арендатора и авторизация Middleware ядра не распространяются на них автоматически.

Продолжите с [Идентификацией и авторизацией](https://docs.univer.ai/ru-RU/server/collaboration/identity-and-authorization.md): там показано сопоставление существующих пользователей и ACL этим общим границам.
