@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.
@@ -0,0 +1,492 @@
1
+ import { r as fail } from "./dev-BAz6e3W-.js";
2
+ import { c as swap, f as DEV, n as hook, p as dev, t as Interpreter, u as bit } from "./instance-BM2UqeXW.js";
3
+ //#region src/parts/compile.ts
4
+ const HANDLER = /^on[A-Z]/;
5
+ const cache = /* @__PURE__ */ new WeakMap();
6
+ /** Splits `machine.parts` for `naming`, once per machine and naming. */
7
+ function compile(machine, naming) {
8
+ let byNaming = cache.get(machine);
9
+ if (!byNaming) cache.set(machine, byNaming = /* @__PURE__ */ new Map());
10
+ let compiled = byNaming.get(naming);
11
+ if (compiled) return compiled;
12
+ const parts = [];
13
+ let size = 0;
14
+ const config = machine.c.parts ?? {};
15
+ for (const name of Object.keys(config)) {
16
+ const statics = [];
17
+ const keys = [];
18
+ const fns = [];
19
+ const on = [];
20
+ const part = config[name];
21
+ for (const key of Object.keys(part)) {
22
+ const value = part[key];
23
+ if (HANDLER.test(key)) on.push([naming(key), value]);
24
+ else if (typeof value === "function") {
25
+ keys.push(naming(key));
26
+ fns.push(value);
27
+ } else statics.push([naming(key), value]);
28
+ }
29
+ const template = Object.fromEntries(statics);
30
+ template.id = void 0;
31
+ template["data-scope"] = machine.c.name ?? "machine";
32
+ template["data-part"] = name;
33
+ template["data-state"] = void 0;
34
+ for (const key of keys) template[key] = void 0;
35
+ for (const [key] of on) template[key] = void 0;
36
+ parts.push({
37
+ name,
38
+ statics,
39
+ keys,
40
+ fns,
41
+ on,
42
+ at: size,
43
+ template
44
+ });
45
+ size += fns.length;
46
+ }
47
+ const record = Object.fromEntries(parts.map((part) => [part.name, void 0]));
48
+ byNaming.set(naming, compiled = {
49
+ parts,
50
+ size,
51
+ record
52
+ });
53
+ return compiled;
54
+ }
55
+ //#endregion
56
+ //#region src/parts/component.ts
57
+ /**
58
+ * A machine rendered by a framework (PLAN §4.4, §4.6, §5.4, §5.5): the
59
+ * interpreter plus its parts, ids, elements and `expose`, and the lifecycle
60
+ * the glue drives. Only the glue creates one, so a machine used without a
61
+ * framework (`start`) ships none of this.
62
+ *
63
+ * - **Parts** are rebuilt only when a field one of their attribute functions
64
+ * read has changed (the change mask, accumulated between renders), and
65
+ * only into a copy of the previous object, which keeps its shape, its
66
+ * static attributes and its handlers (S3: "a precompiled shape"). A part
67
+ * whose values came out the same returns the same object.
68
+ * - **Handlers** are made once per instance, so their identity never changes.
69
+ * - **The view version** (`snapshot`) moves only when a part, `expose` or
70
+ * what the component read through the view itself changed: React renders
71
+ * once per visible change and never for an unhandled event (S3).
72
+ * - **Effects and timers** start once the framework has committed the DOM
73
+ * (D13): never on a server, after the first commit, and after each commit
74
+ * that follows an event entering a state with effects.
75
+ */
76
+ /** `instance.b`: the interpreter's IDLE, BUSY and STOPPED. */
77
+ const IDLE = 0;
78
+ const BUSY = 1;
79
+ const STOPPED = 2;
80
+ /**
81
+ * Elements by part, read from the document when asked: a class per machine.
82
+ * A part not in the document reads `undefined`, as does every part where
83
+ * there is no document (a server render).
84
+ */
85
+ const elementClasses = /* @__PURE__ */ new WeakMap();
86
+ function elements(machine, ids) {
87
+ let Elements = elementClasses.get(machine);
88
+ if (!Elements) {
89
+ class Found {
90
+ $;
91
+ constructor(ids) {
92
+ this.$ = ids;
93
+ }
94
+ }
95
+ for (const part of Object.keys(machine.c.parts ?? {})) Object.defineProperty(Found.prototype, part, { get() {
96
+ return typeof document === "object" ? document.getElementById(this.$[part]) ?? void 0 : void 0;
97
+ } });
98
+ elementClasses.set(machine, Elements = Found);
99
+ }
100
+ return new Elements(ids);
101
+ }
102
+ /**
103
+ * A part's props object copied with its new state: the precompiled shape
104
+ * S3 asked for. Each of a machine's first eight parts is copied at a spread
105
+ * of its own, so V8 sees one shape at each and copies it as a block; one
106
+ * spread for every part sees them all and copies key by key, about three
107
+ * times slower (`bench/machine`'s `props.changed`).
108
+ */
109
+ function copy(p, old, state) {
110
+ let next;
111
+ switch (p) {
112
+ case 0:
113
+ next = { ...old };
114
+ break;
115
+ case 1:
116
+ next = { ...old };
117
+ break;
118
+ case 2:
119
+ next = { ...old };
120
+ break;
121
+ case 3:
122
+ next = { ...old };
123
+ break;
124
+ case 4:
125
+ next = { ...old };
126
+ break;
127
+ case 5:
128
+ next = { ...old };
129
+ break;
130
+ case 6:
131
+ next = { ...old };
132
+ break;
133
+ case 7:
134
+ next = { ...old };
135
+ break;
136
+ default: next = { ...old };
137
+ }
138
+ next["data-state"] = state;
139
+ return next;
140
+ }
141
+ /** The first listener of every component: collects what changed between renders. */
142
+ function collect(instance) {
143
+ instance.cc |= instance.mask;
144
+ }
145
+ /** True when `at` is the leaf or one of its ancestors, or one of a parallel configuration's states. */
146
+ function active(leaf, at) {
147
+ if (leaf.a) return leaf.a.includes(at);
148
+ for (let s = leaf; s; s = s.up) if (s === at) return true;
149
+ return false;
150
+ }
151
+ /**
152
+ * What `useMachine` returns: `state`, `is`, `send`, `context`, `parts`, and
153
+ * a getter for each value `expose` returns (a class per machine adds them).
154
+ * Every getter reads the component's current snapshot, after the
155
+ * framework's tracker (`t`: Vue, Svelte and Solid record the read; React
156
+ * and Preact have none). React and Preact get a new one per view version,
157
+ * since they compare by identity (React's Compiler too); the others keep
158
+ * one for the component's life. Reading `state`, `is` or `context` marks
159
+ * them watched, so a change to them renders even when no part shows it.
160
+ */
161
+ var View = class {
162
+ $;
163
+ t;
164
+ u;
165
+ constructor(component, track, listen) {
166
+ this.$ = component;
167
+ this.t = track;
168
+ this.u = listen;
169
+ }
170
+ get parts() {
171
+ this.t?.();
172
+ return this.$.record();
173
+ }
174
+ get state() {
175
+ this.t?.();
176
+ this.$.wa |= 1;
177
+ return this.$.s.path;
178
+ }
179
+ get is() {
180
+ this.t?.();
181
+ this.$.wa |= 1;
182
+ return this.$.is;
183
+ }
184
+ get context() {
185
+ this.t?.();
186
+ const { $ } = this;
187
+ $.wa |= bit($.M.nm.length - $.M.pn.length) - 1 << 1;
188
+ return $.context;
189
+ }
190
+ get send() {
191
+ return this.$.send;
192
+ }
193
+ setProps(props) {
194
+ this.$.setProps(props);
195
+ }
196
+ /**
197
+ * Stops the instance as an unmount does: its effects and timers, and in
198
+ * plain code its listeners. A framework's glue does it when the component
199
+ * goes; plain code calls it when the element goes.
200
+ */
201
+ stop() {
202
+ if (this.u) this.u.stop();
203
+ else this.$.unmount();
204
+ }
205
+ /** Calls `listener` after each visible change. */
206
+ subscribe(listener) {
207
+ const { $, u } = this;
208
+ if (u) return u.subscribe(listener);
209
+ let seen = $.snapshot();
210
+ return $.subscribe(() => {
211
+ const now = $.snapshot();
212
+ if (now !== seen) {
213
+ seen = now;
214
+ listener();
215
+ }
216
+ });
217
+ }
218
+ };
219
+ const viewClasses = /* @__PURE__ */ new WeakMap();
220
+ /** A view of `component`: {@link View}, with a getter for each key `expose` returns, on a class made once per machine. */
221
+ function viewOf(component, track, listen) {
222
+ let Of = viewClasses.get(component.M);
223
+ if (!Of) {
224
+ Of = class extends View {};
225
+ for (const key of Object.keys(component.exposed() ?? {})) Object.defineProperty(Of.prototype, key, {
226
+ enumerable: true,
227
+ get() {
228
+ this.t?.();
229
+ this.$.snapshot();
230
+ return this.$.exposed()[key];
231
+ }
232
+ });
233
+ viewClasses.set(component.M, Of);
234
+ }
235
+ return new Of(component, track, listen);
236
+ }
237
+ /**
238
+ * Dev only: props given as a function where the framework reads them as an
239
+ * object on each render (React, Preact, plain code), which would ignore them.
240
+ */
241
+ function objectProps(machine, props, framework) {
242
+ if (typeof props === "function") fail("PROPS", machine.c.name ?? "machine", `useMachine was given props as a function, which only Vue, Svelte and Solid read; in ${framework} pass the props object itself, on each render: useMachine(machine, { … }).`);
243
+ }
244
+ /** What the glue's adapters create: a component of `machine`, named for the framework. */
245
+ const component = (machine, naming) => (id, owner) => new Component(machine, owner, id, naming);
246
+ var Component = class extends Interpreter {
247
+ /** The instance id: the framework's (`useId`), or the owner's `id`. */
248
+ id;
249
+ /** The framework's attribute names. */
250
+ nm;
251
+ /** Bits changed since the last snapshot. */
252
+ cc = 0;
253
+ /** Bits the component read through its view (`state`, `is`, `context`). */
254
+ wa = 0;
255
+ /** The view version. */
256
+ pv = 0;
257
+ /** 1 while mounted: effects may start. */
258
+ y = 0;
259
+ /** 1 when effects wait for a commit, which a render must bring. */
260
+ fx = 0;
261
+ /** Effects deferred until the next commit. */
262
+ w;
263
+ /** Parts, once rendered. */
264
+ rd;
265
+ /** `expose`'s last result and the bits it read. */
266
+ xo;
267
+ xr = 0;
268
+ ir;
269
+ er;
270
+ constructor(machine, owner, id, naming) {
271
+ super(machine, void 0, owner);
272
+ this.id = owner?.id ?? id;
273
+ this.nm = naming;
274
+ this.l = [collect];
275
+ }
276
+ /** `data-scope`: the machine's name. */
277
+ get scope() {
278
+ return this.M.c.name ?? "machine";
279
+ }
280
+ /** `ids.<part>`: `<name>:<id>:<part>`, stable for the instance and the same on server and client. */
281
+ get ids() {
282
+ if (!this.ir) {
283
+ const ids = {};
284
+ const prefix = `${this.scope}:${this.id}:`;
285
+ for (const part of Object.keys(this.M.c.parts ?? {})) ids[part] = prefix + part;
286
+ this.ir = ids;
287
+ }
288
+ return this.ir;
289
+ }
290
+ /** `el.<part>`: the part's element, found by its id when read. */
291
+ get el() {
292
+ return this.er ??= elements(this.M, this.ids);
293
+ }
294
+ activate(states) {
295
+ if (this.y) {
296
+ (this.w ??= []).push(...states);
297
+ this.fx = 1;
298
+ }
299
+ }
300
+ exit(at) {
301
+ const w = this.w;
302
+ if (w) {
303
+ const k = w.indexOf(at);
304
+ if (k >= 0) w.splice(k, 1);
305
+ }
306
+ super.exit(at);
307
+ }
308
+ /**
309
+ * Starts what `states` run as one step (D3): what an effect sends waits
310
+ * until every effect has started, then runs to completion and is told,
311
+ * as when the core starts them. Inside a listener (the vanilla glue
312
+ * commits there) the event being handled does that.
313
+ */
314
+ begin(states) {
315
+ if (!states.length || this.b === STOPPED) return;
316
+ if (this.b === BUSY) return super.activate(states);
317
+ this.b = BUSY;
318
+ try {
319
+ super.activate(states);
320
+ this.settle();
321
+ hook?.(this);
322
+ } finally {
323
+ if (this.b === BUSY) {
324
+ this.b = IDLE;
325
+ this.q = this.n = void 0;
326
+ if (DEV) dev.acting = 0;
327
+ }
328
+ }
329
+ }
330
+ /** After the first commit (and again after StrictMode's remount): starts what the active states run. */
331
+ mount() {
332
+ if (this.b === STOPPED && !this.s.fi) this.b = IDLE;
333
+ this.y = 1;
334
+ const chain = [];
335
+ const all = this.s.a;
336
+ if (all) {
337
+ for (const at of all) if (at.af || at.ef) chain.push(at);
338
+ } else for (let at = this.s; at; at = at.up) if (at.af || at.ef) chain.unshift(at);
339
+ this.w = void 0;
340
+ this.begin(chain);
341
+ }
342
+ unmount() {
343
+ this.y = 0;
344
+ this.stop();
345
+ }
346
+ /** After a commit: starts the timers and effects of states entered since, that are still active. */
347
+ commit() {
348
+ const w = this.w;
349
+ if (w && this.y) {
350
+ this.w = void 0;
351
+ this.begin(w.filter((at) => active(this.s, at)));
352
+ }
353
+ }
354
+ /**
355
+ * The view version: brings parts and `expose` up to date, and moves only
356
+ * when something shown changed. Before the first render it does nothing,
357
+ * so creating a component builds no parts.
358
+ */
359
+ snapshot() {
360
+ const changed = this.cc;
361
+ if ((changed || this.fx) && (this.rd || this.xo)) {
362
+ this.cc = 0;
363
+ let moved = this.fx || changed & this.wa;
364
+ this.fx = 0;
365
+ const rd = this.rd;
366
+ if (rd) {
367
+ const { po, pr, pa } = rd;
368
+ const parts = rd.pc.parts;
369
+ const path = this.s.path;
370
+ for (let p = 0; p < parts.length; p++) {
371
+ if (!(pr[p] & changed)) continue;
372
+ const { keys, fns, at } = parts[p];
373
+ const outer = swap(0);
374
+ let dirty = false;
375
+ for (let k = 0; k < fns.length; k++) {
376
+ const value = fns[k](this);
377
+ if (value !== pa[at + k]) {
378
+ pa[at + k] = value;
379
+ dirty = true;
380
+ }
381
+ }
382
+ pr[p] = swap(outer) | 1;
383
+ const old = po[p];
384
+ if (!dirty && old["data-state"] === path) continue;
385
+ const next = copy(p, old, path);
386
+ for (let k = 0; k < keys.length; k++) next[keys[k]] = pa[at + k];
387
+ po[p] = next;
388
+ moved = 1;
389
+ }
390
+ }
391
+ if (this.xr & changed) {
392
+ this.exposed(true);
393
+ moved = 1;
394
+ }
395
+ if (moved) this.pv++;
396
+ }
397
+ return this.pv;
398
+ }
399
+ /** Renders every part the first time: each from its compiled template, then the instance's values. */
400
+ render() {
401
+ this.cc = this.fx = 0;
402
+ const pc = compile(this.M, this.nm);
403
+ this.exposed(true);
404
+ const rd = {
405
+ pc,
406
+ po: Array.from({ length: pc.parts.length }),
407
+ pr: Array.from({ length: pc.parts.length }),
408
+ pa: Array.from({ length: pc.size }),
409
+ rec: void 0,
410
+ rv: -1,
411
+ vo: void 0,
412
+ vv: -1
413
+ };
414
+ const path = this.s.path;
415
+ const ids = this.ids;
416
+ for (let p = 0; p < pc.parts.length; p++) {
417
+ const { name, keys, fns, at, on, template } = pc.parts[p];
418
+ const outer = swap(0);
419
+ for (let k = 0; k < fns.length; k++) rd.pa[at + k] = fns[k](this);
420
+ rd.pr[p] = swap(outer) | 1;
421
+ const object = copy(p, template, path);
422
+ object.id = ids[name];
423
+ for (let k = 0; k < keys.length; k++) object[keys[k]] = rd.pa[at + k];
424
+ for (const [key, handler] of on) object[key] = typeof handler === "string" ? () => this.dispatch(handler) : (event) => handler(this, event);
425
+ rd.po[p] = object;
426
+ }
427
+ return this.rd = rd;
428
+ }
429
+ /** What `expose` returns, computed again when `again` or not yet. */
430
+ exposed(again) {
431
+ const expose = this.M.c.expose;
432
+ if (expose && (again || !this.xo)) {
433
+ const outer = swap(0);
434
+ this.xo = expose(this);
435
+ this.xr = swap(outer);
436
+ if (DEV) {
437
+ for (const key of Object.keys(this.xo ?? {})) if (key in View.prototype || key === "$" || key === "t" || key === "u") fail("EXPOSE_KEY", this.M.name, `expose returns "${key}", which useMachine's result has already; rename it.`);
438
+ }
439
+ }
440
+ return this.xo;
441
+ }
442
+ /** Every part's props object, by part: the same record until one changes. */
443
+ record() {
444
+ const pv = this.snapshot();
445
+ const rd = this.rd ?? this.render();
446
+ if (rd.rv !== pv || !rd.rec) {
447
+ const { parts, record } = rd.pc;
448
+ const out = { ...record };
449
+ for (let p = 0; p < parts.length; p++) out[parts[p].name] = rd.po[p];
450
+ rd.rec = out;
451
+ rd.rv = pv;
452
+ }
453
+ return rd.rec;
454
+ }
455
+ /** The view for the current version, for frameworks that compare by identity: a new one only when something shown changed. */
456
+ view() {
457
+ const pv = this.snapshot();
458
+ const rd = this.rd ?? this.render();
459
+ if (rd.vv !== pv || !rd.vo) {
460
+ rd.vo = viewOf(this);
461
+ rd.vv = pv;
462
+ }
463
+ return rd.vo;
464
+ }
465
+ };
466
+ //#endregion
467
+ //#region ../glue/src/naming.ts
468
+ /** React's names whose DOM name is not their lowercase form. */
469
+ const DOM = {
470
+ htmlFor: "for",
471
+ className: "class",
472
+ onDoubleClick: "onDblClick"
473
+ };
474
+ /** React's name as the DOM spells it, still camel-cased: `htmlFor` is `for`, `onDoubleClick` `onDblClick`. */
475
+ const dom = (key) => DOM[key] ?? key;
476
+ /** React: names as written. */
477
+ const react = (key) => key;
478
+ /** Preact and Solid take React's names but the DOM's attributes, and lowercase an event's name themselves. */
479
+ const jsx = dom;
480
+ /** Svelte and the DOM itself: lowercase (`onclick`, `tabindex`, `for`). */
481
+ const lower = (key) => dom(key).toLowerCase();
482
+ /**
483
+ * Vue derives an event's name from the rest of an `on…` key, hyphenating
484
+ * capitals (`onKeyDown` would listen for `key-down`), so a handler is
485
+ * `onKeydown`; other names stay, but for the DOM's attributes.
486
+ */
487
+ const vue = (key) => {
488
+ const name = dom(key);
489
+ return /^on[A-Z]/.test(name) ? name.slice(0, 3) + name.slice(3).toLowerCase() : name;
490
+ };
491
+ //#endregion
492
+ export { component as a, vue as i, lower as n, objectProps as o, react as r, viewOf as s, jsx as t };
@@ -0,0 +1,10 @@
1
+ //#region src/features/records.ts
2
+ const records = /* @__PURE__ */ new WeakMap();
3
+ const recorded = (instance, history) => records.get(instance)?.get(history);
4
+ function record(instance, history, states) {
5
+ let map = records.get(instance);
6
+ if (!map) records.set(instance, map = /* @__PURE__ */ new Map());
7
+ map.set(history, states);
8
+ }
9
+ //#endregion
10
+ export { recorded as n, records as r, record as t };
@@ -0,0 +1,15 @@
1
+ //#region ../glue/src/source.ts
2
+ /** Equal own values: a render passing equal props does not set them again. */
3
+ function same(a, b) {
4
+ if (a === b) return true;
5
+ if (!a || !b) return false;
6
+ const keys = Object.keys(a);
7
+ if (keys.length !== Object.keys(b).length) return false;
8
+ for (const key of keys) if (!Object.is(a[key], b[key])) return false;
9
+ return true;
10
+ }
11
+ let ids = 0;
12
+ /** An id for frameworks that give none (Svelte, vanilla): unique on the page, not across a server and its client. */
13
+ const nextId = () => `m${++ids}`;
14
+ //#endregion
15
+ export { same as n, nextId as t };
package/dist/test.d.ts ADDED
@@ -0,0 +1,63 @@
1
+ import { _ as InstanceOf, i as AnyMachine, k as StartOptions, n as AnyEvent, u as EventOf } from "./types-Bk8svtO6.js";
2
+ import { t as Step } from "./inspect-C3qosuCm.js";
3
+ //#region src/test/machine.d.ts
4
+ /** What a test machine adds to the instance. */
5
+ interface TestControls {
6
+ /** Moves its clock forward by `ms`, firing the timers due, in time order. */
7
+ advance(ms: number): void;
8
+ /** Fires every pending timer, and those they set, in time order, until none is left. */
9
+ runAll(): void;
10
+ /** Timers set on its clock and not yet fired or cleared. */
11
+ readonly pending: number;
12
+ /** Every event it took, from `machine.start`, with the state after it and what it changed. */
13
+ readonly steps: readonly Step[];
14
+ /** The states it settled in, from the initial one; a state repeated in a row counts once. */
15
+ readonly path: readonly string[];
16
+ /**
17
+ * Sends each event in turn; `machine.after` fires the next pending timer
18
+ * instead. A path from `paths()` replays this way.
19
+ */
20
+ play(events: readonly AnyEvent[]): void;
21
+ }
22
+ type TestMachine<M extends AnyMachine> = InstanceOf<M> & TestControls;
23
+ /**
24
+ * Starts `machine` for a test: events run synchronously, timers wait for
25
+ * `advance` or `runAll`, and every event taken is recorded in `steps` and
26
+ * `path`.
27
+ */
28
+ export declare function testMachine<M extends AnyMachine>(machine: M, options?: StartOptions<M>): TestMachine<M>;
29
+ //#endregion
30
+ //#region src/test/paths.d.ts
31
+ type EventsOf<M extends AnyMachine> = NonNullable<M["types"]>["events"];
32
+ interface PathOptions<M extends AnyMachine> extends StartOptions<M> {
33
+ /** Events in a path at most; 6 when left out. */
34
+ readonly depth?: number;
35
+ /** Paths at most; finding more is an error that says what to narrow. 1000 when left out. */
36
+ readonly limit?: number;
37
+ /**
38
+ * The data to try for events that carry some: each entry is sent as one
39
+ * event (`ADD: [{ by: 1 }, { by: -1 }]`). An event not listed is sent
40
+ * without data.
41
+ */
42
+ readonly events?: { readonly [K in keyof EventsOf<M> & string]?: readonly Omit<EventOf<EventsOf<M>, K>, "type">[]; };
43
+ }
44
+ interface Path {
45
+ /** The events, in order; `machine.after` stands for the next timer firing. */
46
+ readonly events: readonly AnyEvent[];
47
+ /** The state before the first event and after each one. */
48
+ readonly states: readonly string[];
49
+ }
50
+ /**
51
+ * Every path from the initial state that passes no state twice, up to
52
+ * `depth` events. A path ends when it reaches a final state, a state where
53
+ * nothing it can send does anything, `depth`, or a state with context it
54
+ * passed through before; the step back is part of the path, so every
55
+ * transition taken is covered. Events are those the current state handles,
56
+ * plus `machine.after` while a timer is pending. A state is its path and
57
+ * its context and props, compared as JSON, and what its history states
58
+ * recorded. In a parallel configuration the state is every active leaf, and
59
+ * the events tried are those any active state handles.
60
+ */
61
+ export declare function paths<M extends AnyMachine>(machine: M, options?: PathOptions<M>): Path[];
62
+ //#endregion
63
+ export type { Path, PathOptions, Step, TestControls, TestMachine };