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

Dexie Schema — Полный код

Уровень: L3 (deep-dive) | ← Назад к Offline

Файл: frontend/src/offline/db/schema.ts

import Dexie from 'dexie';
import type {
  LocalItem,
  LocalOutfit,
  LocalOutfitItem,
  LocalMedia,
  PendingOperation,
  SyncMetadata,
  LocalEmbedding,
} from '../sync/syncTypes';

export class PlechikiDB extends Dexie {
  // Типизированные таблицы
  items!: Dexie.Table<LocalItem, string>;
  outfits!: Dexie.Table<LocalOutfit, string>;
  outfit_items!: Dexie.Table<LocalOutfitItem, string>;
  media!: Dexie.Table<LocalMedia, string>;
  pending_operations!: Dexie.Table<PendingOperation, string>;
  sync_metadata!: Dexie.Table<SyncMetadata, string>;
  embeddings!: Dexie.Table<LocalEmbedding, [string, string]>;  // Составной PK

  // Таблица бинарных данных (фото) — без лимита 5MB localStorage
  blobs!: Dexie.Table<{ id: string; data: string }, string>;

  constructor() {
    super('plechiki');  // Имя IndexedDB базы данных

    // ─── ВЕРСИЯ 2: Основная схема ────────────────────────────────────
    // Добавлены primary_media_url, cover_media_url, item_media_urls
    this.version(2).stores({
      items: 'id, server_id, sync_status, category_id, primary_color_id',
      outfits: 'id, server_id, sync_status, origin_type',
      outfit_items: 'id, outfit_id, item_id',
      media: 'id, server_media_id, status',
      pending_operations: 'id, status, [entity_type+entity_id], depends_on',
      sync_metadata: 'entity_type',
      embeddings: '[entity_type+entity_id], [entity_type+is_stale]',
    });

    // ─── ВЕРСИЯ 3: Таблица blobs ────────────────────────────────────
    // Перенос хранения фото из localStorage в IndexedDB (нет лимита 5MB)
    this.version(3).stores({
      items: 'id, server_id, sync_status, category_id, primary_color_id',
      outfits: 'id, server_id, sync_status, origin_type',
      outfit_items: 'id, outfit_id, item_id',
      media: 'id, server_media_id, status',
      pending_operations: 'id, status, [entity_type+entity_id], depends_on',
      sync_metadata: 'entity_type',
      embeddings: '[entity_type+entity_id], [entity_type+is_stale]',
      blobs: 'id',
    });

    // ─── ВЕРСИЯ 4: Оптимизация индексов ─────────────────────────────
    // Удалён неиспользуемый индекс [entity_type+is_stale] у embeddings
    this.version(4).stores({
      embeddings: '[entity_type+entity_id]',
    });
  }
}

Файл: frontend/src/offline/db/dexie.ts

import { PlechikiDB } from './schema';

/** Singleton — единственный экземпляр базы данных */
export const db = new PlechikiDB();

Описание таблиц

items — Вещи гардероба

Индекс Тип Назначение
id Primary Key Локальный UUID
server_id Unique UUID от сервера (для маппинга)
sync_status Index Фильтр pending операций
category_id Index Фильтрация по категории
primary_color_id Index Фильтрация по цвету

outfits — Образы

Индекс Тип Назначение
id Primary Key Локальный UUID
server_id Unique UUID от сервера
sync_status Index Фильтр pending
origin_type Index Фильтр свои/чужие

outfit_items — Связь образ↔вещь

Индекс Тип Назначение
id Primary Key {outfitId}_{slotId}_{layerIndex}
outfit_id Index Поиск вещей в образе
item_id Index Поиск образов с вещью

pending_operations — Очередь синхронизации

Индекс Тип Назначение
id Primary Key UUID операции
status Index Фильтр по статусу
[entity_type+entity_id] Compound Все операции для сущности
depends_on Index Поиск зависимостей

embeddings — Векторные представления

Индекс Тип Назначение
[entity_type+entity_id] Compound PK item/outfit + UUID

blobs — Бинарные данные (фото)

Индекс Тип Назначение
id Primary Key UUID медиа

Типы данных

Файл: frontend/src/offline/sync/syncTypes.ts

/** Статус синхронизации локальной сущности */
export type SyncStatus =
  | 'synced'           // Синхронизирован с сервером
  | 'pending_create'   // Создан локально, ждёт отправки
  | 'pending_update'   // Обновлён локально, ждёт отправки
  | 'pending_delete'   // Удалён локально (soft-delete), ждёт подтверждения
  | 'syncing'          // В процессе синхронизации
  | 'failed'           // Ошибка синхронизации
  | 'conflict';        // Конфликт версий

/** Статус операции в очереди */
export type OperationStatus =
  | 'queued'     // Готова к выполнению
  | 'running'    // Выполняется
  | 'done'       // Завершена успешно
  | 'failed'     // Ошибка (будет retry)
  | 'cancelled'  // Отменена
  | 'blocked';   // Ждёт завершения зависимости (depends_on)

/** Типы операций в очереди синхронизации */
export type OperationType =
  | 'ITEM_CREATE'         // Создание вещи на сервере
  | 'ITEM_UPDATE'         // Обновление вещи
  | 'ITEM_DELETE'         // Удаление вещи
  | 'ITEM_MEDIA_UPLOAD'   // Загрузка фото вещи
  | 'OUTFIT_CREATE'       // Создание образа
  | 'OUTFIT_UPDATE'       // Обновление образа
  | 'OUTFIT_DELETE'       // Удаление образа
  | 'FULL_SYNC_ITEMS'     // Полная синхронизация вещей
  | 'FULL_SYNC_OUTFITS'   // Полная синхронизация образов
  | 'EMBEDDING_REFRESH';  // Обновление эмбеддингов

/** Тип сущности, управляемой sync */
export type SyncEntityType = 'item' | 'outfit' | 'media';

/** Локальная запись вещи в IndexedDB */
export interface LocalItem {
  id: string;                    // Локальный PK (UUID)
  server_id: string | null;      // UUID от сервера (null для offline-created)
  local_only: boolean;           // Ещё не существует на сервере
  sync_status: SyncStatus;
  deleted_locally: boolean;      // Soft-delete
  title: string | null;
  category_id: number | null;
  primary_color_id: number | null;
  style_node_ids: number[];
  primary_media_id: string | null;    // Server media UUID
  primary_media_url: string | null;   // Resolved URL
  local_media_ref: string | null;     // Reference to blobs table
  created_at: string;
  updated_at: string;
  server_updated_at: string | null;   // Для conflict detection
}

/** Локальная запись образа */
export interface LocalOutfit {
  id: string;
  server_id: string | null;
  local_only: boolean;
  sync_status: SyncStatus;
  deleted_locally: boolean;
  title: string | null;
  visibility: string;           // private|public|unlisted
  origin_type: string;          // manual|imported|shared|generated
  is_external: boolean;         // Чужой образ (из ленты/шеринга)
  cover_media_id: string | null;
  cover_media_url: string | null;
  item_media_urls: string[];    // URLs фото вещей для превью
  created_at: string;
  updated_at: string;
  server_updated_at: string | null;
}

/** Связь образ ↔ вещь */
export interface LocalOutfitItem {
  id: string;           // "{outfitId}_{slotId}_{layerIndex}"
  outfit_id: string;
  item_id: string;
  slot_id: number;      // Слот (верх, низ, обувь...)
  layer_index: number;  // Порядок слоя в слоте
}

/** Локальные метаданные медиа */
export interface LocalMedia {
  id: string;
  local_blob_key: string | null;    // Ключ в таблице blobs
  server_media_id: string | null;   // UUID от сервера
  server_url: string | null;
  mime_type: string;
  status: 'local' | 'uploading' | 'uploaded' | 'failed';
  width: number | null;
  height: number | null;
}

/** Операция в очереди синхронизации */
export interface PendingOperation {
  id: string;
  operation_type: OperationType;
  entity_type: SyncEntityType;
  entity_id: string;
  payload: Record<string, unknown>;     // Данные операции
  depends_on: string | null;            // ID операции-зависимости
  status: OperationStatus;
  retry_count: number;                  // 0..MAX_RETRIES
  next_retry_at: string | null;         // Когда повторить
  last_error: string | null;
  idempotency_key: string;              // Для дедупликации на сервере
  created_at: string;
  updated_at: string;
}

/** Метаданные синхронизации по типу сущности */
export interface SyncMetadata {
  entity_type: SyncEntityType;
  last_full_sync_at: string | null;
  last_delta_sync_at: string | null;
  etag: string | null;
  version: number;    // Инкрементируется при каждой синхронизации
}

/** Локальный эмбеддинг */
export interface LocalEmbedding {
  entity_type: 'item' | 'outfit';
  entity_id: string;
  vector: Float32Array;         // Вектор размерности dim
  dim: number;                  // Размерность (1024)
  model_version: string;        // "resnet50-v1"
  computed_at: string;
  source: 'server' | 'derived'; // server = с бэкенда, derived = вычислен локально
  is_stale: boolean;            // Нуждается в пересчёте
}

Миграции

Версия Изменения
v2 Начальная схема: items, outfits, outfit_items, media, pending_operations, sync_metadata, embeddings
v3 Добавлена таблица blobs — перенос хранения фото из localStorage в IndexedDB
v4 Удалён индекс [entity_type+is_stale] у embeddings (не использовался)