@evgkch/machjs-inspector
v0.0.8
Published
Look at a state machine: three projections of its transition relation, drawn from the graph and driven by two clicks. Reads a dumped schema, or attaches to a machine that is running
Maintainers
Readme
English · Русский
machjs inspector
Инспектор конечных автоматов на @evgkch/machjs. Читает дамп схемы или подключается к работающей машине и показывает три проекции одного автомата: текст правил, фигуру переходов и прогон. Проекции связаны: наводим на ячейку фигуры — подсвечивается строка правила, наводим на строку — ячейка, называем обе половины перехода — он выполняется.
JSON.stringify(machine) возвращает граф — схему без тел функций, но с их именами. По нему машину рисуют, проверяют и прогоняют.
Содержание
| Раздел | О чём |
| -------------------------------------------------------- | ----------------------------------------------------- |
| Установка | Точки входа, peer-зависимости |
| Способы работы | Страница, работающий автомат, встраивание |
| @evgkch/machjs-inspector | inspect, close, RELAY, что идёт по проводу |
| Реле | scripts/relay.mjs, порт |
| @evgkch/machjs-inspector/ui | overlay, mount, ensemble, субъекты, фокус |
| Виджеты | Шесть custom elements и их wiring |
| Схемы | Готовые файлы, язык правил |
| Ограничения | Что важно помнить при использовании |
Установка
Страницы инспектора и реле — WebSocket-сервер, через который отлаживаемое приложение передаёт свои машины странице наблюдения, — запускаются из клона репозитория. В отлаживаемое приложение ставится npm-пакет:
npm i -D @evgkch/machjs-inspectorВ продакшен-сборку вызов inspect не берут:
if (import.meta.env.DEV) inspect(fsm, { name: "cart" });Если виджеты — часть интерфейса страницы, зависимость обычная.
Пакет поставляется только в формате ESM. Собственная зависимость одна — lit, база custom elements. Peer-зависимости — @evgkch/machjs >= 0.2.1 и @evgkch/chanjs >= 1.1.0: в сборку пакета они не входят, и копия machjs у приложения должна быть одна (см. Ограничения).
Точки входа:
| Точка входа | Что в ней |
| -------------- | --------------------------------------------------------------------------- |
| . | inspect, close, RELAY. Ни DOM, ни стилей — подходит серверу и воркеру |
| ./ui | overlay, mount, виджеты, субъекты. Импорт подключает стили |
| ./style.css | Стили светлого DOM: сетка mount и панель overlay |
| ./tokens.css | Палитра и типографика; виджетам обязателен |
Способы работы
Готовая страница. В клоне репозитория:
npm install
npm run devОткроется инспектор: редактор схемы рядом с фигурой. Схему читают из файла — дамп JSON.stringify(machine) или текст на языке правил, — пишут в редакторе или берут из готовых. Кнопка дампа выписывает набранное обратно в JSON.
Работающий автомат. Реле и страница наблюдения запускаются в клоне репозитория:
npm run inspect # реле ws://localhost:8999 и страница наблюденияВ отлаживаемое приложение добавляется одна строка:
import { inspect } from "@evgkch/machjs-inspector";
const cart = inspect(new StateMachine(schema, start), { name: "cart" });На странице появляется cart: его схема, текущее состояние и каждый новый переход. Машин может быть несколько — из одного процесса или из разных.
На своей странице. Инспектор встраивается в отлаживаемую страницу — без реле и второй страницы: целиком через overlay или по одному виджету (см. Виджеты):
import { overlay } from "@evgkch/machjs-inspector/ui";
import "@evgkch/machjs-inspector/style.css";
const gone = overlay(cart);
// …
gone.close();@evgkch/machjs-inspector
Главная точка входа — то, что пишет приложение. Ни документа, ни стилей: у отлаживаемого процесса может не быть DOM.
inspect
function inspect<T extends AnyMachine>(fsm: T, opts?: Options): T;inspect возвращает ту же машину, что получил, — тот же объект с одним слушателем на шине. Вызов оборачивают вокруг готового экземпляра в строке, где тот объявлен, а после отладки стирают.
| Поле опций | Что делает |
| -------------- | -------------------------------------------------------------------- |
| name? | Имя на странице инспектора. Без него — machine N |
| description? | Строка о том, для чего машина |
| history? | Рекордер history(fsm) из @evgkch/machjs/debug — включает отмотку |
| carry? | Слать с шагами payload и контекст — то, что запишет JSON |
| url? | Адрес реле, когда оно не на этом хосте. По умолчанию RELAY |
| link? | Собственный канал вместо сокета; тогда url не используется |
Машины одного процесса делят один сокет на адрес: соединение открывается на первом inspect и закрывается после ухода последней машины. На pagehide каждая машина шлёт bye.
Отмотка
import { history } from "@evgkch/machjs/debug";
const past = history(cart);
inspect(cart, { name: "cart", history: past });Отмотка из окна инспектора двигает машину в её процессе через переданный рекордер. Без history в опциях кнопки отмотки на странице не действуют.
Что идёт по проводу
Только имена: граф — схема, как её пишет JSON.stringify, — и четыре типа каждого перехода (from, on, to, emit). Контекст состояний и payload событий не покидают процесс приложения. С carry: true в шаге ещё payload, достигнутый контекст и payload выходного события — в той мере, в какой их запишет JSON; контексту с несериализуемым содержимым задаётся свой toJSON. Эти данные видны в консоли разработчика на странице инспектора.
| Сообщение | Кто шлёт | Содержимое |
| --------- | ---------- | ---------------------------------------------------------- |
| hello | приложение | Всё о машине: имя, граф, состояние, прогон, возможности |
| step | приложение | Один переход и текущее состояние машины |
| bye | приложение | Машина перестала публиковаться |
| hail | страница | Запрос повторного hello от всех машин |
| jump | страница | Команда рекордеру: перейти к записи |
hello содержит состояние целиком, поэтому страница, открытая позже или пропустившая сообщения, шлёт hail и получает всё. Порядок доставки не важен, реле ничего не хранит.
close
function close(): void;Шлёт bye за все машины и закрывает сокеты. Node-скрипту он нужен: открытый сокет держит event loop, и без вызова процесс не завершится. Вкладке браузера не нужен.
Сигнатуры
function inspect<T extends AnyMachine>(fsm: T, opts?: Options): T;
function close(): void;
const RELAY = "ws://localhost:8999";
type Options = {
name?: string;
description?: string;
history?: Past;
carry?: boolean;
url?: string;
link?: Link;
};
/** Рекордер по форме — подходит `History` из `machjs/debug`. */
type Past = {
readonly index: number;
jump(index: number): boolean;
readonly rx: { on(msg: "moved", hear: (index: number) => void): () => boolean };
};Реле
Реле — WebSocket-сервер (scripts/relay.mjs). К нему подключаются приложение с машинами и страница наблюдения; напрямую они не связаны.
npm run inspect # реле и страница наблюдения вместе
npm run relay # только реле
node scripts/relay.mjs 9001 # свой порт; иначе PORT, иначе 8999Реле раздаёт каждое сообщение всем остальным клиентам; оно не разбирает сообщений и ничего не хранит. Если порт уже занят другим реле, второй экземпляр печатает одну строку и выходит.
@evgkch/machjs-inspector/ui
Вторая точка входа — интерфейс инспектора для встраивания. Импорт модуля подключает tokens.css.
overlay
function overlay(fsm: AnyMachine, options?: OverlayOptions): Overlaid;Показывает работающую машину без реле и без второй страницы: фигура и прогон монтируются на этой же странице.
| Поле опций | Что делает |
| ------------ | -------------------------------------------------------------- |
| into? | Куда монтировать. Без него — плавающая панель поверх страницы |
| title? | Подпись в шапке панели |
| history? | Рекордер — как у inspect, включает отмотку |
| focus? | Общий фокус с чем-то ещё на странице |
Плавающая панель перетаскивается за шапку и закрывается крестиком; Overlaid.close() делает то же из кода и снимает слушателя с машины.
mount
function mount(host: HTMLElement, subject: Subject, options?: ViewOptions): Handle;Собирает в переданном элементе фигуру и прогон и связывает их: прогон листается с клавиатуры (←/→, Home/End, Escape), названное правило выполняется, панели располагаются рядом или столбиком по ширине хоста. ViewOptions — тот же focus.
type Handle = {
readonly update: () => void; // перерисовать: субъект изменился
readonly enroll: (s: Member) => void; // зарегистрировать ещё один виджет
readonly fire: (id: RuleId) => void; // выполнить правило, если оно доступно
readonly destroy: () => void; // отпустить слушателей и DOM
};
/** Виджет, зарегистрированный рядом с парой mount: перерисовывается вместе с ней. */
type Surface = { draw(start: string): void; dress(): void };
/** Виджет со свойством `wiring` проводится при регистрации сам. */
type Member = Surface & { wiring?: { subject: Subject; focus: Focus } };enroll регистрирует виджет на тех же субъекте и фокусе — например <machjs-diagram>; отдельная проводка странице не нужна.
ensemble
function ensemble(subject: Subject, cast: Cast, options?: { focus?: Focus; start?: string }): Ensemble;
type Cast = { figure?: MachjsFigure; history?: MachjsHistory; diagram?: MachjsDiagram };
type Ensemble = {
readonly focus: Focus;
readonly start: string; // от какого состояния считаются ряды
readonly draw: () => void; // перерисовать всех участников
readonly dress: () => void; // обновить подсветку
readonly fire: (id: RuleId) => void; // выполнить правило, если оно доступно
readonly rewind: (step: number) => void; // подвинуть рекордер, сбросить выбор
readonly forget: () => void; // сбросить выбор и указатель
readonly enroll: (s: Member) => void;
readonly destroy: () => void;
};Связка без разметки. Виджеты независимы: каждый подписан на субъект и рисует себя; ensemble проводит участников на общие субъект и фокус и выполняет названное на любом из них правило — один раз, в одном месте. mount построен на ensemble и добавляет сетку, измерение и клавиатуру; страница со своей разметкой вызывает ensemble напрямую.
Субъекты
Всё рисуется с субъекта — интерфейса «граф, где стоим, что происходило, как подвинуть»:
function fromMachine(fsm: AnyMachine, opts?: { history?: Past }): Subject;
function fromText(graph: Graph, start: string): Text;fromMachine читает живую машину. Нажать правило — отправить событие; какое из правил ячейки сработает, определяют условия в приложении.
fromText строит из дампа настоящую машину. Дамп хранит имена условий без кода, поэтому каждому правилу задаётся условие «названо ли именно оно» — названное правило срабатывает.
Фокус
function newFocus(): Focus; // что выбрано и на что наведено — две машины и look()Конечные автоматы на той же библиотеке. Общий focus на странице — общая подсветка.
Виджеты
Панели инспектора публикуются как custom elements. Импорт любого из них регистрирует элемент:
import {
MachjsFigure,
MachjsHistory,
MachjsEditor,
MachjsDiagram,
MachjsLegend,
} from "@evgkch/machjs-inspector/ui";| Элемент | Класс | Что рисует |
| ----------------- | -------------- | ------------------------------------------------------ |
| <machjs-figure> | MachjsFigure | Фигуру переходов: три блока вокруг двух осей |
| <machjs-history> | MachjsHistory | Прогон: шаги по строкам состояний, отмотка кликом |
| <machjs-editor> | MachjsEditor | Текст правил с подсветкой, гуттером и дополнением |
| <machjs-diagram> | MachjsDiagram | Классическую диаграмму: состояния в строку, переходы дугами |
| <machjs-legend> | MachjsLegend | Строку капсул без рамки: kind — states, in или out |
| <machjs-desk> | MachjsDesk | Пульт: меню переключателей и синхронизация остальных |
Виджет настраивают свойством wiring — объектом JavaScript, не атрибутом. Рисует он в shadow root со своим стилем; стили страницы внутрь не проникают. Снаружи читается палитра: токены — custom properties, они наследуются сквозь shadow, поэтому tokens.css обязателен, а переопределение токена меняет и виджет.
Минимальная сборка фигуры с прогоном:
import {
MachjsFigure,
MachjsHistory,
fromMachine,
newFocus,
} from "@evgkch/machjs-inspector/ui";
import "@evgkch/machjs-inspector/tokens.css";
const subject = fromMachine(fsm);
const focus = newFocus();
const forget = () => {
focus.choice.dispatch("drop");
focus.pointer.dispatch("leave");
};
const start = subject.at;
const figure = new MachjsFigure();
figure.wiring = { subject, focus, forget };
host.append(figure);
figure.draw(start); // до draw ничего не нарисовано
const run = new MachjsHistory();
run.wiring = { subject, focus, rewind: (i) => subject.rewind?.(i) };
host.append(run);
run.show(subject.graph, start); // порядок строк
run.draw();draw зовут снова, когда сменился граф; dress — когда сдвинулся только фокус. Названное правило выполняет не одиночный виджет, а связка — mount или ensemble. Если свой порядок панелей не нужен, используйте mount.
<machjs-diagram> подключается так же: wiring = { subject, focus, fire? }, затем draw(start) — или handle.enroll(diagram). fire берёт правило и сбрасывает выбор; ensemble передаёт свой, без него виджет делает это сам. Состояния — ячейки в строку, переходы — дуги: налево — над строкой, направо — под ней, цвет дуги — цвет целевого состояния. Правила, различающиеся только условием, — одна стрелка; правило с другим emit — своя линия. Подпись — on · when / emit в той мере, в какой у правила есть условие и выход: три down из одного состояния различимы по именам условий. Наведение на дугу подсвечивает её правило в фигуре и в тексте; клик по дуге выполняет доступное правило и сбрасывает выбор. Взятый машиной переход пробегает пунктиром по своей дуге — на каждом шаге, чей бы он ни был. Подпись on / emit действует наравне со своей дугой — и на наведение, и на клик. Клик по состоянию — нажатие в общем выборе: на диаграмме остаются только его переходы, в фигуре — полоса его строки, в истории — пунктирные варианты шага; сбрасывается по Escape и вместе с остальными. Нажатие исходящего состояния и затем входящего берёт переход между ними — второй способ взятия, равный клику по дуге; то же состояние дважды — переход в себя. Наведение на входящее при нажатом исходящем рисует в фигуре полосы кандидатов: строку источника, колонку цели, колонки их событий и строки выходов.
<machjs-legend> — строка капсул без рамки и без управления: атрибут kind выбирает states (имена в цветах полос, текущее залито своим цветом, недостижимые зачёркнуты), in или out. Страницы инспектора держат три такие строки над панелями, каждая под своим переключателем.
<machjs-desk> — пульт: виджет, который управляет остальными. Внутри — свой ensemble, в shadow root — меню: по переключателю на каждый зарегистрированный виджет. Виджеты остаются в разметке страницы; переключатель ставит и снимает hidden.
import { MachjsDesk, MachjsDiagram, fromMachine } from "@evgkch/machjs-inspector/ui";
import "@evgkch/machjs-inspector/tokens.css";
const desk = new MachjsDesk();
desk.wiring = { subject: fromMachine(fsm) };
bar.append(desk);
const diagram = new MachjsDiagram();
host.append(diagram);
desk.enroll(diagram); // проводка, отрисовка и переключательИмя переключателя — тег без machjs-; нескольким виджетам одного тега имя задаётся вторым аргументом. desk.seat(имя, { locked?, title? }) — переключатель без проводки, для панели, которую страница показывает и прячет сама; его состояние читается с desk.panels — машины панелей. desk.ensemble — связка (fire, rewind, forget, draw). Меню обеих страниц инспектора — этот же пульт.
<machjs-editor> собирается из readSchema: она возвращает то, что принимает show, — правила с номером строки, цвет каждого состояния и результат проверок.
function readSchema(text: string, keep?: string): Read;
type Read =
| { ok: true; graph: Graph; start: string; rules: readonly Written[] }
| { ok: false; say: string; line: number | null };import {
MachjsEditor,
flaws,
newFocus,
palette,
readSchema,
} from "@evgkch/machjs-inspector/ui";
import "@evgkch/machjs-inspector/tokens.css";
const editor = new MachjsEditor();
// За текстом нет машины: выполнять нечего, текущего состояния нет.
editor.wiring = {
focus: newFocus(),
onEdit: () => paint(),
fires: () => false,
here: () => "",
fire: () => {},
};
host.append(editor);
let start = "";
function paint(): void {
const read = readSchema(editor.text(), start);
// Сообщение и его строка — на нижней полосе редактора.
if (!read.ok) return editor.blame(read.say, read.line);
start = read.start;
editor.blame(null, null);
editor.show(read.rules, palette(read.graph, start), flaws(read.graph, start));
}
editor.set("FROM locked ON coin TO open\nFROM open ON pass TO locked\n");
paint();readSchema читает оба вида текста — язык правил и JSON-дамп — и не бросает исключений. ok — признак разбора: при true возвращаются graph, start и rules, при false — сообщение say и строка line, к которой оно относится; у дампа строк нет, и там line равно null. Второй аргумент — состояние, с которого продолжается прогон: оно сохраняется, пока такое состояние есть в графе, иначе берётся первое состояние графа. onEdit вызывается на каждое нажатие клавиши — страница инспектора ждёт 300 мс и только потом читает текст. fires возвращает, доступно ли правило из текущего состояния, here — имя этого состояния, fire выполняет правило: если передать subject и общий с фигурой focus, клик по метке в гуттере выполняет правило своей строки. Типы экспортируются оттуда же: Read, Shown, Written, Row, Lane и Flaws.
Схемы
В schemas/ лежат девять готовых файлов. Шесть — the-inspectors-* — схемы машин самого инспектора, выписанные из его исходников командой npm run dump. Три написаны вручную: selection-rectangle, upload-with-retry и a-schema-with-problems — последняя показывает, как выглядят мёртвые правила и недостижимые состояния.
Файл читается в двух видах: JSON-дамп и язык правил библиотеки —
FROM locked ON coin WHEN underCap TO locked WITH addCoin
FROM locked ON coin TO open WITH reset EMIT opened
FROM open ON pass TO lockedFROM, ON и TO обязательны, порядок слов фиксирован, комментарии — # или // до конца строки. После круга «текст → схема → дамп» получается та же схема, а не эквивалентная.
Ограничения
- Копия
@evgkch/machjsу приложения должна быть одна. Переходы публикуются по символуTRANSITION, а у второй копии библиотеки символ свой — слушательinspectне срабатывает. При сборке из нескольких бандлов помогаетresolve.dedupeу Vite. - Node-скрипту нужен
close(). Открытый сокет держит event loop; без вызова процесс не завершится. - Отмотка требует переданного рекордера. Без
historyв опцияхinspectкнопки отмотки не действуют. - По проводу идут только имена, если не сказано
carry. Условия дампа не выполняются: страница наблюдения отправляет событие, а какое правило сработает, определяют условия в процессе приложения. Безcarry: trueконтекст иpayloadне покидают приложение. - Виджетам нужен
tokens.css. Палитра проходит в shadow root через custom properties; без токенов виджет остаётся без цветов.
Лицензия
MIT
