[perf][server] Низковисящие оптимизации бэка: триграммные индексы для саджестов, 4 лишних DB-запроса на каждый API-вызов, ре-эмбеддинг без дедупа, CT… #348

Open
opened 2026-07-04 20:07:48 +03:00 by agent_vscode · 0 comments
Collaborator

Суть

Серверная пара к клиентскому перф-комплекту (#342/#343/#344/#346, комментарии — #340). Бэкенд в форке уже сильно оптимизирован (персист Y.Doc дебаунсится 10–45 с, history коалесцируется через jobId+delay, FTS на GIN, пагинация без COUNT(*), breadcrumbs одним рекурсивным CTE, CASL-права в Redis-кэше, broadcast'ы скоупены) — но осталась россыпь дешёвых точечных фиксов, каждый из которых бьёт по горячему пути:

  • саджест-поиск (спотлайт, пикеры ссылок/меншенов) делает seq scan на каждое нажатие клавиши;
  • каждый авторизованный API-вызов тратит 4 DB-запроса ещё до хендлера, из них два — за одним и тем же workspace-row;
  • каждое сохранение документа ставит в очередь полный ре-эмбеддинг страницы без дедупликации при воркере с concurrency 1;
  • проверка прав гоняет рекурсивный CTE по предкам на 5 горячих эндпоинтах, даже когда ограниченных страниц в воркспейсе ноль.

Суммарно — одна миграция и пара десятков строк кода, без изменения поведения.

Диагноз (по соотношению эффект/усилия)

1. Саджесты: leading-wildcard LIKE без триграммного индекса

search.service.ts (/search/suggest): LOWER(f_unaccent(pages.title)) LIKE LOWER(f_unaccent('%q%')) — и то же для users.name, groups.name. Leading % по функции ⇒ btree бесполезен ⇒ скан страниц воркспейса на каждый keystroke в спотлайте и пикерах. Расширения pg_trgm + unaccent уже установлены (миграция 20250729T213756), но gin_trgm_ops-индексов в миграциях нет ни одного (проверено grep'ом).

2. Горячий путь auth: 4 некэшированных DB-запроса до хендлера

На каждый запрос: domain.middleware.ts грузит workspace (findFirst() self-hosted / findByHostname() cloud) → jwt.strategy.ts грузит тот же workspace повторно (workspaceRepo.findById), затем юзера (userRepo.findById) и сессию (userSessionRepo.findActiveById). Совпадение workspace уже гарантировано проверкой req.raw.workspaceId === payload.workspaceId в начале validate(). CASL-слой при этом уже закэширован (getUserSpaceRoles, canUserEditPagewithCache, 5 с) — некэшированной осталась только identity-часть.

3. AI-очередь: ре-эмбеддинг на каждое сохранение без jobId/delay

persistence.extension.ts#L437-L442: aiQueue.add(QueueJob.PAGE_CONTENT_UPDATED, {pageIds:[page.id]…}) без опций. Консьюмер (embedding-indexer.service.ts → reindexPage) на каждый джоб re-chunk'ает всю страницу, зовёт внешний embedding-API по всем чанкам и переписывает все строки page_embeddings в транзакции — при concurrency 1. Активная правка = очередь копится за латентностью внешнего API, давя на Redis/БД. Правильный паттерн ({jobId, delay} + возрастной дебаунс) уже реализован в enqueuePageHistory в этом же файле.

4. filterAccessiblePageIds: рекурсивный CTE без workspace-level short-circuit

page-permission.repo.ts#L656-L731: short-circuit hasRestrictedPagesInSpace срабатывает только при переданном spaceId. Без него — полный рекурсивный обход предков + anti-join на page_access всегда. Горячие вызовы без spaceId: favorites (грузится при старте приложения, favorite.service.ts), notifications, recent по всем спейсам, created-by, глобальный поиск (search.service.ts). В типичном воркспейсе ограниченных страниц ноль — вся работа впустую.

5. Пакет мелочи (по строчке-две)

  • Trash тянет полный content: page.repo.ts#L584 getDeletedPagesInSpace.select('content'), UI рендерит только title/icon/deletedBy.
  • Transclusion-синк: 3 последовательных SELECT на каждое сохранение даже на страницах без трансклюзий (persistence.extension.ts#L406syncTransclusion).
  • Mention-нотификации: enqueue при наличии меншенов, а не при их измененииoldMentionedUserIds уже вычисляется рядом (persistence.extension.ts#L414-L435).
  • Комментарии: страница грузится дважды на открытие панели — comment.controller.ts (findById) и comment.service.ts (findById повторно).
  • page_history: ORDER BY id DESC при индексе (page_id, created_at DESC) — лишний sort; выровнять (индекс (page_id, id DESC) либо сортировка по created_at).
  • comments: WHERE page_id ORDER BY id при индексе только (page_id) — композитный (page_id, id).
  • Share-страница: getShareForPage + hasRestrictedAncestor — два рекурсивных CTE по предкам подряд + 3 независимых await'а настроек последовательно (share.service.ts, share.controller.ts) — Promise.all + слить обходы.
  • hasRestrictedPagesInSpace — единственная проверка прав без withCache (page.service.ts#L423).
  • ValidationPipe { transform: true } глобально (main.ts): class-transformer деп-клонирует многомегабайтный content на каждом create/update/import страницы.
  • Конфиг/гигиена: нет statement_timeout у пула (database.module.ts); maintainLock в redis-sync.extension.ts#L222-L231 ставит interval без clearInterval предыдущего (утечка при reload-without-unload).

Проверено и уже правильно (не трогать): индексы дерева/комментариев/избранного/нотификаций (20260329T163516-add-new-indexes); list-эндпоинты content не отдают (кроме trash выше); N+1 по breadcrumbs/членствам нет; per-keystroke путь collab чист; history-очередь — эталон коалесинга; removeOnComplete у очередей ограничен; логирование без сериализации тел; основной /search — индексированный tsv @@ c ts_headline только по ≤25 строкам после LIMIT.

Границы изменения

Только apps/server: одна миграция (индексы), auth-путь (middleware + strategy), persistence.extension (опции enqueue + short-circuit'ы), пара repo/service-методов, main.ts (pipe), database.module.ts (конфиг пула). Поведение API 1:1; кэши — с короткими TTL и явной инвалидацией. Схема данных не меняется (только индексы).

Решение

1. Миграция с триграммными индексами

// Expressions MUST be byte-identical to the query predicates in search.service.ts
await sql`CREATE INDEX IF NOT EXISTS idx_pages_title_trgm
  ON pages USING gin ((LOWER(f_unaccent(title))) gin_trgm_ops)`.execute(db);
await sql`CREATE INDEX IF NOT EXISTS idx_users_name_trgm
  ON users USING gin ((LOWER(f_unaccent(name))) gin_trgm_ops)`.execute(db);
await sql`CREATE INDEX IF NOT EXISTS idx_groups_name_trgm
  ON groups USING gin ((LOWER(f_unaccent(name))) gin_trgm_ops)`.execute(db);

Плюс индексы из «мелочи»: page_history (page_id, id DESC) (или смена сортировки), comments (page_id, id).

2. Auth-путь: убрать дубль + withCache

  • В jwt.strategy.ts переиспользовать req.raw.workspace, когда req.raw.workspaceId === payload.workspaceId (равенство уже проверено выше по коду), fallback на запрос.
  • domain.middleware.ts: обернуть lookup в существующий withCache (ключ — константа для self-hosted / workspace:byhost:<subdomain> для cloud, TTL 10–30 с) с инвалидацией на обновление workspace.
  • Юзер: withCache user:<wsId>:<userId>, TTL 5–15 с, явный del при disable/logout/смене роли.
  • Сессия: кэш по sessionId, TTL ~5 с, явный del в revoke/logout — отзыв мгновенный, TTL лишь страховка. trackActivity уже throttled — не трогать.

Итог: тёплый авторизованный запрос — 0 pre-handler DB-запросов вместо 4.

3. Дедуп AI-очереди (одна строка)

// Coalesce re-embeds the same way enqueuePageHistory does: repeated stores
// within the window collapse into one job; the processor reads the CURRENT
// page state at run time, so the last content always wins.
await this.aiQueue.add(
  QueueJob.PAGE_CONTENT_UPDATED,
  { pageIds: [page.id], workspaceId: page.workspaceId },
  { jobId: page.id, delay: 30_000 },
);

4. Workspace-level short-circuit прав

Кэшируемый hasRestrictedPagesInWorkspace(workspaceId) (EXISTS по page_access.workspace_id, зеркально per-space версии, withCache с тем же PERMISSION_CACHE_TTL_MS ≈ 5 с — самозаживающий, отдельная инвалидация не обязательна) + short-circuit в filterAccessiblePageIds, когда spaceId не передан.

5. Пакет мелочи

По списку из диагноза: удалить .select('content') в trash; short-circuit syncTransclusion при отсутствии transclusion/reference-узлов и пустых существующих записях; дифф меншенов перед enqueue; прокинуть загруженную страницу из comment.controller в service; Promise.all + объединённый обход предков в share; withCache для hasRestrictedPagesInSpace; route-scoped pipe с transform:false для content-несущих DTO (валидация остаётся); statement_timeout (щедрый, ~30 с, чтобы не убить export) в конфиг пула; clearInterval-guard в maintainLock.

Крайние случаи

  • Триграммные выражения должны байт-в-байт совпадать с предикатами запросов (LOWER(f_unaccent(col))), иначе планировщик их не возьмёт — проверить EXPLAIN после миграции. f_unaccent — IMMUTABLE-обёртка, для expression-индекса пригодна. На больших таблицах build индекса при миграции на старте займёт время — приемлемо для self-hosted, но отметить в CHANGELOG.
  • Кэш юзера/сессии — окно безопасности: заблокированный юзер/отозванная сессия живут до TTL. Обязательные явные del в logout/revoke/disable; TTL держать ≤15 с. Не кэшировать негативные результаты.
  • Дедуп AI-джоба: BullMQ игнорирует add с существующим delayed jobId — это и есть коалесинг; корректно, т.к. процессор читает актуальное состояние страницы в момент выполнения. Проверить, что завершённый джоб не блокирует повторный jobId (removeOnComplete: true у AI-очереди уже стоит).
  • hasRestrictedPagesInWorkspace: после выдачи первого page-level ограничения кэш до 5 с может отдавать «нет ограничений» — окно то же, что у существующих permission-кэшей (осознанный трейд-офф форка), поведение консистентно.
  • transform:false для page-DTO: убедиться, что whitelist и валидация остальных полей сохраняются (ручная проверка формы content — лёгкая, без прогонки через class-transformer).
  • statement_timeout: не задеть экспорт больших спейсов (грузит все страницы одним запросом) — либо 30–60 с глобально, либо переопределение в export-сессии.

Тесты / проверка

  • EXPLAIN (ANALYZE) саджест-запроса до/после: Bitmap Index Scan по trgm-индексу вместо Seq Scan.
  • Интеграционный тест auth: тёплый запрос не делает SELECT workspace/user/session (можно через query-лог/спай на репо); logout → следующий запрос с той же сессией = 401 немедленно.
  • Юнит на enqueue: два store подряд в окне 30 с → один AI-джоб; store после обработки → новый джоб.
  • Юнит filterAccessiblePageIds: воркспейс без ограничений → вход == выход без CTE (спай на db); с ограничениями — прежнее поведение.
  • Существующие тесты persistence.extension, comment, share — прогнать; trash-эндпоинт больше не содержит content в ответе.
  • Смок: правка страницы двумя клиентами 2 мин → очередь AI не растёт монотонно (bull метрики/Redis).

Вне скоупа

  • Клиентская часть — комплект #340/#342/#343/#344/#346.
  • Вынос export в очередь с download-ссылкой (заметное усилие, редкая операция) — отдельная задача при необходимости.
  • ts_headline по большим text_content в полном поиске — bounded LIMIT 25, оставляем.
  • Инкрементальные эмбеддинги (re-chunk только изменённых блоков) — отдельная большая задача; здесь только дедуп джобов.

План работ

  1. Миграция: 3 trgm-индекса + page_history(page_id, id DESC) + comments(page_id, id); EXPLAIN-проверка.
  2. Auth-путь: reuse req.raw.workspace + withCache workspace/user/session + инвалидация в logout/disable (п. 2).
  3. {jobId, delay} для PAGE_CONTENT_UPDATED (п. 3).
  4. hasRestrictedPagesInWorkspace + short-circuit (п. 4).
  5. Пакет мелочи (п. 5) — можно дробить по PR.
  6. Тесты + смок с метриками очередей; замер p50/p95 латентности API до/после на слабом инстансе.
# Суть Серверная пара к клиентскому перф-комплекту (#342/#343/#344/#346, комментарии — #340). Бэкенд в форке уже сильно оптимизирован (персист Y.Doc дебаунсится 10–45 с, history коалесцируется через `jobId+delay`, FTS на GIN, пагинация без `COUNT(*)`, breadcrumbs одним рекурсивным CTE, CASL-права в Redis-кэше, broadcast'ы скоупены) — но осталась россыпь дешёвых точечных фиксов, каждый из которых бьёт по горячему пути: - саджест-поиск (спотлайт, пикеры ссылок/меншенов) делает **seq scan на каждое нажатие клавиши**; - **каждый** авторизованный API-вызов тратит 4 DB-запроса ещё до хендлера, из них два — за одним и тем же workspace-row; - каждое сохранение документа ставит в очередь **полный ре-эмбеддинг страницы без дедупликации** при воркере с concurrency 1; - проверка прав гоняет рекурсивный CTE по предкам на 5 горячих эндпоинтах, даже когда ограниченных страниц в воркспейсе ноль. Суммарно — одна миграция и пара десятков строк кода, без изменения поведения. # Диагноз (по соотношению эффект/усилия) ### 1. Саджесты: leading-wildcard LIKE без триграммного индекса [search.service.ts](apps/server/src/core/search/search.service.ts) (`/search/suggest`): `LOWER(f_unaccent(pages.title)) LIKE LOWER(f_unaccent('%q%'))` — и то же для `users.name`, `groups.name`. Leading `%` по функции ⇒ btree бесполезен ⇒ скан страниц воркспейса **на каждый keystroke** в спотлайте и пикерах. Расширения `pg_trgm` + `unaccent` уже установлены (миграция `20250729T213756`), но `gin_trgm_ops`-индексов в миграциях нет ни одного (проверено grep'ом). ### 2. Горячий путь auth: 4 некэшированных DB-запроса до хендлера На каждый запрос: [domain.middleware.ts](apps/server/src/common/middlewares/domain.middleware.ts) грузит workspace (`findFirst()` self-hosted / `findByHostname()` cloud) → [jwt.strategy.ts](apps/server/src/core/auth/strategies/jwt.strategy.ts) грузит **тот же workspace повторно** (`workspaceRepo.findById`), затем юзера (`userRepo.findById`) и сессию (`userSessionRepo.findActiveById`). Совпадение workspace уже гарантировано проверкой `req.raw.workspaceId === payload.workspaceId` в начале `validate()`. CASL-слой при этом уже закэширован (`getUserSpaceRoles`, `canUserEditPage` — `withCache`, 5 с) — некэшированной осталась только identity-часть. ### 3. AI-очередь: ре-эмбеддинг на каждое сохранение без jobId/delay [persistence.extension.ts#L437-L442](apps/server/src/collaboration/extensions/persistence.extension.ts#L437-L442): `aiQueue.add(QueueJob.PAGE_CONTENT_UPDATED, {pageIds:[page.id]…})` **без опций**. Консьюмер (`embedding-indexer.service.ts → reindexPage`) на каждый джоб re-chunk'ает всю страницу, зовёт внешний embedding-API по всем чанкам и переписывает все строки `page_embeddings` в транзакции — при concurrency 1. Активная правка = очередь копится за латентностью внешнего API, давя на Redis/БД. Правильный паттерн (`{jobId, delay}` + возрастной дебаунс) уже реализован в `enqueuePageHistory` **в этом же файле**. ### 4. `filterAccessiblePageIds`: рекурсивный CTE без workspace-level short-circuit [page-permission.repo.ts#L656-L731](apps/server/src/database/repos/page/page-permission.repo.ts#L656-L731): short-circuit `hasRestrictedPagesInSpace` срабатывает только при переданном `spaceId`. Без него — полный рекурсивный обход предков + anti-join на `page_access` всегда. Горячие вызовы без `spaceId`: favorites (грузится при старте приложения, `favorite.service.ts`), notifications, recent по всем спейсам, created-by, глобальный поиск (`search.service.ts`). В типичном воркспейсе ограниченных страниц ноль — вся работа впустую. ### 5. Пакет мелочи (по строчке-две) - **Trash тянет полный `content`**: [page.repo.ts#L584](apps/server/src/database/repos/page/page.repo.ts#L584) `getDeletedPagesInSpace` — `.select('content')`, UI рендерит только title/icon/deletedBy. - **Transclusion-синк**: 3 последовательных SELECT на каждое сохранение даже на страницах без трансклюзий ([persistence.extension.ts#L406](apps/server/src/collaboration/extensions/persistence.extension.ts#L406) → `syncTransclusion`). - **Mention-нотификации**: enqueue при *наличии* меншенов, а не при их *изменении* — `oldMentionedUserIds` уже вычисляется рядом ([persistence.extension.ts#L414-L435](apps/server/src/collaboration/extensions/persistence.extension.ts#L414-L435)). - **Комментарии**: страница грузится дважды на открытие панели — `comment.controller.ts` (`findById`) и `comment.service.ts` (`findById` повторно). - **`page_history`**: `ORDER BY id DESC` при индексе `(page_id, created_at DESC)` — лишний sort; выровнять (индекс `(page_id, id DESC)` либо сортировка по `created_at`). - **`comments`**: `WHERE page_id ORDER BY id` при индексе только `(page_id)` — композитный `(page_id, id)`. - **Share-страница**: `getShareForPage` + `hasRestrictedAncestor` — два рекурсивных CTE по предкам подряд + 3 независимых await'а настроек последовательно ([share.service.ts](apps/server/src/core/share/share.service.ts), [share.controller.ts](apps/server/src/core/share/share.controller.ts)) — `Promise.all` + слить обходы. - **`hasRestrictedPagesInSpace`** — единственная проверка прав без `withCache` ([page.service.ts#L423](apps/server/src/core/page/services/page.service.ts#L423)). - **`ValidationPipe { transform: true }`** глобально ([main.ts](apps/server/src/main.ts)): class-transformer деп-клонирует многомегабайтный `content` на каждом create/update/import страницы. - **Конфиг/гигиена**: нет `statement_timeout` у пула ([database.module.ts](apps/server/src/database/database.module.ts)); `maintainLock` в [redis-sync.extension.ts#L222-L231](apps/server/src/collaboration/extensions/redis-sync.extension.ts#L222-L231) ставит interval без clearInterval предыдущего (утечка при reload-without-unload). **Проверено и уже правильно (не трогать):** индексы дерева/комментариев/избранного/нотификаций (`20260329T163516-add-new-indexes`); list-эндпоинты `content` не отдают (кроме trash выше); N+1 по breadcrumbs/членствам нет; per-keystroke путь collab чист; history-очередь — эталон коалесинга; `removeOnComplete` у очередей ограничен; логирование без сериализации тел; основной `/search` — индексированный `tsv @@` c `ts_headline` только по ≤25 строкам после LIMIT. # Границы изменения Только `apps/server`: одна миграция (индексы), auth-путь (middleware + strategy), persistence.extension (опции enqueue + short-circuit'ы), пара repo/service-методов, main.ts (pipe), database.module.ts (конфиг пула). Поведение API 1:1; кэши — с короткими TTL и явной инвалидацией. Схема данных не меняется (только индексы). # Решение ## 1. Миграция с триграммными индексами ```ts // Expressions MUST be byte-identical to the query predicates in search.service.ts await sql`CREATE INDEX IF NOT EXISTS idx_pages_title_trgm ON pages USING gin ((LOWER(f_unaccent(title))) gin_trgm_ops)`.execute(db); await sql`CREATE INDEX IF NOT EXISTS idx_users_name_trgm ON users USING gin ((LOWER(f_unaccent(name))) gin_trgm_ops)`.execute(db); await sql`CREATE INDEX IF NOT EXISTS idx_groups_name_trgm ON groups USING gin ((LOWER(f_unaccent(name))) gin_trgm_ops)`.execute(db); ``` Плюс индексы из «мелочи»: `page_history (page_id, id DESC)` (или смена сортировки), `comments (page_id, id)`. ## 2. Auth-путь: убрать дубль + `withCache` - В `jwt.strategy.ts` переиспользовать `req.raw.workspace`, когда `req.raw.workspaceId === payload.workspaceId` (равенство уже проверено выше по коду), fallback на запрос. - `domain.middleware.ts`: обернуть lookup в существующий `withCache` (ключ — константа для self-hosted / `workspace:byhost:<subdomain>` для cloud, TTL 10–30 с) с инвалидацией на обновление workspace. - Юзер: `withCache` `user:<wsId>:<userId>`, TTL 5–15 с, **явный `del` при disable/logout/смене роли**. - Сессия: кэш по `sessionId`, TTL ~5 с, **явный `del` в revoke/logout** — отзыв мгновенный, TTL лишь страховка. `trackActivity` уже throttled — не трогать. Итог: тёплый авторизованный запрос — 0 pre-handler DB-запросов вместо 4. ## 3. Дедуп AI-очереди (одна строка) ```ts // Coalesce re-embeds the same way enqueuePageHistory does: repeated stores // within the window collapse into one job; the processor reads the CURRENT // page state at run time, so the last content always wins. await this.aiQueue.add( QueueJob.PAGE_CONTENT_UPDATED, { pageIds: [page.id], workspaceId: page.workspaceId }, { jobId: page.id, delay: 30_000 }, ); ``` ## 4. Workspace-level short-circuit прав Кэшируемый `hasRestrictedPagesInWorkspace(workspaceId)` (EXISTS по `page_access.workspace_id`, зеркально per-space версии, `withCache` с тем же `PERMISSION_CACHE_TTL_MS` ≈ 5 с — самозаживающий, отдельная инвалидация не обязательна) + short-circuit в `filterAccessiblePageIds`, когда `spaceId` не передан. ## 5. Пакет мелочи По списку из диагноза: удалить `.select('content')` в trash; short-circuit `syncTransclusion` при отсутствии transclusion/reference-узлов и пустых существующих записях; дифф меншенов перед enqueue; прокинуть загруженную страницу из comment.controller в service; `Promise.all` + объединённый обход предков в share; `withCache` для `hasRestrictedPagesInSpace`; route-scoped pipe с `transform:false` для content-несущих DTO (валидация остаётся); `statement_timeout` (щедрый, ~30 с, чтобы не убить export) в конфиг пула; `clearInterval`-guard в `maintainLock`. # Крайние случаи - **Триграммные выражения** должны байт-в-байт совпадать с предикатами запросов (`LOWER(f_unaccent(col))`), иначе планировщик их не возьмёт — проверить `EXPLAIN` после миграции. `f_unaccent` — IMMUTABLE-обёртка, для expression-индекса пригодна. На больших таблицах build индекса при миграции на старте займёт время — приемлемо для self-hosted, но отметить в CHANGELOG. - **Кэш юзера/сессии — окно безопасности**: заблокированный юзер/отозванная сессия живут до TTL. Обязательные явные `del` в logout/revoke/disable; TTL держать ≤15 с. Не кэшировать негативные результаты. - **Дедуп AI-джоба**: BullMQ игнорирует `add` с существующим delayed `jobId` — это и есть коалесинг; корректно, т.к. процессор читает актуальное состояние страницы в момент выполнения. Проверить, что завершённый джоб не блокирует повторный `jobId` (`removeOnComplete: true` у AI-очереди уже стоит). - **`hasRestrictedPagesInWorkspace`**: после выдачи первого page-level ограничения кэш до 5 с может отдавать «нет ограничений» — окно то же, что у существующих permission-кэшей (осознанный трейд-офф форка), поведение консистентно. - **`transform:false` для page-DTO**: убедиться, что `whitelist` и валидация остальных полей сохраняются (ручная проверка формы `content` — лёгкая, без прогонки через class-transformer). - **`statement_timeout`**: не задеть экспорт больших спейсов (грузит все страницы одним запросом) — либо 30–60 с глобально, либо переопределение в export-сессии. # Тесты / проверка - `EXPLAIN (ANALYZE)` саджест-запроса до/после: Bitmap Index Scan по trgm-индексу вместо Seq Scan. - Интеграционный тест auth: тёплый запрос не делает SELECT workspace/user/session (можно через query-лог/спай на репо); logout → следующий запрос с той же сессией = 401 немедленно. - Юнит на enqueue: два store подряд в окне 30 с → один AI-джоб; store после обработки → новый джоб. - Юнит `filterAccessiblePageIds`: воркспейс без ограничений → вход == выход без CTE (спай на db); с ограничениями — прежнее поведение. - Существующие тесты `persistence.extension`, comment, share — прогнать; trash-эндпоинт больше не содержит `content` в ответе. - Смок: правка страницы двумя клиентами 2 мин → очередь AI не растёт монотонно (`bull` метрики/Redis). # Вне скоупа - Клиентская часть — комплект #340/#342/#343/#344/#346. - Вынос export в очередь с download-ссылкой (заметное усилие, редкая операция) — отдельная задача при необходимости. - `ts_headline` по большим `text_content` в полном поиске — bounded LIMIT 25, оставляем. - Инкрементальные эмбеддинги (re-chunk только изменённых блоков) — отдельная большая задача; здесь только дедуп джобов. # План работ 1. Миграция: 3 trgm-индекса + `page_history(page_id, id DESC)` + `comments(page_id, id)`; `EXPLAIN`-проверка. 2. Auth-путь: reuse `req.raw.workspace` + `withCache` workspace/user/session + инвалидация в logout/disable (п. 2). 3. `{jobId, delay}` для `PAGE_CONTENT_UPDATED` (п. 3). 4. `hasRestrictedPagesInWorkspace` + short-circuit (п. 4). 5. Пакет мелочи (п. 5) — можно дробить по PR. 6. Тесты + смок с метриками очередей; замер p50/p95 латентности API до/после на слабом инстансе.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: vvzvlad/gitmost#348