Функция-трансформер и её контракт

Функция-трансформер в библиотеке idb-keyval — это механизм, позволяющий управлять процессом сериализации и десериализации данных при записи и чтении из IndexedDB. Она действует как промежуточный слой между приложением и базой данных, обеспечивая контроль над форматом хранимых данных, их типами и структурой.

Трансформер представляет собой объект с двумя обязательными методами:

  • serialize — вызывается при записи значения в хранилище. Должен принимать любое значение и возвращать объект вида { value: any, type: string }, где value — сериализованное значение, а type — строковый идентификатор типа данных.
  • deserialize — вызывается при чтении значения из хранилища. Принимает объект { value, type } и возвращает восстановленное значение исходного типа.

Контракт трансформера строго определяет, что сериализация и десериализация должны быть взаимно обратимыми. Любое значение, которое прошло через serialize, после применения deserialize должно полностью соответствовать исходному.


Сигнатуры методов

interface Transformer {
    serialize: (value: any) => { value: any, type: string };
    deserialize: (record: { value: any, type: string }) => any;
}

Пояснения к сигнатурам:

  • value: any — любое значение JavaScript, включая объекты, массивы, примитивы, Map, Set.
  • type: string — уникальная строка, однозначно идентифицирующая тип сериализованного объекта. Она необходима для корректного восстановления объекта при десериализации.
  • Возвращаемый объект { value, type } может содержать любое внутреннее представление данных, главное — чтобы десериализатор понимал, как его реконструировать.

Контракт сериализации

  1. Обратимость: Для всех значений x должно выполняться:

    transformer.deserialize(transformer.serialize(x)) === x

    Для сложных объектов обратимость может быть проверена через глубокое сравнение (deepEqual).

  2. Определённость типа: Каждый сериализованный объект должен иметь чётко определённое поле type, позволяющее десериализатору точно понять структуру данных.

  3. Нейтральность к побочным эффектам: Методы serialize и deserialize не должны изменять исходные объекты. Они должны работать исключительно с копиями или промежуточными представлениями.


Примеры реализации трансформеров

Сериализация простых объектов и примитивов

const simpleTransformer = {
    serialize(value) {
        return { value, type: typeof value };
    },
    deserialize(record) {
        return record.value;
    }
};

В этом случае примитивные типы (string, number, boolean) и объекты хранятся без изменений, тип используется лишь для информационных целей.

Сериализация объектов Date

const dateTransformer = {
    serialize(value) {
        if (value instanceof Date) {
            return { value: value.toISOString(), type: 'date' };
        }
        return { value, type: typeof value };
    },
    deserialize(record) {
        if (record.type === 'date') {
            return new Date(record.value);
        }
        return record.value;
    }
};

Этот трансформер обеспечивает безопасное хранение дат в виде строки ISO и корректное восстановление объекта Date.

Сериализация коллекций Map и Set

const collectionTransformer = {
    serialize(value) {
        if (value instanceof Map) {
            return { value: Array.from(value.entries()), type: 'map' };
        }
        if (value instanceof Set) {
            return { value: Array.from(value), type: 'set' };
        }
        return { value, type: typeof value };
    },
    deserialize(record) {
        if (record.type === 'map') {
            return new Map(record.value);
        }
        if (record.type === 'set') {
            return new Set(record.value);
        }
        return record.value;
    }
};

Такой подход позволяет хранить нестандартные структуры JavaScript без потери данных.


Интеграция трансформера с Idb-keyval

При создании нового хранилища через createStore или при вызове методов get, set можно передать трансформер:

import { createStore, set, get } from 'idb-keyval';

const store = createStore('my-database', 'my-store', {
    transformer: dateTransformer
});

set('eventDate', new Date(), store);
get('eventDate', store).then(date => console.log(date instanceof Date)); // true

Ключевые моменты:

  • Передача трансформера через объект конфигурации позволяет применять единый контракт сериализации для всего хранилища.
  • Можно комбинировать несколько трансформеров, обрабатывая разные типы данных в одном хранилище, реализуя логику внутри методов serialize и deserialize.
  • Трансформеры не влияют на структуру IndexedDB: они лишь управляют форматом значений, оставаясь полностью совместимыми с браузерным API.

Практические рекомендации по контракту

  1. Явное указание типов — всегда использовать type, даже если данные примитивные. Это облегчает отладку и расширение трансформера.
  2. Минимализм сериализованного объекта — не хранить лишние поля, только value и type, чтобы не перегружать IndexedDB.
  3. Глубокая сериализация сложных объектов — при необходимости сериализовать вложенные структуры (Map внутри Set, вложенные объекты) делать это рекурсивно внутри serialize.
  4. Обработка ошибок — десериализатор должен корректно реагировать на неизвестный тип, выбрасывая исключение или возвращая исходное значение.

Контракт трансформера и расширяемость

Трансформер в idb-keyval является точкой расширения для:

  • Поддержки пользовательских типов (Blob, ArrayBuffer, TypedArray).
  • Создания универсального хранилища, которое автоматически преобразует данные между JavaScript и IndexedDB.
  • Интеграции с внешними библиотеками сериализации, например msgpack, protobuf или JSON.stringify для сложных объектов.

Строгий контракт гарантирует, что любые данные, сериализованные через serialize, всегда могут быть корректно восстановлены через deserialize, что делает хранилище предсказуемым и безопасным для сложных веб-приложений.