Загрузка и сериализация
Экземпляры разбираются из JSON в add и сериализуются обратно в json() через одни и те же декларации полей. Используйте эту страницу при связывании моделей с API: что попадает в JSON, как серверные id заменяют временные и почему по дороге ничего не ломается.
Примеры на этой странице выполняются в активном скоупе - см. Коллекции и экземпляры.
Сериализация
const t = todos.add({ id: "1", title: "написать доку" });
t.json(); // { id: "1", title: "написать доку", done: false }
JSON.stringify(t); // то же - toJSON подключён к json()Что уходит:
- связанные поля под своими ключами JSON, через выходные трансформы;
- ссылки - как id, дети - встроенными объектами;
- локальные поля, инверсные виды и члены - никогда.
Временные id
Экземпляр, созданный без id, получает автогенерированный временный. Локально он работает везде, но не сериализуется никогда:
const t = todos.add({ title: "новая" });
t.id; // "~tmp1" - годится для роутов, get(), ссылок
t.json(); // { title: "новая", done: false } - без id: его назначит серверВ json() попадают только id, пришедшие извне - из входа add или из rebind.
Смена id: rebind
Оптимистичный сценарий создания, от начала до конца:
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 нет, алиас всегда след переименования:
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. Это второе поле:
const Article = staticModel({
data: {
title: f.string(),
slug: f.string().indexed().unique(),
},
});
articles.where(Article.slug.eq("intro")).first; // поиск по вторичному ключуСтабильные ключи списков
instance.key - идентичность для UI-списков:
{todos.items.map((t) => <Row key={t.key} todo={t} />)}Это не id: он переживает rebind (строка не ремаунтится, когда сервер переименовал сущность) и не повторяется при переиспользовании слотов (строки разных сущностей никогда не склеиваются).
Загрузка в любом порядке
Серверные ответы редко приходят в порядке зависимостей - и не обязаны:
- id ссылки, загруженный раньше цели, читается как
null, а после добавления цели разрешается; - пере-
addзагруженного списка мержит по id - существующие экземпляры обновляются на месте, ссылки на них остаются валидными; - массив детей авторитетен: известные id мержатся, новые создаются, отсутствующие уничтожаются, порядок - из массива.
Адаптация чужих форматов
Модель держит одну каноничную форму JSON. Легаси или вторичный формат конвертируется отдельной функцией-адаптером рядом с вызовом API - сама модель о нём не знает:
function fromLegacy(row: LegacyRow) {
return todos.add({ name: row.title_text, done: row.state === 2 });
}Контракт
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загруженного списка поверх живой коллекции; - многозапросной загрузки - сущности и ссылки в любом порядке.
Связанные разделы
- Поля и данные - ключи, дефолты, трансформы, локальные поля.
- Связи - как сериализуются ссылки и дети.
- UI-биндинги -
instance.keyиget(id)в компонентах.