@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 +21 -0
- package/README.md +135 -2
- package/dist/chart-BWjU2VF4.js +395 -0
- package/dist/compiled.d.ts +58 -0
- package/dist/compiled.js +43 -0
- package/dist/component-DDXOyYUI.d.ts +147 -0
- package/dist/dev-BAz6e3W-.js +79 -0
- package/dist/dom.d.ts +187 -0
- package/dist/dom.js +659 -0
- package/dist/glue/preact.d.ts +4 -0
- package/dist/glue/preact.js +54 -0
- package/dist/glue/react.d.ts +4 -0
- package/dist/glue/react.js +50 -0
- package/dist/glue/solid.d.ts +4 -0
- package/dist/glue/solid.js +42 -0
- package/dist/glue/svelte.svelte.d.ts +4 -0
- package/dist/glue/svelte.svelte.js +57 -0
- package/dist/glue/vanilla.d.ts +4 -0
- package/dist/glue/vanilla.js +83 -0
- package/dist/glue/vue.d.ts +4 -0
- package/dist/glue/vue.js +41 -0
- package/dist/index.d.ts +95 -0
- package/dist/index.js +582 -0
- package/dist/inspect-BmyLUhcy.js +74 -0
- package/dist/inspect-C3qosuCm.d.ts +22 -0
- package/dist/instance-BM2UqeXW.js +418 -0
- package/dist/machine-BstWHtWL.d.ts +335 -0
- package/dist/naming-Csj5186j.js +492 -0
- package/dist/records-7FFepzo9.js +10 -0
- package/dist/source-DdFTPjID.js +15 -0
- package/dist/test.d.ts +63 -0
- package/dist/test.js +262 -0
- package/dist/types-Bk8svtO6.d.ts +580 -0
- package/dist/vicss.d.ts +19 -0
- package/dist/vicss.js +51 -0
- package/dist/vite.d.ts +27 -0
- package/dist/vite.js +4281 -0
- package/package.json +87 -3
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
|
-
#
|
|
1
|
+
# @menapo/machine
|
|
2
2
|
|
|
3
|
-
|
|
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 };
|
package/dist/compiled.js
ADDED
|
@@ -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 };
|