Files
ds-galo/openspec/changes/archive/2026-10-12-fix-trash-counter-sync/design.md
T

4.7 KiB

Design

Context

См. proposal.md — Why. Существенное:

  • TrashStore (frontend/src/lib/stores/trash.svelte.ts) держит count и умеет load() (запрос GET /api/trash/count) и reset().
  • AlbumRail.svelte показывает trash.count и вызывает trash.load() только в $effect при смене $page.url.pathname и при аутентификации.
  • trash/+page.svelte вызывает trash.load() только в emptyAll; restore, confirmRestore, remove (окончательное удаление) обновляют лишь свой список.
  • Удаление фото разбросано: deleteBatch в сетке и панели, прямой api(...DELETE) в просмотре; удаление альбома — в AlbumActionsDialogs и settings/+page.svelte.
  • Серверный TrashCount считает мягко удалённые фото, доступные пользователю, и корректен.

Goals / Non-Goals

Goals:

  • Счётчик корзины в рейле отражает актуальное состояние после любой операции, меняющей содержимое корзины, без перезагрузки.
  • Единообразный, предсказуемый способ обновления во всех точках.

Non-Goals:

  • Синхронизация счётчика между сессиями/устройствами (SSE-событий корзины нет).
  • Изменения backend и Android.
  • Оптимистичные локальные инкременты без сверки с сервером.

Decisions

1. Обновлять счётчик из сервера (trash.load()), а не инкрементировать локально

После каждой успешной мутации вызываем trash.load(). Это даёт точное значение для всех случаев, включая пакетное удаление и удаление альбома (где число фото заранее неизвестно точно), и повторяет уже принятый в emptyAll паттерн. Альтернатива с оптимистичным +n/-1 требует знать точное число и расходится при частичных ошибках; альтернатива с общим сигналом (как photo-updates) добавляет лишний слой ради одного значения.

2. Вызывать обновление в каждой точке мутации, после успешного await

Обновление идёт после успешного ответа (в try, не в finally), чтобы при ошибке счётчик не менялся. Точки: пакетное удаление (сетка), удаление из панели/просмотра, удаление в просмотре, восстановление и окончательное удаление в корзине, удаление альбома (диалог и настройки). Это устраняет исходный дефект и родственные рассинхроны той же природы.

3. Оставить асинхронность неблокирующей

trash.load() уже глотает ошибки и не мешает основному действию; вызов не await-им блокирующе там, где это меняет UX (в остальных местах допустимо await для детерминизма тестов). Достаточно, чтобы счётчик обновился к моменту отрисовки.

Risks / Trade-offs

  • [Лишний GET /api/trash/count на каждую мутацию] → незначительная нагрузка, сопоставимо с текущим поведением при навигации и очистке корзины.
  • [Гонка при параллельных мутациях] → последний ответ сервера побеждает; значение остаётся корректным, т.к. берётся из источника истины.
  • [Разброс точек вызова — риск пропустить новую в будущем] → спека фиксирует инвариант; при добавлении новых операций с корзиной требование распространяется на них.

Migration Plan

Не требуется: изменения только во фронтенде, без изменения API и данных.