@menapo/machine 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 menapo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,136 @@
1
- # Temporary Holding Version
1
+ # @menapo/machine
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Typed state machines for UI components and app logic. One object describes a
4
+ machine: its states, events and data, what it does while a state is active,
5
+ and the DOM attributes of a component's parts. The same machine runs in
6
+ React, Preact, Vue, Svelte, Solid, plain code, on servers and on Workers, and
7
+ its types come from that object alone.
8
+
9
+ **Status: built, private until 1.0.** The core, its types, parts and the
10
+ glue for six frameworks, the `/dom` helpers, parallel states, history and
11
+ jobs, `/test`, DevTools, build-time compilation and the vicss preset are
12
+ implemented and tested, each framework against an example app; the release
13
+ waits on the owner's review.
14
+
15
+ ```ts title="packages/machine/tests/docs/snippets.test.ts"
16
+ const counter = defineMachine({
17
+ context: { count: 0 },
18
+ events: { INC: null, ADD: payload<{ by: number }>(), TWICE: null },
19
+ initial: "active",
20
+ states: {
21
+ active: {
22
+ on: {
23
+ INC: ({ context }) => void context.count++,
24
+ ADD: ({ context, event }) => void (context.count += event.by), // event: { type: "ADD"; by: number }
25
+ // Sends made while an event is handled run once it is done, before `send`
26
+ // returns; subscribers hear once for all of it.
27
+ TWICE: ({ send }) => (send("INC"), send("INC")),
28
+ },
29
+ },
30
+ },
31
+ });
32
+
33
+ const c = start(counter);
34
+ ```
35
+
36
+ `defineMachine`, `payload` and `start` come from `@menapo/machine`. In a
37
+ component, the same import in every framework:
38
+ `const api = useMachine(dialog, props)`, then
39
+ `<button {...api.parts.trigger}>` ([Getting started](https://github.com/menapo/kit/blob/main/packages/machine/docs/guide/getting-started.md)).
40
+
41
+ ## Why
42
+
43
+ - **Small.** A compiled machine ships tables and only the runtime they use,
44
+ a fraction of Zag's or XState's engine. Parallel states, history, jobs and
45
+ each DOM helper are separate imports: a machine that uses none ships none.
46
+ - **Fast.** Events run synchronously from per-state lookup tables, with no
47
+ microtask and no snapshot per event; an event no state handles allocates
48
+ nothing.
49
+ - **One render per visible change.** A part's props object is rebuilt only
50
+ when what it shows changed, and its handlers never change identity, so
51
+ React renders once to open a dialog (Zag: twice), never for a change no
52
+ part shows, and memoised children of unchanged parts stay put.
53
+ - **Readable, typed from one place.** Functions sit where they are used;
54
+ context is written directly (`context.count++`); each handler gets its own
55
+ event; targets, names and events are checked with "Did you mean", with
56
+ fewer type instantiations and a faster check than XState's.
57
+ - **Controlled values without twins.** `bindable(false, { states })` gives
58
+ the owner `open`, `defaultOpen` and `onOpenChange` with one transition.
59
+ - **Checked and compiled at build time.** With the Vite plugin a mistake in a
60
+ machine fails the build at its line, and the bundle ships tables instead of
61
+ the code that builds them.
62
+ - **The platform first.** Modal overlays use `<dialog>` and `inert`; each
63
+ `/dom` helper is a module of its own.
64
+
65
+ ### In numbers
66
+
67
+ Measured against pinned Zag.js and XState on every change to the machine
68
+ ([benchmarks](https://github.com/menapo/kit/blob/main/bench/README.md#menapomachine));
69
+ these lines are written by that run:
70
+
71
+ <!-- results:machine-numbers -->
72
+
73
+ - **Size, gzip:** a compiled toggle with its engine 1.49 KB (Zag 5.22 KB, XState 11.36 KB); the dialog a React app ships (machine, parts, glue, DOM behaviour) 5.97 KB (Zag 15.57 KB).
74
+ - **Speed, Node / Chromium:** a handled event that writes context and notifies 73 ns / 66 ns (the better of the others 2.6 µs (Zag) / 2.6 µs (XState)); an unhandled one 18 ns / 23 ns, allocating nothing; creating a toggle 124–145 ns / 98–100 ns; opening or closing the dialog with its DOM behaviour in Chromium 15 µs (Zag 113 µs).
75
+
76
+ Measured on Node v22.22.0, Chromium 141.0.7390.37, Intel(R) Xeon(R) Processor @ 2.10GHz (4 cores, load average 1.0 → 1.5).
77
+
78
+ <!-- /results:machine-numbers -->
79
+
80
+ <!-- results:machine-types -->
81
+
82
+ The benchmark's 30-state wizard, written out as a compiled config, type-checks with 35,703 instantiations in 25 ms; XState's takes 74,628 in 108 ms. Zag's is cheaper, 2,027 in 4 ms, because Zag's machine casts its states to a hand-written schema and checks no transition target: not a like-for-like row, so ours is held to XState's (`bench:machine-types`).
83
+
84
+ <!-- /results:machine-types -->
85
+
86
+ ## Install
87
+
88
+ ```sh
89
+ vp add @menapo/machine
90
+ ```
91
+
92
+ The engine needs nothing; components need the Vite plugin (Vite 8.3 or
93
+ newer, Node 22.18 or newer):
94
+
95
+ ```ts title="examples/machine-react/vite.config.ts"
96
+ import { machine } from "@menapo/machine/vite";
97
+ // …
98
+ export default defineConfig(({ isSsrBuild }) => ({
99
+ plugins: [react(), machine(), vicss()],
100
+ ```
101
+
102
+ ## Entries
103
+
104
+ | Entry | What |
105
+ | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
106
+ | `@menapo/machine` | `defineMachine`, `start`, `payload`, `bindable`, `provide`, `useMachine`, `parallel`, `history`, `job`, `inspect` |
107
+ | `@menapo/machine/dom` | DOM behaviour for effects: `dismiss`, `focusTrap`, `hideOthers`, `scrollLock`, `presence`, `popper`, `typeahead`, `liveRegion`, `spread` |
108
+ | `@menapo/machine/test` | `testMachine` (a fake clock, every step recorded) and `paths` |
109
+ | `@menapo/machine/vicss` | `conditions(...machines)`: vicss conditions for every state, `_dialogOpen` |
110
+ | `@menapo/machine/vite` | `machine()`: `useMachine` per framework, build-time compilation, the DevTools panel |
111
+
112
+ ## Guide
113
+
114
+ - [Getting started](https://github.com/menapo/kit/blob/main/packages/machine/docs/guide/getting-started.md)
115
+ - [States and transitions](https://github.com/menapo/kit/blob/main/packages/machine/docs/guide/states.md) ·
116
+ [Context, props and derived values](https://github.com/menapo/kit/blob/main/packages/machine/docs/guide/data.md) ·
117
+ [Events](https://github.com/menapo/kit/blob/main/packages/machine/docs/guide/events.md) ·
118
+ [Effects and DOM behaviour](https://github.com/menapo/kit/blob/main/packages/machine/docs/guide/effects.md) ·
119
+ [Parts and expose](https://github.com/menapo/kit/blob/main/packages/machine/docs/guide/parts.md)
120
+ - [Frameworks](https://github.com/menapo/kit/blob/main/packages/machine/docs/guide/frameworks.md) ·
121
+ [Servers and app logic](https://github.com/menapo/kit/blob/main/packages/machine/docs/guide/servers.md) ·
122
+ [Server-side rendering](https://github.com/menapo/kit/blob/main/packages/machine/docs/guide/ssr.md)
123
+ - [Parallel states, history and jobs](https://github.com/menapo/kit/blob/main/packages/machine/docs/guide/statecharts.md) ·
124
+ [Build-time compilation](https://github.com/menapo/kit/blob/main/packages/machine/docs/guide/compilation.md) ·
125
+ [Testing](https://github.com/menapo/kit/blob/main/packages/machine/docs/guide/testing.md) ·
126
+ [DevTools](https://github.com/menapo/kit/blob/main/packages/machine/docs/guide/devtools.md) ·
127
+ [TypeScript](https://github.com/menapo/kit/blob/main/packages/machine/docs/guide/typescript.md)
128
+ - [Performance and size](https://github.com/menapo/kit/blob/main/packages/machine/docs/guide/performance.md) ·
129
+ [Coming from Zag or XState](https://github.com/menapo/kit/blob/main/packages/machine/docs/guide/coming-from.md)
130
+
131
+ The design, the budgets and what each milestone measured are in
132
+ [PLAN.md](https://github.com/menapo/kit/blob/main/packages/machine/docs/PLAN.md).
133
+
134
+ ## License
135
+
136
+ MIT
@@ -0,0 +1,395 @@
1
+ import { r as run, u as bit } from "./instance-BM2UqeXW.js";
2
+ import { n as recorded, t as record } from "./records-7FFepzo9.js";
3
+ //#region src/core/props.ts
4
+ /** Marks a `bindable()` value and carries its {@link Control}. */
5
+ const BOUND = Symbol();
6
+ /**
7
+ * Records each declared prop's default, and hands a `bindable()` one to its
8
+ * {@link Control}: what the normaliser does, and a compiled machine at load.
9
+ */
10
+ function bind(tables, props) {
11
+ tables.pn.forEach((prop, j) => {
12
+ const value = props[prop];
13
+ const control = value?.[BOUND];
14
+ tables.pd.push(control ? value.value : value);
15
+ if (control) (tables.k = control).bind(tables, j, prop, value, tables.Q.prototype);
16
+ else tables.pl.push(j);
17
+ });
18
+ }
19
+ /**
20
+ * Takes the owner's props; a missing plain prop falls back to its default.
21
+ * From `setProps` (`queue`): taken once the event being handled is done,
22
+ * as one event of their own when none is, never once stopped.
23
+ */
24
+ function own(instance, owner, queue) {
25
+ if (queue) {
26
+ if (instance.b !== 2) {
27
+ instance.n = owner;
28
+ if (!instance.b) instance.go(void 0, instance.event);
29
+ }
30
+ return;
31
+ }
32
+ const { M, props } = instance;
33
+ const previous = instance.o;
34
+ instance.o = owner;
35
+ for (const j of M.pl) {
36
+ const given = owner[M.pn[j]];
37
+ const value = given === void 0 ? M.pd[j] : given;
38
+ if (props.v[j] !== value) {
39
+ props.v[j] = value;
40
+ props.m |= bit(1 + M.nm.length - M.pn.length + j);
41
+ }
42
+ }
43
+ if (instance.s) M.k?.own(instance, previous);
44
+ }
45
+ //#endregion
46
+ //#region src/core/effects.ts
47
+ const AFTER = { type: "machine.after" };
48
+ /** An `after` timer: setTimeout's extra arguments spare a closure per timer. */
49
+ function fire(instance, rules) {
50
+ if (!instance.b) instance.go(rules, AFTER);
51
+ }
52
+ /** `[state, cleanup or timer, …]`: what an instance keeps while its states are active. */
53
+ const keep = (instance, at, item) => void (instance.r ??= []).push(at, item);
54
+ /** `instance.b` once stopped (the interpreter's STOPPED). */
55
+ const STOPPED = 2;
56
+ /**
57
+ * Sets the timers and starts the effects of `states`, entered in the event
58
+ * that just settled and still active, while the instance is busy: what an
59
+ * effect sends runs once all have started. The glue's component calls it
60
+ * only once the framework has committed the DOM, and never on a server.
61
+ *
62
+ * An effect that stops the instance ends it: nothing after it starts, and
63
+ * the cleanup it returns runs at once. An effect that throws does not keep
64
+ * the others from starting: the event runs to completion and tells its
65
+ * subscribers, then the first error is thrown.
66
+ */
67
+ function activate(instance, states) {
68
+ let thrown;
69
+ for (const at of states) {
70
+ if (instance.b === STOPPED) break;
71
+ if (at.af) for (const [ms, rules] of at.af) keep(instance, at, setTimeout(fire, ms, instance, rules));
72
+ if (at.ef) for (const effect of at.ef) {
73
+ if (instance.b === STOPPED) break;
74
+ try {
75
+ const cleanup = effect(instance);
76
+ if (typeof cleanup === "function") {
77
+ if (instance.b === STOPPED) cleanup();
78
+ else keep(instance, at, cleanup);
79
+ }
80
+ } catch (error) {
81
+ thrown ??= [error];
82
+ }
83
+ }
84
+ }
85
+ if (thrown) {
86
+ instance.settle();
87
+ throw thrown[0];
88
+ }
89
+ }
90
+ /**
91
+ * Stops what `instance` keeps for state `at`, latest first, and forgets it
92
+ * if it waits to start; without `at` (the instance stops), everything. A
93
+ * cleanup that throws does not keep the others from running: the first
94
+ * error is thrown once all have.
95
+ */
96
+ function release(instance, at) {
97
+ const { r, x } = instance;
98
+ let thrown;
99
+ if (!at) instance.r = instance.x = void 0;
100
+ else x?.splice(x.indexOf(at) >>> 0, 1);
101
+ if (r) {
102
+ for (let k = r.length - 2; k >= 0; k -= 2) if (!at || r[k] === at) {
103
+ const item = r.splice(k, 2)[1];
104
+ try {
105
+ if (typeof item === "function") item();
106
+ else clearTimeout(item);
107
+ } catch (error) {
108
+ thrown ??= [error];
109
+ }
110
+ }
111
+ }
112
+ if (thrown) throw thrown[0];
113
+ }
114
+ //#endregion
115
+ //#region src/features/resume.ts
116
+ function wireHistory(tables) {
117
+ const states = tables.ss;
118
+ const histories = states.filter((s) => s.h !== void 0);
119
+ /** History state → the state its parent enters when nothing is recorded yet: its initial child. */
120
+ const initial = /* @__PURE__ */ new Map();
121
+ for (const h of histories) {
122
+ const parent = h.up;
123
+ let child = parent.lf;
124
+ while (child.up !== parent) child = child.up;
125
+ initial.set(h, child);
126
+ const deep = h.h;
127
+ const keep = (view) => {
128
+ const leaf = view.s;
129
+ let child = leaf;
130
+ while (child.up !== parent) child = child.up;
131
+ record(view, h, [deep ? leaf : child]);
132
+ };
133
+ parent.ex = [keep, ...parent.ex ?? []];
134
+ }
135
+ const all = /* @__PURE__ */ new Set();
136
+ for (const state of [states[0].up, ...states]) {
137
+ for (const handler of state.on.values()) for (const rule of handler.ru) all.add(rule);
138
+ for (const rule of state.al ?? []) all.add(rule);
139
+ for (const [, rules] of state.af ?? []) for (const rule of rules) all.add(rule);
140
+ }
141
+ for (const rule of all) {
142
+ const h = rule.to;
143
+ if (!h || !initial.has(h)) continue;
144
+ const guard = rule.if;
145
+ const routed = rule;
146
+ routed.h = h;
147
+ routed.if = (view) => (!guard || guard(view)) && !!(routed.to = recorded(view, h)?.[0] ?? initial.get(h));
148
+ }
149
+ }
150
+ //#endregion
151
+ //#region src/features/chart.ts
152
+ /**
153
+ * The runtime of `type: parallel` (PLAN §4.9, D12): what a machine with
154
+ * parallel states runs on, wired by {@link wireParallel} once its tables are
155
+ * prepared (`parallel.ts`, which the normaliser runs, or the plugin at build
156
+ * time for a compiled machine, M7). Only a machine that declares a parallel
157
+ * state ships this module.
158
+ *
159
+ * The core keeps one leaf (`instance.s`). In a machine with parallel
160
+ * states that leaf is a configuration: an object shaped like a state, one
161
+ * per set of active states, made once per machine and reused, whose `path`
162
+ * lists the active leaves, whose `on` holds every event an active state
163
+ * handles, and whose `up` chain names every active state for `is()`. Each
164
+ * of its handlers is one rule whose guard selects the transitions (SCXML's
165
+ * selection: per active leaf in document order, its own handler first,
166
+ * then its ancestors'; of two whose exits overlap, the one declared deeper,
167
+ * else the earlier) and whose action takes them all as one microstep:
168
+ * exits in reverse document order, the transitions' actions, entries in
169
+ * document order. The core's own `move` is never used; it runs the queue,
170
+ * `always`, timers, effects and notifications as for any machine.
171
+ *
172
+ * Where a transition's exits stop is the core's rule: at the nearest common
173
+ * ancestor of the leaf it was found from and the target's parent, so a
174
+ * transition into a state's own subtree never exits that state; when that
175
+ * ancestor is parallel, the next one up, so every region is left together.
176
+ *
177
+ * A region whose active child is a `type: "final"` state is done; when
178
+ * every region of a parallel state is, the state's `done` transition is
179
+ * taken after the event that finished it (an internal `machine.done`).
180
+ * History states inside parallel machines are handled here too, with the
181
+ * records `history` keeps.
182
+ */
183
+ const DONE = { type: "machine.done" };
184
+ /** Each parallel machine's chart, by its tables. */
185
+ const charts = /* @__PURE__ */ new WeakMap();
186
+ /** `at` strictly inside `up`: depth-first numbering puts a subtree in one range. */
187
+ const inside = (at, up) => at.i > up.i && at.i <= up.e;
188
+ /** What a leaf takes events through: a final state in a region has none of its own, its region's apply. */
189
+ const handlersOf = (chart, leaf) => leaf.fi && leaf.up !== chart.r ? leaf.up.on : leaf.on;
190
+ const configOf = (instance) => instance.s;
191
+ /** The configuration of these active states (document order), made once per machine. */
192
+ function configuration(chart, a) {
193
+ const key = a.map((s) => s.i).join();
194
+ let made = chart.m.get(key);
195
+ if (!made) {
196
+ const l = a.filter((s, k) => !(a[k + 1] && inside(a[k + 1], s)));
197
+ let up;
198
+ for (const s of a) up = {
199
+ path: s.path,
200
+ up
201
+ };
202
+ const on = /* @__PURE__ */ new Map();
203
+ for (const leaf of l) for (const [type, handler] of handlersOf(chart, leaf)) if (!on.has(type)) on.set(type, {
204
+ ev: handler.ev,
205
+ ru: chart.e
206
+ });
207
+ if (a.some((s) => s.dn)) on.set(DONE.type, {
208
+ ev: DONE,
209
+ ru: chart.f
210
+ });
211
+ chart.m.set(key, made = {
212
+ i: -2,
213
+ path: l.map((s) => s.path).join(" "),
214
+ up,
215
+ fi: l.length === 1 && l[0].fi && l[0].up === chart.r,
216
+ on,
217
+ al: l.some((s) => s.al) ? chart.a : void 0,
218
+ a,
219
+ l
220
+ });
221
+ }
222
+ return made;
223
+ }
224
+ /**
225
+ * The transitions a guard selected, read by the action right after it: the
226
+ * core runs a rule's action as soon as its guard passed (`take`), so one
227
+ * module binding is enough.
228
+ */
229
+ let picked = [];
230
+ /**
231
+ * Keeps the transitions found (rule, leaf, …) that SCXML would: one per
232
+ * rule, and of two whose exits overlap the one declared deeper, else the
233
+ * earlier. True when any is left, kept in {@link picked}.
234
+ */
235
+ function choose(instance, found) {
236
+ const taken = [];
237
+ next: for (let k = 0; k < found.length; k += 2) {
238
+ const rule = found[k];
239
+ const leaf = found[k + 1];
240
+ if (taken.some((t) => t[0] === rule)) continue;
241
+ let domain;
242
+ if (rule.to) {
243
+ domain = leaf;
244
+ let b = rule.to.up;
245
+ while (domain !== b) if (domain.i > b.i) domain = domain.up;
246
+ else b = b.up;
247
+ while (domain.p) domain = domain.up;
248
+ }
249
+ const exits = domain ? configOf(instance).a.filter((s) => inside(s, domain)) : [];
250
+ for (let j = 0; j < taken.length; j++) {
251
+ const other = taken[j];
252
+ if (!exits.some((s) => other[2].includes(s))) continue;
253
+ if (inside(rule.s ?? leaf, other[0].s ?? leaf)) taken.splice(j--, 1);
254
+ else continue next;
255
+ }
256
+ taken.push([
257
+ rule,
258
+ domain,
259
+ exits
260
+ ]);
261
+ }
262
+ picked = taken;
263
+ return taken.length > 0;
264
+ }
265
+ /** Pushes the first rule of `rules` whose guard passes, found from `leaf`. */
266
+ function first(instance, rules, leaf, found) {
267
+ for (const rule of rules) if (!rule.if || rule.if(instance)) return found.push(rule, leaf);
268
+ }
269
+ /** Per active leaf, its first transition for the event being handled, or (`always`) its first eventless one. */
270
+ function pick(chart, instance, always) {
271
+ const found = [];
272
+ for (const leaf of configOf(instance).l) {
273
+ const rules = always ? leaf.al : handlersOf(chart, leaf).get(instance.event.type)?.ru;
274
+ if (rules) first(instance, rules, leaf, found);
275
+ }
276
+ return choose(instance, found);
277
+ }
278
+ /** A state's own rules (a timer's, a `done`): taken from the first active leaf inside it. */
279
+ function pickFrom(instance, rules, state) {
280
+ const { a, l } = configOf(instance);
281
+ const found = [];
282
+ if (a.includes(state)) first(instance, rules, l.find((s) => s === state || inside(s, state)), found);
283
+ return choose(instance, found);
284
+ }
285
+ /** A rule whose guard picks the transitions and whose action takes them. */
286
+ const routed = (pick) => ({
287
+ to: void 0,
288
+ if: pick,
289
+ do: [step]
290
+ });
291
+ /** The action of every configuration rule: the microstep of what its guard picked. */
292
+ const step = (view) => {
293
+ const instance = view;
294
+ const taken = picked;
295
+ const { a, l } = configOf(instance);
296
+ const exits = /* @__PURE__ */ new Set();
297
+ for (const t of taken) for (const s of t[2]) exits.add(s);
298
+ for (const s of exits) for (const h of s.hs) record(instance, h, h.h ? l.filter((leaf) => inside(leaf, s)) : a.filter((c) => c.up === s));
299
+ for (const s of [...exits].sort((x, y) => y.i - x.i)) instance.exit(s);
300
+ for (const t of taken) run(t[0].do, instance);
301
+ const enter = /* @__PURE__ */ new Set();
302
+ for (const [rule, domain] of taken) if (rule.to) {
303
+ descend(instance, rule.to, enter);
304
+ ascend(instance, rule.to, domain, enter);
305
+ }
306
+ settle(instance, a.filter((s) => !exits.has(s)), enter);
307
+ };
308
+ /** Adds `state` and what entering it enters (SCXML's addDescendantStatesToEnter). */
309
+ function descend(instance, state, enter) {
310
+ if (state.h !== void 0) {
311
+ const parent = state.up;
312
+ const states = recorded(instance, state) ?? (parent.p ? parent.k : [parent.n]);
313
+ for (const s of states) descend(instance, s, enter);
314
+ for (const s of states) ascend(instance, s, parent, enter);
315
+ } else {
316
+ enter.add(state);
317
+ if (state.p) for (const region of state.k) regions(instance, region, enter);
318
+ else if (state.n) descend(instance, state.n, enter);
319
+ }
320
+ }
321
+ /** Enters `region` by default unless a state in it is entered already. */
322
+ function regions(instance, region, enter) {
323
+ for (const s of enter) if (s === region || inside(s, region)) return;
324
+ descend(instance, region, enter);
325
+ }
326
+ /** Adds the ancestors of `state` below `stop`, and the other regions of the parallel ones. */
327
+ function ascend(instance, state, stop, enter) {
328
+ for (let up = state.up; up !== stop && up.i >= 0; up = up.up) {
329
+ enter.add(up);
330
+ if (up.p) for (const region of up.k) regions(instance, region, enter);
331
+ }
332
+ }
333
+ /**
334
+ * Enters the new states in document order, each added to the configuration
335
+ * just before its entry actions run (so `is()` there sees it and its
336
+ * ancestors, not what is entered after it, as in the core): their timers
337
+ * and effects start once the event settles, and a final state that
338
+ * completes a parallel state queues that state's `done`.
339
+ */
340
+ function settle(instance, kept, enter) {
341
+ const chart = charts.get(instance.M);
342
+ let active = kept;
343
+ instance.s = configuration(chart, active);
344
+ for (const s of [...enter].sort((x, y) => x.i - y.i)) {
345
+ active = [...active, s].sort((x, y) => x.i - y.i);
346
+ instance.s = configuration(chart, active);
347
+ instance.d = 1;
348
+ run(s.en, instance);
349
+ if (s.af || s.ef) (instance.x ??= []).push(s);
350
+ const done = s.fi && s.up.up?.dn;
351
+ if (done && !instance.q?.includes(done.event) && complete(instance, s.up.up)) {
352
+ const q = instance.q ??= [];
353
+ let k = 0;
354
+ while (k < q.length && chart.d.has(q[k])) k++;
355
+ q.splice(k, 0, done.event);
356
+ }
357
+ }
358
+ }
359
+ /** SCXML's isInFinalState: a compound state whose active child is final, a parallel one whose regions all are. */
360
+ const complete = (instance, state) => state.p ? state.k.every((region) => complete(instance, region)) : configOf(instance).a.some((s) => s.up === state && s.fi);
361
+ /** The runtime of a machine whose tables `parallel.ts` prepared: its chart, its timers' rules, its first configuration. */
362
+ function wireParallel(tables) {
363
+ const states = tables.ss;
364
+ const root = states[0].up;
365
+ const chart = {
366
+ r: root,
367
+ m: /* @__PURE__ */ new Map(),
368
+ d: /* @__PURE__ */ new Map(),
369
+ e: [routed((view) => pick(chart, view))],
370
+ a: [routed((view) => pick(chart, view, true))],
371
+ f: [routed((view) => {
372
+ const state = chart.d.get(view.event);
373
+ return !!state && complete(view, state) && pickFrom(view, state.dn.rules, state);
374
+ })]
375
+ };
376
+ charts.set(tables, chart);
377
+ for (const s of [root, ...states]) {
378
+ if (s.af) s.af = s.af.map(([ms, list]) => [ms, [{
379
+ ...routed((view) => pickFrom(view, list, s)),
380
+ w: list
381
+ }]]);
382
+ if (s.dn) chart.d.set(s.dn.event, s);
383
+ }
384
+ tables.i0 = {
385
+ i: -2,
386
+ up: root,
387
+ en: [(view) => {
388
+ const enter = /* @__PURE__ */ new Set();
389
+ descend(view, root.n, enter);
390
+ settle(view, [], enter);
391
+ }]
392
+ };
393
+ }
394
+ //#endregion
395
+ export { release as a, own as c, activate as i, wireParallel as n, BOUND as o, wireHistory as r, bind as s, DONE as t };
@@ -0,0 +1,58 @@
1
+ import { c as derivedClass, l as storeClass, n as Tables, o as Interpreter, t as State } from "./machine-BstWHtWL.js";
2
+ //#region src/core/effects.d.ts
3
+ /**
4
+ * Sets the timers and starts the effects of `states`, entered in the event
5
+ * that just settled and still active, while the instance is busy: what an
6
+ * effect sends runs once all have started. The glue's component calls it
7
+ * only once the framework has committed the DOM, and never on a server.
8
+ *
9
+ * An effect that stops the instance ends it: nothing after it starts, and
10
+ * the cleanup it returns runs at once. An effect that throws does not keep
11
+ * the others from starting: the event runs to completion and tells its
12
+ * subscribers, then the first error is thrown.
13
+ */
14
+ export declare function activate(instance: Interpreter, states: readonly State[]): void;
15
+ /**
16
+ * Stops what `instance` keeps for state `at`, latest first, and forgets it
17
+ * if it waits to start; without `at` (the instance stops), everything. A
18
+ * cleanup that throws does not keep the others from running: the first
19
+ * error is thrown once all have.
20
+ */
21
+ export declare function release(instance: Interpreter, at?: State): void;
22
+ //#endregion
23
+ //#region src/core/props.d.ts
24
+ type Owner = Readonly<Record<string, unknown>>;
25
+ /**
26
+ * Records each declared prop's default, and hands a `bindable()` one to its
27
+ * {@link Control}: what the normaliser does, and a compiled machine at load.
28
+ */
29
+ export declare function bind(tables: Tables, props: Owner): void;
30
+ /**
31
+ * Takes the owner's props; a missing plain prop falls back to its default.
32
+ * From `setProps` (`queue`): taken once the event being handled is done,
33
+ * as one event of their own when none is, never once stopped.
34
+ */
35
+ export declare function own(instance: Interpreter, owner: Owner, queue?: boolean): void;
36
+ //#endregion
37
+ //#region src/features/chart.d.ts
38
+ /** The runtime of a machine whose tables `parallel.ts` prepared: its chart, its timers' rules, its first configuration. */
39
+ export declare function wireParallel(tables: Tables): void;
40
+ //#endregion
41
+ //#region src/features/resume.d.ts
42
+ export declare function wireHistory(tables: Tables): void;
43
+ //#endregion
44
+ //#region src/compiled.d.ts
45
+ /**
46
+ * Development only: what the normaliser adds in development (the name for
47
+ * messages, every event some state handles, frozen context defaults) and
48
+ * checks (its parts), which a compiled machine works out at load instead of
49
+ * carrying it.
50
+ */
51
+ export declare function devTables(tables: Tables): void;
52
+ /**
53
+ * Development only: a value the plugin took for a function without seeing
54
+ * it (an imported name, a call's result), checked when the machine loads.
55
+ */
56
+ export declare function fn<T>(value: T, where: string, name: string): T;
57
+ //#endregion
58
+ export { derivedClass, storeClass };
@@ -0,0 +1,43 @@
1
+ import { n as checkParts, r as fail } from "./dev-BAz6e3W-.js";
2
+ import { f as DEV, o as derivedClass, s as storeClass } from "./instance-BM2UqeXW.js";
3
+ import { a as release, c as own, i as activate, n as wireParallel, r as wireHistory, s as bind } from "./chart-BWjU2VF4.js";
4
+ //#region src/compiled.ts
5
+ /**
6
+ * `@menapo/machine/compiled`: what a machine the plugin compiled at build
7
+ * time (PLAN §5.6, M7) runs on, and the only module its emitted code
8
+ * imports. Internal: no app imports it, as vi18n's `/engine` (PLAN §5.1).
9
+ *
10
+ * The plugin emits a machine's tables (the shape the runtime normaliser
11
+ * produces) and imports from here only what they use: the stores of
12
+ * context, props and derived values, binding props, the timers and effects
13
+ * of states, and the runtimes of parallel states and history. Nothing here
14
+ * imports the normaliser, so an app whose machines are all compiled ships
15
+ * none of it.
16
+ */
17
+ /**
18
+ * Development only: what the normaliser adds in development (the name for
19
+ * messages, every event some state handles, frozen context defaults) and
20
+ * checks (its parts), which a compiled machine works out at load instead of
21
+ * carrying it.
22
+ */
23
+ function devTables(tables) {
24
+ const handled = /* @__PURE__ */ new Set();
25
+ for (const state of [tables.ss[0]?.up, ...tables.ss]) for (const type of state?.on.keys() ?? []) handled.add(type);
26
+ const name = tables.c.name ?? "machine";
27
+ Object.assign(tables, {
28
+ name,
29
+ handled
30
+ });
31
+ checkParts(tables.c, name, handled);
32
+ for (const value of tables.df ?? []) if (Array.isArray(value) || Object.getPrototypeOf(value ?? 0) === Object.prototype) Object.freeze(value);
33
+ }
34
+ /**
35
+ * Development only: a value the plugin took for a function without seeing
36
+ * it (an imported name, a call's result), checked when the machine loads.
37
+ */
38
+ function fn(value, where, name) {
39
+ if (DEV && typeof value !== "function") fail("NOT_A_FUNCTION", name, `${where} is ${JSON.stringify(value)}, which the build took for a function; write a target or a name as a string literal in the config, or a function, so the plugin can compile it.`);
40
+ return value;
41
+ }
42
+ //#endregion
43
+ export { activate, bind, derivedClass, devTables, fn, own, release, storeClass, wireHistory, wireParallel };