Offline-First Architecture¶
Уровень: L2 (details) | ← Назад
Обзор¶
Offline-слой обеспечивает работу приложения без интернета. Все данные хранятся локально в IndexedDB (через Dexie.js), UI всегда читает из локальной БД, а синхронизация с сервером происходит в фоне.
Архитектура — 4 слоя¶
graph TB
subgraph "Layer 4: UI Hooks"
useLocalItems[useLocalItems]
useLocalOutfits[useLocalOutfits]
useLocalItem[useLocalItem]
useLocalReference[useLocalReference]
usePendingCount[usePendingCount]
useLocalItemsByIds[useLocalItemsByIds]
useLocalOutfitsByIds[useLocalOutfitsByIds]
end
subgraph "Layer 3: Services"
offlineItems[offlineItemsService<br/>create/update/delete]
offlineOutfits[offlineOutfitsService<br/>create/update/delete]
initialSync[initialSync<br/>performInitialSync/performRefreshSync]
localData[localData<br/>clearLocalData]
end
subgraph "Layer 2: Repositories"
itemsRepo[itemsRepository<br/>CRUD + bulk ops]
outfitsRepo[outfitsRepository<br/>CRUD + bulk ops]
syncRepo[syncRepository<br/>operation queue]
mediaRepo[mediaRepository<br/>blob URLs]
end
subgraph "Layer 1: Database"
DB[(PlechikiDB<br/>Dexie/IndexedDB)]
end
useLocalItems --> itemsRepo
useLocalOutfits --> outfitsRepo
usePendingCount --> syncRepo
offlineItems --> itemsRepo
offlineItems --> syncRepo
offlineOutfits --> outfitsRepo
offlineOutfits --> syncRepo
initialSync --> itemsRepo
initialSync --> outfitsRepo
localData --> DB
itemsRepo --> DB
outfitsRepo --> DB
syncRepo --> DB
mediaRepo --> DB

ER-диаграмма IndexedDB¶
erDiagram
ITEMS {
string id PK "UUID (local PK)"
string server_id UK "UUID from server"
string sync_status "synced|pending_create|pending_update|pending_delete|failed"
boolean local_only "true for offline-created"
boolean deleted_locally "soft-delete flag"
string title "nullable"
int category_id FK "→ reference"
int primary_color_id FK "→ reference"
string primary_media_id "server media UUID"
string primary_media_url "resolved URL"
string local_media_ref "blob reference"
string created_at "ISO timestamp"
string updated_at "ISO timestamp"
}
OUTFITS {
string id PK "UUID (local PK)"
string server_id UK "UUID from server"
string sync_status "synced|pending_*|failed"
boolean local_only "true for offline-created"
boolean deleted_locally "soft-delete"
string title "nullable"
string visibility "private|public|unlisted"
string origin_type "manual|imported|shared|generated"
boolean is_external "from another user"
string cover_media_url "resolved URL"
string[] item_media_urls "preview URLs"
}
OUTFIT_ITEMS {
string id PK "outfitId_slotId_layerIndex"
string outfit_id FK "→ OUTFITS.id"
string item_id FK "→ ITEMS.id"
int slot_id "верх/низ/обувь/..."
int layer_index "порядок слоя"
}
MEDIA {
string id PK "local UUID"
string local_blob_key "reference to blobs table"
string server_media_id "UUID from server"
string server_url "resolved URL"
string mime_type "image/jpeg etc"
string status "local|uploading|uploaded|failed"
}
PENDING_OPERATIONS {
string id PK "UUID"
string operation_type "ITEM_CREATE|ITEM_UPDATE|..."
string entity_type "item|outfit|media"
string entity_id FK "→ entity"
json payload "operation data"
string depends_on FK "→ PENDING_OPERATIONS.id"
string status "queued|running|done|failed|cancelled|blocked"
int retry_count "0..5"
string next_retry_at "ISO timestamp"
string idempotency_key "UUID"
}
SYNC_METADATA {
string entity_type PK "item|outfit|media"
string last_full_sync_at "ISO timestamp"
string last_delta_sync_at "ISO timestamp"
int version "incrementing"
}
EMBEDDINGS {
string entity_type PK "item|outfit"
string entity_id PK "UUID"
Float32Array vector "embedding vector"
int dim "1024"
string model_version "resnet50-v1"
string source "server|derived"
boolean is_stale "needs recomputation"
}
BLOBS {
string id PK "UUID"
string data "base64 data URL"
}
OUTFITS ||--o{ OUTFIT_ITEMS : "contains"
ITEMS ||--o{ OUTFIT_ITEMS : "used in"
ITEMS ||--o| MEDIA : "has photo"
ITEMS ||--o| EMBEDDINGS : "has embedding"
OUTFITS ||--o| EMBEDDINGS : "has embedding"
PENDING_OPERATIONS ||--o| PENDING_OPERATIONS : "depends_on"

Краткое описание каждого слоя¶
Layer 1: Database (Dexie/IndexedDB)¶
- Singleton-экземпляр
PlechikiDB - 8 таблиц: items, outfits, outfit_items, media, pending_operations, sync_metadata, embeddings, blobs
- 4 версии схемы (миграции)
- Составные индексы для эффективных запросов
Layer 2: Repositories¶
- itemsRepository — CRUD для вещей, bulk upsert, reconcile, deduplicate
- outfitsRepository — CRUD для образов, транзакционное обновление с outfit_items
- syncRepository — управление очередью операций и метаданными синхронизации
- mediaRepository — резолв URL медиа (server URL или local blob)
Layer 3: Services¶
- offlineItemsService — бизнес-логика создания/обновления/удаления вещей с blob storage
- offlineOutfitsService — бизнес-логика образов с транзакционной записью
- initialSync — первичная загрузка данных после login + периодическое обновление
- localData — полная очистка при logout
Layer 4: Hooks¶
- useLocalItems/useLocalOutfits — чтение из IndexedDB + background refresh
- useLocalItem/useLocalOutfit — одиночные записи с network fallback
- useLocalReference — справочники из localStorage с stale time
- usePendingCount — счётчик ожидающих операций для UI badge
- useLocalItemsByIds/useLocalOutfitsByIds — batch-загрузка по ID (для similarity)
Ключевые паттерны¶
| Паттерн | Описание |
|---|---|
| Local-first reads | UI всегда читает из IndexedDB, не ждёт сеть |
| Optimistic updates | Изменения видны мгновенно, sync в фоне |
| Soft-deletes | deleted_locally: true до подтверждения сервером |
| Dual-ID mapping | id (local UUID) ↔ server_id (server UUID) |
| Dependency tracking | Операции могут зависеть от завершения других (upload → create) |
| Blob-in-IndexedDB | Фото хранятся как base64 data URL в таблице blobs (без лимита 5MB) |
| Transaction safety | Multi-table updates через db.transaction() |
Навигация¶
- → Схема БД — полный код Dexie, таблицы, индексы
- → Репозитории — полный код всех репозиториев
- → Сервисы — полный код бизнес-логики
- → Хуки — полный код React-хуков offline-слоя
- ← Назад к обзору