Перейти к содержанию

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()

Навигация