@doeixd/machine 0.0.5 → 0.0.7

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/README.md CHANGED
@@ -499,6 +499,68 @@ import { next } from "@doeixd/machine";
499
499
  const updated = next(counter, (ctx) => ({ count: ctx.count + 1 }));
500
500
  ```
501
501
 
502
+ ### Transition Binding Helpers
503
+
504
+ These utilities eliminate the need for `.call(m.context, ...)` boilerplate when invoking transitions.
505
+
506
+ #### `call<C, F>(fn, context, ...args)`
507
+
508
+ Explicitly binds a transition function to a context and invokes it. Useful when you need to call a transition with proper `this` binding.
509
+
510
+ ```typescript
511
+ import { call } from "@doeixd/machine";
512
+
513
+ type MyContext = { count: number };
514
+ const increment = function(this: MyContext) {
515
+ return { count: this.count + 1 };
516
+ };
517
+
518
+ const result = call(increment, { count: 5 }); // Returns { count: 6 }
519
+
520
+ // Particularly useful with generator-based flows:
521
+ const result = run(function* (m) {
522
+ m = yield* step(call(m.increment, m.context));
523
+ m = yield* step(call(m.add, m.context, 5));
524
+ return m;
525
+ }, counter);
526
+ ```
527
+
528
+ #### `bindTransitions<M>(machine)`
529
+
530
+ Returns a Proxy that automatically binds all transition methods to the machine's context. Eliminates `.call(m.context, ...)` boilerplate entirely.
531
+
532
+ ```typescript
533
+ import { bindTransitions } from "@doeixd/machine";
534
+
535
+ const counter = bindTransitions(createMachine(
536
+ { count: 0 },
537
+ {
538
+ increment(this: { count: number }) {
539
+ return createMachine({ count: this.count + 1 }, this);
540
+ },
541
+ add(this: { count: number }, n: number) {
542
+ return createMachine({ count: this.count + n }, this);
543
+ }
544
+ }
545
+ ));
546
+
547
+ // All methods are automatically bound - no need for .call()!
548
+ const next = counter.increment(); // Works!
549
+ const result = counter.add(5); // Works!
550
+
551
+ // Great for generator-based flows:
552
+ const result = run(function* (m) {
553
+ m = yield* step(m.increment()); // Clean syntax!
554
+ m = yield* step(m.add(5)); // No .call() needed
555
+ return m;
556
+ }, counter);
557
+ ```
558
+
559
+ **How it works:**
560
+ The Proxy intercepts all property access on the machine. When a property is a function (transition method), it wraps it to automatically call `.apply(machine.context, args)` before invoking. Non-callable properties are returned as-is.
561
+
562
+ **Note:** The Proxy preserves type safety while providing ergonomic syntax. Use this when writing generator-based flows or any code that frequently calls transitions.
563
+
502
564
  #### `matchMachine<M, K, R>(machine, key, handlers)`
503
565
 
504
566
  Type-safe pattern matching on discriminated unions in context.
@@ -1673,6 +1735,98 @@ const charts = extractMachines({
1673
1735
  });
1674
1736
  ```
1675
1737
 
1738
+ ### Advanced Patterns: Hierarchical and Parallel Machines
1739
+
1740
+ **NEW**: The extractor now supports advanced state machine patterns for complex systems.
1741
+
1742
+ #### Hierarchical (Nested States)
1743
+
1744
+ Model parent states containing child states:
1745
+
1746
+ ```typescript
1747
+ const config: MachineConfig = {
1748
+ input: 'src/dashboard.ts',
1749
+ classes: ['Dashboard', 'ErrorState'],
1750
+ id: 'dashboard',
1751
+ initialState: 'Dashboard',
1752
+ children: {
1753
+ contextProperty: 'child',
1754
+ initialState: 'ViewingMachine',
1755
+ classes: ['ViewingMachine', 'EditingMachine']
1756
+ }
1757
+ };
1758
+ ```
1759
+
1760
+ Generates:
1761
+
1762
+ ```json
1763
+ {
1764
+ "id": "dashboard",
1765
+ "initial": "Dashboard",
1766
+ "states": {
1767
+ "Dashboard": {
1768
+ "initial": "ViewingMachine",
1769
+ "states": {
1770
+ "ViewingMachine": { "on": { /* ... */ } },
1771
+ "EditingMachine": { "on": { /* ... */ } }
1772
+ }
1773
+ }
1774
+ }
1775
+ }
1776
+ ```
1777
+
1778
+ #### Parallel (Orthogonal Regions)
1779
+
1780
+ Model independent regions that evolve simultaneously:
1781
+
1782
+ ```typescript
1783
+ const config: MachineConfig = {
1784
+ input: 'src/editor.ts',
1785
+ id: 'editor',
1786
+ parallel: {
1787
+ regions: [
1788
+ {
1789
+ name: 'fontWeight',
1790
+ initialState: 'Normal',
1791
+ classes: ['Normal', 'Bold']
1792
+ },
1793
+ {
1794
+ name: 'textDecoration',
1795
+ initialState: 'None',
1796
+ classes: ['None', 'Underline']
1797
+ }
1798
+ ]
1799
+ }
1800
+ };
1801
+ ```
1802
+
1803
+ Generates:
1804
+
1805
+ ```json
1806
+ {
1807
+ "id": "editor",
1808
+ "type": "parallel",
1809
+ "states": {
1810
+ "fontWeight": {
1811
+ "initial": "Normal",
1812
+ "states": {
1813
+ "Normal": { "on": { /* ... */ } },
1814
+ "Bold": { "on": { /* ... */ } }
1815
+ }
1816
+ },
1817
+ "textDecoration": {
1818
+ "initial": "None",
1819
+ "states": {
1820
+ "None": { "on": { /* ... */ } },
1821
+ "Underline": { "on": { /* ... */ } }
1822
+ }
1823
+ }
1824
+ }
1825
+ }
1826
+ ```
1827
+
1828
+ **See [docs/ADVANCED_EXTRACTION.md](./docs/ADVANCED_EXTRACTION.md) for complete guide.**
1829
+
1676
1830
  ### Runtime Extraction API
1677
1831
 
1678
1832
  Extract statecharts from **running machine instances** without requiring source code access:
@@ -1884,6 +2038,10 @@ overrideTransitions<M, T>(machine: M, overrides: T): M & T
1884
2038
  extendTransitions<M, T>(machine: M, newTransitions: T): M & T
1885
2039
  createMachineBuilder<M>(template: M): (context) => M
1886
2040
 
2041
+ // Transition Binding
2042
+ call<C, F>(fn: F, context: C, ...args): ReturnType<F>
2043
+ bindTransitions<M extends Machine<any>>(machine: M): M
2044
+
1887
2045
  // Pattern Matching
1888
2046
  matchMachine<M, K, R>(machine: M, key: K, handlers): R
1889
2047
  hasState<M, K, V>(machine: M, key: K, value: V): boolean
@@ -20,20 +20,27 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var src_exports = {};
22
22
  __export(src_exports, {
23
+ BoundMachine: () => BoundMachine,
23
24
  META_KEY: () => META_KEY,
24
25
  MachineBase: () => MachineBase,
25
26
  MultiMachineBase: () => MultiMachineBase,
26
27
  RUNTIME_META: () => RUNTIME_META,
27
28
  action: () => action,
29
+ bindTransitions: () => bindTransitions,
30
+ call: () => call,
28
31
  createAsyncMachine: () => createAsyncMachine,
29
32
  createEnsemble: () => createEnsemble,
33
+ createEvent: () => createEvent,
34
+ createFetchMachine: () => createFetchMachine,
30
35
  createFlow: () => createFlow,
31
36
  createMachine: () => createMachine,
32
37
  createMachineBuilder: () => createMachineBuilder,
33
38
  createMachineFactory: () => createMachineFactory,
34
39
  createMultiMachine: () => createMultiMachine,
35
40
  createMutableMachine: () => createMutableMachine,
41
+ createParallelMachine: () => createParallelMachine,
36
42
  createRunner: () => createRunner,
43
+ delegateToChild: () => delegateToChild,
37
44
  describe: () => describe,
38
45
  extendTransitions: () => extendTransitions,
39
46
  extractFromInstance: () => extractFromInstance,
@@ -46,10 +53,14 @@ __export(src_exports, {
46
53
  guarded: () => guarded,
47
54
  hasState: () => hasState,
48
55
  invoke: () => invoke,
56
+ isState: () => isState,
57
+ logState: () => logState,
49
58
  matchMachine: () => matchMachine,
59
+ mergeContext: () => mergeContext,
50
60
  metadata: () => metadata,
51
61
  next: () => next,
52
62
  overrideTransitions: () => overrideTransitions,
63
+ pipeTransitions: () => pipeTransitions,
53
64
  run: () => run,
54
65
  runAsync: () => runAsync,
55
66
  runMachine: () => runMachine,
@@ -60,6 +71,7 @@ __export(src_exports, {
60
71
  setContext: () => setContext,
61
72
  step: () => step,
62
73
  stepAsync: () => stepAsync,
74
+ toggle: () => toggle,
63
75
  transitionTo: () => transitionTo,
64
76
  yieldMachine: () => yieldMachine
65
77
  });
@@ -135,12 +147,12 @@ function attachRuntimeMeta(fn, metadata2) {
135
147
  const existing = fn[RUNTIME_META] || {};
136
148
  const merged = { ...existing, ...metadata2 };
137
149
  if (metadata2.guards && existing.guards) {
138
- merged.guards = [...existing.guards, ...metadata2.guards];
150
+ merged.guards = [...metadata2.guards, ...existing.guards];
139
151
  } else if (metadata2.guards) {
140
152
  merged.guards = [...metadata2.guards];
141
153
  }
142
154
  if (metadata2.actions && existing.actions) {
143
- merged.actions = [...existing.actions, ...metadata2.actions];
155
+ merged.actions = [...metadata2.actions, ...existing.actions];
144
156
  } else if (metadata2.actions) {
145
157
  merged.actions = [...metadata2.actions];
146
158
  }
@@ -256,17 +268,17 @@ function parseInvokeService(obj) {
256
268
  }
257
269
  return service;
258
270
  }
259
- function extractFromCallExpression(call, verbose = false) {
260
- if (!import_ts_morph.Node.isCallExpression(call)) {
271
+ function extractFromCallExpression(call2, verbose = false) {
272
+ if (!import_ts_morph.Node.isCallExpression(call2)) {
261
273
  return null;
262
274
  }
263
- const expression = call.getExpression();
275
+ const expression = call2.getExpression();
264
276
  const fnName = import_ts_morph.Node.isIdentifier(expression) ? expression.getText() : null;
265
277
  if (!fnName) {
266
278
  return null;
267
279
  }
268
280
  const metadata2 = {};
269
- const args = call.getArguments();
281
+ const args = call2.getArguments();
270
282
  switch (fnName) {
271
283
  case "transitionTo":
272
284
  if (args[0]) {
@@ -406,6 +418,26 @@ function analyzeStateNode(classSymbol, verbose = false) {
406
418
  }
407
419
  return chartNode;
408
420
  }
421
+ function analyzeStateNodeWithNesting(className, classSymbol, sourceFile, childConfig, verbose = false) {
422
+ const stateNode = analyzeStateNode(classSymbol, verbose);
423
+ if (childConfig) {
424
+ if (verbose) {
425
+ console.error(` 👪 Analyzing children for state: ${className}`);
426
+ }
427
+ stateNode.initial = childConfig.initialState;
428
+ stateNode.states = {};
429
+ for (const childClassName of childConfig.classes) {
430
+ const childClassDeclaration = sourceFile.getClass(childClassName);
431
+ if (childClassDeclaration) {
432
+ const childSymbol = childClassDeclaration.getSymbolOrThrow();
433
+ stateNode.states[childClassName] = analyzeStateNode(childSymbol, verbose);
434
+ } else {
435
+ console.warn(`⚠️ Warning: Child class '${childClassName}' not found.`);
436
+ }
437
+ }
438
+ }
439
+ return stateNode;
440
+ }
409
441
  function extractMachine(config, project, verbose = false) {
410
442
  if (verbose) {
411
443
  console.error(`
@@ -416,6 +448,45 @@ function extractMachine(config, project, verbose = false) {
416
448
  if (!sourceFile) {
417
449
  throw new Error(`Source file not found: ${config.input}`);
418
450
  }
451
+ if (config.parallel) {
452
+ if (verbose) {
453
+ console.error(` ⏹️ Parallel machine detected. Analyzing regions.`);
454
+ }
455
+ const parallelChart = {
456
+ id: config.id,
457
+ type: "parallel",
458
+ states: {}
459
+ };
460
+ if (config.description) {
461
+ parallelChart.description = config.description;
462
+ }
463
+ for (const region of config.parallel.regions) {
464
+ if (verbose) {
465
+ console.error(` 📍 Analyzing region: ${region.name}`);
466
+ }
467
+ const regionStates = {};
468
+ for (const className of region.classes) {
469
+ const classDeclaration = sourceFile.getClass(className);
470
+ if (classDeclaration) {
471
+ const classSymbol = classDeclaration.getSymbolOrThrow();
472
+ regionStates[className] = analyzeStateNode(classSymbol, verbose);
473
+ } else {
474
+ console.warn(`⚠️ Warning: Class '${className}' not found for region '${region.name}'.`);
475
+ }
476
+ }
477
+ parallelChart.states[region.name] = {
478
+ initial: region.initialState,
479
+ states: regionStates
480
+ };
481
+ }
482
+ if (verbose) {
483
+ console.error(` ✅ Extracted ${config.parallel.regions.length} parallel regions`);
484
+ }
485
+ return parallelChart;
486
+ }
487
+ if (!config.initialState || !config.classes) {
488
+ throw new Error(`Machine config for '${config.id}' must have either 'parallel' or 'initialState'/'classes'.`);
489
+ }
419
490
  const fullChart = {
420
491
  id: config.id,
421
492
  initial: config.initialState,
@@ -431,7 +502,14 @@ function extractMachine(config, project, verbose = false) {
431
502
  continue;
432
503
  }
433
504
  const classSymbol = classDeclaration.getSymbolOrThrow();
434
- const stateNode = analyzeStateNode(classSymbol, verbose);
505
+ const hasChildren = className === config.initialState && config.children;
506
+ const stateNode = analyzeStateNodeWithNesting(
507
+ className,
508
+ classSymbol,
509
+ sourceFile,
510
+ hasChildren ? config.children : void 0,
511
+ verbose
512
+ );
435
513
  fullChart.states[className] = stateNode;
436
514
  }
437
515
  if (verbose) {
@@ -797,6 +875,198 @@ function createMutableMachine(sharedContext, factories, getDiscriminant) {
797
875
  });
798
876
  }
799
877
 
878
+ // src/higher-order.ts
879
+ function delegateToChild(actionName) {
880
+ return function(...args) {
881
+ const child = this.context.child;
882
+ if (typeof child[actionName] === "function") {
883
+ const newChildState = child[actionName](...args);
884
+ return setContext(this, { ...this.context, child: newChildState });
885
+ }
886
+ return this;
887
+ };
888
+ }
889
+ function toggle(prop) {
890
+ return function() {
891
+ if (typeof this.context[prop] !== "boolean") {
892
+ console.warn(`[toggle primitive] Property '${String(prop)}' is not a boolean. Toggling may have unexpected results.`);
893
+ }
894
+ return setContext(this, {
895
+ ...this.context,
896
+ [prop]: !this.context[prop]
897
+ });
898
+ };
899
+ }
900
+ var IdleMachine = class extends MachineBase {
901
+ constructor(config) {
902
+ super({ status: "idle" });
903
+ this.config = config;
904
+ this.fetch = (params) => new LoadingMachine(this.config, params != null ? params : this.config.initialParams, 1);
905
+ }
906
+ };
907
+ var LoadingMachine = class extends MachineBase {
908
+ constructor(config, params, attempts) {
909
+ super({ status: "loading", abortController: new AbortController(), attempts });
910
+ this.config = config;
911
+ this.params = params;
912
+ this.succeed = (data) => {
913
+ var _a, _b;
914
+ (_b = (_a = this.config).onSuccess) == null ? void 0 : _b.call(_a, data);
915
+ return new SuccessMachine(this.config, { status: "success", data });
916
+ };
917
+ this.fail = (error) => {
918
+ var _a, _b, _c;
919
+ const maxRetries = (_a = this.config.maxRetries) != null ? _a : 3;
920
+ if (this.context.attempts < maxRetries) {
921
+ return new RetryingMachine(this.config, this.params, error, this.context.attempts);
922
+ }
923
+ (_c = (_b = this.config).onError) == null ? void 0 : _c.call(_b, error);
924
+ return new ErrorMachine(this.config, { status: "error", error });
925
+ };
926
+ this.cancel = () => {
927
+ this.context.abortController.abort();
928
+ return new CanceledMachine(this.config);
929
+ };
930
+ this.execute();
931
+ }
932
+ async execute() {
933
+ }
934
+ };
935
+ var RetryingMachine = class extends MachineBase {
936
+ constructor(config, params, error, attempts) {
937
+ super({ status: "retrying", error, attempts });
938
+ this.config = config;
939
+ this.params = params;
940
+ // This would be called after a delay.
941
+ this.retry = (params) => new LoadingMachine(this.config, params != null ? params : this.params, this.context.attempts + 1);
942
+ }
943
+ };
944
+ var SuccessMachine = class extends MachineBase {
945
+ constructor(config, context) {
946
+ super(context);
947
+ this.config = config;
948
+ this.refetch = (params) => new LoadingMachine(this.config, params != null ? params : this.config.initialParams, 1);
949
+ }
950
+ };
951
+ var ErrorMachine = class extends MachineBase {
952
+ constructor(config, context) {
953
+ super(context);
954
+ this.config = config;
955
+ this.retry = (params) => new LoadingMachine(this.config, params != null ? params : this.config.initialParams, 1);
956
+ }
957
+ };
958
+ var CanceledMachine = class extends MachineBase {
959
+ constructor(config) {
960
+ super({ status: "canceled" });
961
+ this.config = config;
962
+ this.refetch = (params) => new LoadingMachine(this.config, params != null ? params : this.config.initialParams, 1);
963
+ }
964
+ };
965
+ function createFetchMachine(config) {
966
+ return new IdleMachine(config);
967
+ }
968
+ function createParallelMachine(m1, m2) {
969
+ const combinedContext = { ...m1.context, ...m2.context };
970
+ const transitions1 = { ...m1 };
971
+ const transitions2 = { ...m2 };
972
+ delete transitions1.context;
973
+ delete transitions2.context;
974
+ const combinedTransitions = {};
975
+ for (const key in transitions1) {
976
+ const transitionFn = transitions1[key];
977
+ combinedTransitions[key] = (...args) => {
978
+ const nextM1 = transitionFn.apply(m1.context, args);
979
+ return createParallelMachine(nextM1, m2);
980
+ };
981
+ }
982
+ for (const key in transitions2) {
983
+ const transitionFn = transitions2[key];
984
+ combinedTransitions[key] = (...args) => {
985
+ const nextM2 = transitionFn.apply(m2.context, args);
986
+ return createParallelMachine(m1, nextM2);
987
+ };
988
+ }
989
+ return {
990
+ context: combinedContext,
991
+ ...combinedTransitions
992
+ };
993
+ }
994
+
995
+ // src/utils.ts
996
+ function isState(machine, machineClass) {
997
+ return machine instanceof machineClass;
998
+ }
999
+ function createEvent(type, ...args) {
1000
+ return { type, args };
1001
+ }
1002
+ function mergeContext(machine, partialContext) {
1003
+ return setContext(machine, (ctx) => ({ ...ctx, ...partialContext }));
1004
+ }
1005
+ async function pipeTransitions(initialMachine, ...transitions) {
1006
+ let current = initialMachine;
1007
+ for (const transitionFn of transitions) {
1008
+ current = await transitionFn(current);
1009
+ }
1010
+ return current;
1011
+ }
1012
+ function logState(machine, label) {
1013
+ if (label) {
1014
+ console.log(label, machine.context);
1015
+ } else {
1016
+ console.log(machine.context);
1017
+ }
1018
+ return machine;
1019
+ }
1020
+ function call(fn, context, ...args) {
1021
+ return fn.apply(context, args);
1022
+ }
1023
+ function bindTransitions(machine) {
1024
+ return new Proxy(machine, {
1025
+ get(target, prop) {
1026
+ const value = target[prop];
1027
+ if (typeof value === "function") {
1028
+ return function(...args) {
1029
+ const result = value.apply(target.context, args);
1030
+ if (result && typeof result === "object" && "context" in result) {
1031
+ return bindTransitions(result);
1032
+ }
1033
+ return result;
1034
+ };
1035
+ }
1036
+ return value;
1037
+ }
1038
+ });
1039
+ }
1040
+ var BoundMachine = class _BoundMachine {
1041
+ constructor(machine) {
1042
+ this.wrappedMachine = machine;
1043
+ return new Proxy(this, {
1044
+ get: (target, prop) => {
1045
+ if (prop === "wrappedMachine" || prop === "context") {
1046
+ return Reflect.get(target, prop);
1047
+ }
1048
+ const value = this.wrappedMachine[prop];
1049
+ if (typeof value === "function") {
1050
+ return (...args) => {
1051
+ const result = value.apply(this.wrappedMachine.context, args);
1052
+ if (result && typeof result === "object" && "context" in result) {
1053
+ return new _BoundMachine(result);
1054
+ }
1055
+ return result;
1056
+ };
1057
+ }
1058
+ return value;
1059
+ }
1060
+ });
1061
+ }
1062
+ /**
1063
+ * Access the underlying machine's context directly.
1064
+ */
1065
+ get context() {
1066
+ return this.wrappedMachine.context;
1067
+ }
1068
+ };
1069
+
800
1070
  // src/index.ts
801
1071
  function createMachine(context, fns) {
802
1072
  return Object.assign({ context }, fns);