@devpablocristo/core-fsm 0.2.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.
Files changed (3) hide show
  1. package/package.json +27 -0
  2. package/src/fsm.ts +151 -0
  3. package/src/index.ts +8 -0
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@devpablocristo/core-fsm",
3
+ "version": "0.2.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./src/index.ts"
8
+ },
9
+ "scripts": {
10
+ "typecheck": "tsc --noEmit",
11
+ "test": "vitest run --globals --passWithNoTests src"
12
+ },
13
+ "devDependencies": {
14
+ "typescript": "^5.8.0",
15
+ "vitest": "^3.2.4"
16
+ },
17
+ "files": [
18
+ "src",
19
+ "!src/**/*.test.ts",
20
+ "!src/**/*.test.tsx",
21
+ "!src/**/*.spec.ts",
22
+ "!src/**/*.spec.tsx"
23
+ ],
24
+ "publishConfig": {
25
+ "access": "public"
26
+ }
27
+ }
package/src/fsm.ts ADDED
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Máquinas de estados finitas — paridad con core/concurrency/go/fsm.
3
+ *
4
+ * Dos variantes:
5
+ * - `Machine<S, E>`: genérica tipada con reglas From+Event→To
6
+ * - `StringMachine`: builder fluido con terminales, grupos libres y aristas explícitas
7
+ */
8
+
9
+ // --- Machine genérica tipada ---
10
+
11
+ export type Rule<S, E> = {
12
+ from: S;
13
+ event: E;
14
+ to: S;
15
+ };
16
+
17
+ export class Machine<S, E> {
18
+ private transitions: Map<string, S>;
19
+
20
+ constructor(rules: Rule<S, E>[]) {
21
+ this.transitions = new Map();
22
+ for (const r of rules) {
23
+ this.transitions.set(`${r.from}|${r.event}`, r.to);
24
+ }
25
+ }
26
+
27
+ transition(from: S, event: E): S {
28
+ const to = this.transitions.get(`${from}|${event}`);
29
+ if (to === undefined) {
30
+ throw new InvalidTransitionError(`${from}`, `${event}`);
31
+ }
32
+ return to;
33
+ }
34
+
35
+ canTransition(from: S, event: E): boolean {
36
+ return this.transitions.has(`${from}|${event}`);
37
+ }
38
+ }
39
+
40
+ // --- StringMachine con builder ---
41
+
42
+ export class StringMachine {
43
+ private terminals: Set<string>;
44
+ private freeGroup: Set<string>;
45
+ private explicit: Map<string, Set<string>>;
46
+ private allowAnyNonTerminalTo: Set<string>;
47
+
48
+ constructor(
49
+ terminals: Set<string>,
50
+ freeGroup: Set<string>,
51
+ explicit: Map<string, Set<string>>,
52
+ allowAnyNonTerminalTo: Set<string>,
53
+ ) {
54
+ this.terminals = terminals;
55
+ this.freeGroup = freeGroup;
56
+ this.explicit = explicit;
57
+ this.allowAnyNonTerminalTo = allowAnyNonTerminalTo;
58
+ }
59
+
60
+ canTransition(from: string, to: string): boolean {
61
+ if (from === to) return true;
62
+ if (this.terminals.has(from)) return false;
63
+ if (this.freeGroup.has(from) && this.freeGroup.has(to)) return true;
64
+ if (this.explicit.get(from)?.has(to)) return true;
65
+ if (this.allowAnyNonTerminalTo.has(to)) return true;
66
+ return false;
67
+ }
68
+
69
+ validate(from: string, to: string): void {
70
+ if (from === to) return;
71
+ if (this.terminals.has(from)) {
72
+ throw new TerminalStateError(from);
73
+ }
74
+ if (!this.canTransition(from, to)) {
75
+ throw new InvalidTransitionError(from, to);
76
+ }
77
+ }
78
+
79
+ isTerminal(state: string): boolean {
80
+ return this.terminals.has(state);
81
+ }
82
+ }
83
+
84
+ export class Builder {
85
+ private _terminals = new Set<string>();
86
+ private _freeGroup = new Set<string>();
87
+ private _explicit = new Map<string, Set<string>>();
88
+ private _allowAnyNonTerminalTo = new Set<string>();
89
+
90
+ terminal(...states: string[]): this {
91
+ for (const s of states) {
92
+ if (s) this._terminals.add(s);
93
+ }
94
+ return this;
95
+ }
96
+
97
+ freeTransitionsAmong(...states: string[]): this {
98
+ for (const s of states) {
99
+ if (s) this._freeGroup.add(s);
100
+ }
101
+ return this;
102
+ }
103
+
104
+ allow(from: string, to: string): this {
105
+ return this.allowFromStatesTo(to, from);
106
+ }
107
+
108
+ allowFromStatesTo(to: string, ...froms: string[]): this {
109
+ if (!to) return this;
110
+ for (const from of froms) {
111
+ if (!from) continue;
112
+ let targets = this._explicit.get(from);
113
+ if (!targets) {
114
+ targets = new Set();
115
+ this._explicit.set(from, targets);
116
+ }
117
+ targets.add(to);
118
+ }
119
+ return this;
120
+ }
121
+
122
+ allowAnyTo(to: string): this {
123
+ if (to) this._allowAnyNonTerminalTo.add(to);
124
+ return this;
125
+ }
126
+
127
+ build(): StringMachine {
128
+ return new StringMachine(
129
+ new Set(this._terminals),
130
+ new Set(this._freeGroup),
131
+ new Map(Array.from(this._explicit.entries()).map(([k, v]) => [k, new Set(v)])),
132
+ new Set(this._allowAnyNonTerminalTo),
133
+ );
134
+ }
135
+ }
136
+
137
+ // --- Errores ---
138
+
139
+ export class InvalidTransitionError extends Error {
140
+ constructor(from: string, to: string) {
141
+ super(`fsm: invalid transition (${from} -> ${to})`);
142
+ this.name = 'InvalidTransitionError';
143
+ }
144
+ }
145
+
146
+ export class TerminalStateError extends Error {
147
+ constructor(state: string) {
148
+ super(`fsm: cannot leave terminal state (${state})`);
149
+ this.name = 'TerminalStateError';
150
+ }
151
+ }
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ export {
2
+ Machine,
3
+ StringMachine,
4
+ Builder,
5
+ InvalidTransitionError,
6
+ TerminalStateError,
7
+ type Rule,
8
+ } from './fsm';