Бинарные данные: TypedArray

Binary data в браузере представляют собой один из ключевых сценариев работы с современными веб-приложениями, особенно когда требуется хранить изображения, аудио, шифрованные блоки данных, результаты вычислений WebGL, файлы или любые структуры, выходящие за пределы строк и JSON. В экосистеме браузера такие данные чаще всего представлены через ArrayBuffer и типизированные массивы (TypedArray), которые обеспечивают эффективную работу с двоичными буферами фиксированного размера.

localForage, как абстракция над IndexedDB, WebSQL и localStorage, предоставляет единый интерфейс хранения, но при работе с бинарными данными возникает ряд особенностей, связанных с сериализацией, клонированием и выбором драйвера.

В JavaScript бинарные данные не являются «первоклассным» типом в привычном смысле строки или числа. Вместо этого используется набор структур:

  • ArrayBuffer — сырой блок памяти фиксированного размера
  • TypedArray — представления над буфером (Uint8Array, Int16Array, Float32Array и др.)
  • DataView — низкоуровневый доступ с управлением порядком байтов

TypedArray не хранит данные сам по себе, а лишь предоставляет «окно» в ArrayBuffer. Это критично для понимания того, как такие структуры сериализуются при сохранении через localForage.

Поведение localForage при сохранении бинарных данных

localForage использует разные драйверы в зависимости от окружения, но наиболее важный для бинарных данных — IndexedDB. Именно он поддерживает хранение Blob, ArrayBuffer и частично TypedArray без преобразования в строки.

Базовый сценарий:

import localforage from "localforage";

const buffer = new ArrayBuffer(8);
const view = new Uint8Array(buffer);

view[0] = 255;
view[1] = 128;

await localforage.setItem("bin", buffer);

const result = await localforage.getItem("bin");

В большинстве современных браузеров ArrayBuffer сохраняется напрямую, без преобразования в JSON или Base64, если используется IndexedDB. Это обеспечивает высокую производительность и минимальные накладные расходы.

TypedArray как входной тип данных

localForage принимает не только ArrayBuffer, но и любые TypedArray. Однако важно понимать, что сохраняется не сам объект представления, а его underlying buffer.

const data = new Uint16Array([1000, 2000, 3000]);

await localforage.setItem("typed", data);

При сохранении происходит извлечение data.buffer. При восстановлении возвращается именно ArrayBuffer, а не Uint16Array. Это означает, что типизация теряется и должна быть восстановлена вручную.

Восстановление TypedArray после загрузки

После получения данных из storage необходимо восстановить конкретный тип представления:

const buffer = await localforage.getItem("typed");

const restored = new Uint16Array(buffer);

console.log(restored[0]); // 1000

Это ключевой момент архитектуры: localForage не хранит метаданные о типе массива, поэтому восстановление всегда является явной операцией.

Работа с различными представлениями памяти

TypedArray включает множество типов, и выбор зависит от задачи:

  • Uint8Array — универсальный формат байтов (изображения, файлы)
  • Int16Array — аудио с 16-bit PCM
  • Float32Array — WebGL и аудиосигналы
  • Uint32Array — индексы, hash-структуры

Пример хранения Float32 данных:

const signal = new Float32Array([0.1, 0.5, -0.3, 0.8]);

await localforage.setItem("signal", signal.buffer);

const loaded = await localforage.getItem("signal");
const restoredSignal = new Float32Array(loaded);

Особенности сериализации в разных драйверах

localForage может переключаться между IndexedDB, WebSQL и localStorage. Это критически влияет на работу с бинарными данными.

IndexedDB

  • Поддерживает ArrayBuffer и Blob
  • Не требует преобразований
  • Максимальная производительность

WebSQL

  • Частичная поддержка бинарных данных
  • Возможны преобразования в строки (Blob/base64)

localStorage

  • Не поддерживает бинарные данные напрямую
  • localForage использует сериализацию (обычно Base64)

Следствие: одинаковый код может вести себя по-разному в зависимости от драйвера.

Проблема клонирования и структурного копирования

IndexedDB использует алгоритм структурного клонирования (structured clone algorithm). Он поддерживает:

  • ArrayBuffer
  • TypedArray (как views)
  • Blob
  • Map/Set (частично)

Но важно, что TypedArray при сохранении может восстанавливаться как buffer без сохранения типа view.

Это объясняет поведение:

const original = new Uint8Array([1, 2, 3]);

await localforage.setItem("x", original);

const loaded = await localforage.getItem("x");

console.log(loaded instanceof Uint8Array); // false
console.log(loaded instanceof ArrayBuffer); // true

Работа с поддиапазонами буфера

TypedArray может ссылаться на часть буфера:

const buffer = new ArrayBuffer(10);
const full = new Uint8Array(buffer);
const slice = new Uint8Array(buffer, 2, 4);

slice.set([10, 20, 30, 40]);

При сохранении через localForage сохраняется весь ArrayBuffer, а не подмассив. Это означает, что логика «срезов» должна быть восстановлена отдельно.

Копирование и защита от мутаций

Поскольку ArrayBuffer передается по ссылке, возможны побочные эффекты при изменении исходного объекта после сохранения:

const buffer = new ArrayBuffer(4);
const view = new Uint8Array(buffer);

await localforage.setItem("safe", buffer);

view[0] = 99;

В IndexedDB данные уже сериализованы, поэтому изменения после setItem не влияют на сохраненное значение, но в некоторых драйверах или при буферизации это может отличаться. Поэтому безопасной практикой является явное копирование:

const copy = buffer.slice(0);
await localforage.setItem("safe", copy);

Производительность при больших бинарных объектах

TypedArray используется в сценариях, где критична скорость:

  • обработка изображений (Canvas ImageData)
  • аудио буферы (Web Audio API)
  • видео чанки (MediaRecorder)
  • криптографические операции

localForage добавляет минимальный overhead поверх IndexedDB, но важно учитывать:

  • запись больших ArrayBuffer блокирующая для транзакции
  • частые записи ухудшают производительность
  • предпочтительнее батчинг данных

Пример пакетной записи:

const chunks = [];

for (let i = 0; i < 10; i++) {
  const buf = new Uint8Array(1024);
  crypto.getRandomValues(buf);
  chunks.push(buf.buffer);
}

await Promise.all(
  chunks.map((chunk, i) => localforage.setItem(`chunk_${i}`, chunk))
);

Восстановление сложных структур

При работе с бинарными данными часто требуется хранить не только сам буфер, но и метаданные: длину, тип, смещение.

Один из подходов — комбинированное хранение:

const record = {
  type: "float32",
  length: data.length,
  buffer: data.buffer
};

await localforage.setItem("record", record);

Но здесь возникает ограничение: localForage сериализует объект целиком, и ArrayBuffer останется валидным только в IndexedDB-драйвере.

Более универсальный подход — разделение:

await localforage.setItem("meta", {
  type: "float32",
  length: data.length
});

await localforage.setItem("data", data.buffer);

Интеграция TypedArray с Web APIs

TypedArray часто является мостом между localForage и другими API:

  • Canvas: getImageData().data → Uint8ClampedArray
  • Web Audio: AudioBuffer → Float32Array
  • Fetch streams: ArrayBuffer responses
  • WebGL buffers

Пример сохранения изображения:

const imageData = ctx.getImageData(0, 0, 100, 100);

await localforage.setItem("img", imageData.data.buffer);

И восстановление:

const buffer = await localforage.getItem("img");
const pixels = new Uint8ClampedArray(buffer);

const imageData = new ImageData(pixels, 100, 100);
ctx.putImageData(imageData, 0, 0);

Ограничения и нюансы TypedArray в localForage

Несмотря на универсальность, существуют ограничения:

  • нет автоматического восстановления типа TypedArray
  • возможна деградация до Base64 в localStorage
  • большие буферы могут превышать лимиты синхронного доступа
  • невозможно хранить ссылки на shared memory (SharedArrayBuffer) в некоторых режимах

Также важно учитывать различие между:

  • view (Uint8Array)
  • buffer (ArrayBuffer)

localForage оперирует именно buffer-уровнем, игнорируя view-метаданные.

Оптимальные стратегии работы с бинарными данными

Для устойчивой архитектуры хранения обычно применяются следующие принципы:

  • всегда сохранять ArrayBuffer, а не TypedArray
  • отдельно хранить тип интерпретации данных
  • избегать частых мелких записей
  • использовать Uint8Array как универсальный транспортный слой
  • явно восстанавливать view при чтении

Пример универсального паттерна:

function saveTyped(key, typedArray) {
  return localforage.setItem(key, {
    buffer: typedArray.buffer,
    type: typedArray.constructor.name
  });
}

function loadTyped(record) {
  const map = {
    Uint8Array,
    Uint16Array,
    Float32Array
  };

  return new map[record.type](record.buffer);
}

Такая схема делает работу с binary data предсказуемой независимо от драйвера и окружения.