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]',
});
}
}
/** Статус синхронизации локальной сущности */
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; // Нуждается в пересчёте
}