# Create Offline Outbox

> Use when пользователь просит реализовать offline-first очередь исходящих мутаций, outbox или гарантированную последующую отправку локально сохранённых действий через Room, WorkManager и Ktor. Не используй для обычной фоновой загрузки, периодического refresh, fire-and-forget telemetry или запроса, который можно безопасно повторить напрямую без durable queue; для них достаточно WorkManager- или Ktor-workflow. Этот skill требует серверной либо protocol-level стратегии идемпотентности.

- Skill: `michaelbel/create-offline-outbox` (Agent Skill)
- Install (CLI): `npx skillmds@latest add michaelbel/create-offline-outbox`
- Raw SKILL.md: https://api.skillmd.com/api/skills/michaelbel/create-offline-outbox/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Productivity
- Author: michaelbel (https://skillmd.com/u/michaelbel)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/michaelbel/create-offline-outbox

---


# Offline outbox

Создаёт durable pipeline `local mutation -> Room outbox -> unique WorkManager job -> Ktor ->
acknowledged removal`. Пользовательское действие считается поставленным в очередь только после
успешной записи Room; запись удаляется только после подтверждённого сервером успеха этой операции.

## Сначала определи контракт доставки

Зафиксируй до реализации:

- какие мутации разрешено выполнять offline и какой payload нужен для точного replay;
- стабильный `operationId` / idempotency key и как сервер дедуплицирует повторную отправку;
- scope порядка: глобальный, на пользователя, заказ, маршрут или другую aggregate-сущность;
- какие HTTP/domain ошибки retryable, а какие permanent;
- что происходит с permanently failed первой операцией: блокировка очереди, явный dead-letter или
  показ ошибки пользователю;
- срок жизни операции при logout и смене аккаунта.

Если endpoint не поддерживает идемпотентность и повтор может применить мутацию дважды, не называй
pipeline гарантированным. Остановись на проектировании protocol change или явно согласованной
компенсации.

## Room-модель очереди

Храни минимум stable operation id, owner/aggregate scope, монотонную последовательность, operation
type/endpoint, сериализованный request payload и диагностическое состояние попытки. Payload должен
быть самодостаточным для replay после process death и обновления access token; не сохраняй transient
UI state или секреты, которые можно получить в момент отправки.

- Обеспечь уникальность `operationId` и детерминированный `ORDER BY` внутри ordering scope.
- Добавь DAO для вставки, чтения следующей порции, обновления retry metadata и удаления
  подтверждённой записи.
- Регистрируй entity/DAO в `AppDatabase` и увеличивай `DATABASE_VERSION`.
- Если локальное optimistic-изменение и outbox-запись должны появиться вместе, выполни оба DAO-вызова
  в одной `AppDatabase.withTransaction` на уровне use case.
- Не удаляй или заменяй ранее поставленную операцию как «устаревшую», если её успешная отправка не
  подтверждена сервером. Coalescing требует отдельной явно доказанной бизнес-семантики и не является
  поведением по умолчанию.

## Постановка в очередь

Domain use case сериализует request, создаёт stable idempotency key, атомарно сохраняет outbox и
только затем планирует unique one-time work. Повторное планирование безопасно: используй стабильное
unique work name для нужного scope и политику, которая не запускает параллельных drain-workers.

Room commit и `enqueueUniqueWork` не образуют общей транзакции: process death между ними может
оставить запись без worker. Добавь обязательную reconciliation-точку — при старте авторизованной
сессии и восстановлении процесса проверяй наличие pending outbox и повторно планируй drain. Можно
также наблюдать очередь отдельным process-scoped coordinator, но он не заменяет startup recovery.

Worker требует сеть. Настрой backoff по принятой политике и не делай собственный бесконечный retry
loop поверх WorkManager. Новая запись, добавленная во время работы worker, должна либо попасть в
текущий drain, либо гарантированно вызвать следующий запуск.

## Drain и replay

1. Worker получает зависимости через принятый Hilt/WorkManager механизм и делегирует операции
   конкретным use case; он не содержит endpoint-specific бизнес-логику.
2. Читай записи в строгом порядке выбранного scope. Не отправляй более позднюю зависимую операцию,
   пока предыдущая не получила подтверждение.
3. Десериализуй payload по operation type, передай тот же `operationId` как idempotency key и
   выполни сетевой вызов через `NetworkService`/Ktor с обычной обработкой ответа проекта.
4. Удаляй конкретную запись в Room только после подтверждённого success. Если процесс погиб после
   server success, но до delete, следующий replay должен быть безопасен благодаря тому же
   idempotency key.
5. На retryable ошибке сохрани диагностику, останови этот ordering scope и верни `Result.retry()`;
   не позволяй следующей записи обогнать неуспешную.
6. На permanent ошибке сохрани её как видимое blocked/dead-letter состояние и верни результат по
   явной продуктовой политике. Не удаляй запись и не маскируй ошибку как success.
7. Пробрасывай `CancellationException`; в `finally` выполняй только ограниченный cleanup и
   диагностируй stop reason там, где API это поддерживает.

## Согласованность и lifecycle

- Не держи transaction открытой во время HTTP-запроса. Claim/lease нужен только если архитектура
  допускает несколько consumers; проще обеспечить одного unique worker на scope.
- После refresh с сервера не затирай локальное optimistic state, пока связанная outbox-операция не
  подтверждена или конфликт не разрешён явно.
- При logout запроси отмену worker нужного account scope и дождись её завершения там, где lifecycle
  допускает ожидание, затем примени выбранную политику к очереди. Поскольку уже начатый HTTP-запрос
  мог завершиться, worker проверяет owner/current account перед каждой отправкой и перед Room
  mutation; отдельно зафиксируй продуктовую семантику ответа, пришедшего во время logout. Никогда не
  отправляй запись с credentials другого пользователя.
- Закрой окно между owner-check и чтением auth headers: request boundary/interceptor сверяет
  session generation операции с текущей сессией непосредственно перед отправкой и отклоняет
  несовпадение либо использует credential snapshot, однозначно привязанный к тому же owner. Не
  сохраняй access token внутри outbox payload.
- Наблюдаемое количество failures/blocked operations предоставляй отдельным `FlowUseCase`, если UI
  действительно его показывает.

## Проверка результата

Проверь постановку без сети, process death после Room commit до enqueue, process death до запуска
worker, server success с падением до Room delete, retryable и permanent ошибки, две зависимые
операции, конкурентное планирование, добавление операции во время drain и logout со сменой
пользователя. Главные утверждения теста: операция не теряется, не обгоняется и не применяется
дважды на сервере.

