Skip to content

Загрузка и сериализация

Экземпляры разбираются из JSON в add и сериализуются обратно в json() через одни и те же декларации полей. Используйте эту страницу при связывании моделей с API: что попадает в JSON, как серверные id заменяют временные и почему по дороге ничего не ломается.

Примеры на этой странице выполняются в активном скоупе - см. Коллекции и экземпляры.

Сериализация

ts
const t = todos.add({ id: "1", title: "написать доку" });

t.json();           // { id: "1", title: "написать доку", done: false }
JSON.stringify(t);  // то же - toJSON подключён к json()

Что уходит:

  • связанные поля под своими ключами JSON, через выходные трансформы;
  • ссылки - как id, дети - встроенными объектами;
  • локальные поля, инверсные виды и члены - никогда.

Временные id

Экземпляр, созданный без id, получает автогенерированный временный. Локально он работает везде, но не сериализуется никогда:

ts
const t = todos.add({ title: "новая" });

t.id;     // "~tmp1" - годится для роутов, get(), ссылок
t.json(); // { title: "новая", done: false } - без id: его назначит сервер

В json() попадают только id, пришедшие извне - из входа add или из rebind.

Смена id: rebind

Оптимистичный сценарий создания, от начала до конца:

ts
const t = todos.add({ name });          // временный id
navigate(`/todo/${t.id}`);              // URL со временным id

const saved = await api.save(t.json()); // в payload нет id
t.rebind(saved.id);                     // теперь серверный

Что происходит на rebind:

  • каждая связь, указывающая на экземпляр, переписывается на новый id;
  • старый id продолжает разрешаться: todos.get("~tmp1") возвращает тот же экземпляр - открытый роут и компоненты со старым id работают дальше;
  • этот форвардинг существует только для поиска - он не попадает в индексы, результаты запросов и json();
  • запись форвардинга умирает вместе с экземпляром.

Алиасы id

Каждый rebind оставляет алиас: старый id продолжает находить экземпляр. Это единственный механизм алиасов - никакого addAlias нет, алиас всегда след переименования:

ts
const t = todos.add({ id: "draft-7", title: "x" });

t.rebind("42");

todos.get("draft-7") === t; // true - алиас разрешается
todos.get("42") === t;      // true - настоящий id, всегда сильнее алиаса
t.json().id;                // "42" - алиасы никогда не сериализуются

Правила алиасов:

  • разрешение - только фолбэк: настоящий id в коллекции всегда побеждает алиас с тем же написанием;
  • алиасы живут только в get (и в useModel(todos.get(...))) - никогда в индексах, результатах запросов и связях;
  • алиас умирает вместе со своим экземпляром: таблица хранит живые переименования, а не историю;
  • add матчит только настоящие id; вход, чей id совпал с живым алиасом, забирает это написание себе как настоящий id и снимает алиас с dev-предупреждением;
  • алиасы не образуют цепочек: после rebind("A") и затем rebind("B") и "A", и исходный id указывают сразу на "B" - без обхода цепочки.

Поиск сущности по нескольким идентификаторам - slug и uuid - это не алиас и не второй id. Это второе поле:

ts
const Article = staticModel({
  data: {
    title: f.string(),
    slug: f.string().indexed().unique(),
  },
});

articles.where(Article.slug.eq("intro")).first; // поиск по вторичному ключу

Стабильные ключи списков

instance.key - идентичность для UI-списков:

tsx
{todos.items.map((t) => <Row key={t.key} todo={t} />)}

Это не id: он переживает rebind (строка не ремаунтится, когда сервер переименовал сущность) и не повторяется при переиспользовании слотов (строки разных сущностей никогда не склеиваются).

Загрузка в любом порядке

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

  • id ссылки, загруженный раньше цели, читается как null, а после добавления цели разрешается;
  • пере-add загруженного списка мержит по id - существующие экземпляры обновляются на месте, ссылки на них остаются валидными;
  • массив детей авторитетен: известные id мержатся, новые создаются, отсутствующие уничтожаются, порядок - из массива.

Адаптация чужих форматов

Модель держит одну каноничную форму JSON. Легаси или вторичный формат конвертируется отдельной функцией-адаптером рядом с вызовом API - сама модель о нём не знает:

ts
function fromLegacy(row: LegacyRow) {
  return todos.add({ name: row.title_text, done: row.state === 2 });
}

Контракт

ts
instance.json(): Record<string, unknown>; // временный id опускается
instance.toJSON();                        // алиас для JSON.stringify
instance.rebind(newId: string): void;     // переписывает ссылки, оставляет форвардинг
instance.key: string;                     // стабильная UI-идентичность

collection.get(oldId); // разрешает форвардинг после rebind

Частые кейсы

Используйте этот поток для:

  • оптимистичного создания - add, navigate, save, rebind;
  • PATCH-запросов - json() отредактированного экземпляра;
  • обновления кеша - пере-add загруженного списка поверх живой коллекции;
  • многозапросной загрузки - сущности и ссылки в любом порядке.

Связанные разделы