Перейти к содержанию
Mercury
Русский
Дизайн книга
Меню
Русский

Горячая замена: смена графа

Правка модели должна сохранять видимое состояние и точно сообщать, что сохранить не удалось. Hypergraph принимает решение для каждого хранилища (store) по sid и описанию структуры (shape) и указывает причину потери каждого значения.

Горячая замена определяет, что происходит с состоянием модели при сохранении правки кода. Правило просто: сохранить значение, если оно по-прежнему относится к тому же хранилищу, и указать причину потери каждого отброшенного значения.

Что реализовано и что запланировано

Механизм замены реализован внутри @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 и строкой журнала, поэтому потеря значения не остаётся незамеченной.