Горячая замена определяет, что происходит с состоянием модели при сохранении правки кода. Правило просто: сохранить значение, если оно по-прежнему относится к тому же хранилищу, и указать причину потери каждого отброшенного значения.
Что реализовано и что запланировано
Механизм замены реализован внутри @fiberfx/hypergraph: корень HMR и сравнение, по которому он принимает решение, с 27 собственными векторами проверки соответствия (hmr.json).
Он ещё не экспортируется: точки входа пакета — ., ./node и ./inspect, а @fiberfx/hypergraph/hmr запланирован. mx dev также пока не доставляет замену, поэтому сегодня правка модели перезагружает страницу. Здесь нет импорта, который уже можно использовать: страница описывает поведение замены после появления её доставки через mx dev, как оно сейчас задано ядром и векторами.
Два вида обновления
Обновление передаёт корню изменившиеся модули. Далее выполняется один из двух вариантов:
- Слоты, на месте. Если каждый изменённый модуль отличается от загруженного только телами функций — редьюсера,
map, фильтра, обработчика, — каждая функция получает новое тело. Поколение, изолированная область состояния (scope), её значения, наблюдатели и выполняющиеся эффекты (effect) остаются прежними. - Замена с новым поколением. Любая другая правка: новая или удалённая единица Hypergraph (unit), новое начальное значение, новое ребро. Корень загружает все модули в новое поколение, создаёт там область, восстанавливает только значения, сохраняемые по результату сравнения, фиксирует поколение, сообщает о каждом сбросе и новом хранилище и завершает старое поколение.
Первое обновление — первоначальная загрузка; замена слотов на месте возможна только после неё.
Поколения
Поколение — одна загруженная версия графа модели. Каждая единица, ребро и наблюдатель принадлежат поколению кода, которое их создало.
- Два поколения могут кратко существовать одновременно; затем старое завершается. Завершённое поколение не принимает работу.
- Наблюдатель завершается вместе с поколением создавшего его кода, даже если область продолжает жить. Подписка не переживает объявивший её код.
- Дескриптор
Scopeне меняется. При каждой фиксации действующие области переходят к зафиксированному поколению и переносят то, что могут; неперенесённое сообщается наблюдениемreset(наблюдение).
Какие хранилища участвуют
В решении участвует только изменяемое хранилище с sid. Не участвуют:
- события (event) и эффекты, которым нечего сохранять как значение;
- производные хранилища, заново вычисляемые графом;
- хранилище без sid, которое нельзя найти повторно;
- хранилище с
serialize: 'ignore'по любую сторону правки.
Хранилища, объявленные внутри региона, участвуют на общих основаниях.
Сохранение, создание или сброс
Каждое участвующее хранилище попадает ровно в один из трёх списков, упорядоченных по sid:
| Результат | Условие | Сообщаемая причина |
|---|---|---|
| keep — сохранить | sid присутствует с обеих сторон с той же структурой | нет: значение переносится |
| new — новое | sid есть только в новом поколении | new: информация, ничего не потеряно |
| reset — сбросить | sid присутствует с другой структурой | shape changed from <old> to <new> |
| reset — сбросить | ни одно хранилище нового поколения не использует sid | removed |
| reset — сбросить | прежний sid исчез, и ровно одно новое хранилище имеет его имя | renamed (a hint, not applied: it may now be <new sid>) |
| reset — сбросить | ключ с любой стороны получен из положения в файле | positional |
Три правила требуют отдельного пояснения:
- Изменение начального значения при прежней структуре сохраняет состояние. Если
initсчётчика меняется с0на1, текущее значение счётчика сохраняется. Причины "init changed" нет (векторhmr.keep-literal-change-within-one-shape). - Переименование — только подсказка, она никогда не применяется. Старое значение не переносится к новому sid даже при совпадении структуры. Подсказка относится к прежнему sid, а новый sid отмечается как новый (вектор
hmr.renamed-is-a-hint-never-applied). - Позиционный ключ никогда не сохраняет значение в изменившемся модуле: одинаковое положение не доказывает, что это то же хранилище. Пока хеш модуля от компилятора не передаётся графу времени выполнения, каждый позиционный ключ приводит к сбросу.
Пример правки
Это вектор hmr.reset-shape-changed-names-both-shapes. До правки:
import { store } from "@fiberfx/hypergraph";
export const $count = store(0, { sid: "count" });
export const $user = store(null, { sid: "user" });
После правки начальное значение $count — объект:
import { store } from "@fiberfx/hypergraph";
export const $count = store({}, { sid: "count" });
export const $user = store(null, { sid: "user" });
Пока компилятор не выдаёт структуры, структура хранилища определяется по его начальному значению: runtime:number до правки, runtime:object{} после. Результат сравнения:
- keep:
user; - new: нет;
- reset:
count,shape changed from runtime:number to runtime:object{}.
$user сохраняет прежнее значение; $count начинает с {}, а журнал объясняет причину: hmr: count: shape changed from runtime:number to runtime:object{}.
Добавление поля объекта тоже меняет структуру: {name: ""} имеет структуру runtime:object{name:string}, а {name: "", age: 0} — runtime:object{age:number,name:string} (вектор hmr.reset-on-a-new-field-in-a-literal-object).
Когда обновление отклоняется
Сравнение отклоняет промежуточное представление (IR), по которому нельзя принять решение, с ошибкой DiffError, чьё поле code принимает значения:
duplicate-sid: две единицы используют один sid;no-shape: у участвующего хранилища нет структуры.
Отклонённое обновление завершает созданное им поколение и выбрасывает исключение. Загруженное поколение и его область остаются действующими, а вызывающая сторона перезагружает страницу. Обновление, начатое во время другого обновления, отклоняется таким же образом, а не объединяется с ним.
Как писать модели для горячей замены
- Сохраняйте структуру хранилища, если хотите сохранить его значение: меняйте литерал, а не структуру.
- Для временного состояния, которое нельзя переносить, задавайте
serialize: 'ignore'. - При добавлении поля в хранилище объекта ожидайте сброса: причина укажет обе структуры.
- Читайте причины. Каждая представлена наблюдением
inspectи строкой журнала, поэтому потеря значения не остаётся незамеченной.