Skip to content

Commit c4bdbb2

Browse files
committed
Addd DEV_GUIDE.md and updated README.md
1 parent f3a7bf4 commit c4bdbb2

File tree

2 files changed

+327
-0
lines changed

2 files changed

+327
-0
lines changed

DEV_GUIDE.md

Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
## PureMVC MultiCore tate Machine Utility — Developer Guide
2+
3+
This guide explains how the Finite State Machine (FSM) utility works and how to use it with PureMVC (MultiCore) in TypeScript. It includes practical examples showing both programmatic setup and JSON-driven configuration via `FSMInjector`.
4+
5+
### What you get
6+
- A lightweight `StateMachine` `Mediator` that manages named `State` objects
7+
- Declarative transitions between states triggered by simple action strings
8+
- Optional notifications fired when entering, exiting, or after changing state
9+
- A `FSMInjector` that builds and registers the `StateMachine` from a JSON config
10+
11+
### Install
12+
```
13+
npm install @puremvc/puremvc-typescript-multicore-framework
14+
npm install @puremvc/puremvc-typescript-util-state-machine
15+
```
16+
17+
### Imports (ESM)
18+
```ts
19+
import {
20+
StateMachine,
21+
FSMInjector,
22+
State,
23+
type FSM,
24+
} from '@puremvc/puremvc-typescript-util-state-machine';
25+
```
26+
27+
---
28+
29+
### Core concepts
30+
31+
- StateMachine (Mediator)
32+
- Name: `StateMachine.NAME` (`"StateMachine"`)
33+
- Notifications it cares about:
34+
- `StateMachine.ACTION` — trigger transitions (type = action string; body = optional data)
35+
- `StateMachine.CANCEL` — cancel a pending transition during exiting/entering
36+
- Notification it emits:
37+
- `StateMachine.CHANGED` — broadcast after a successful transition; body = new `State`; type = state name
38+
- Holds a registry of `State` instances and tracks `currentState`.
39+
40+
- State
41+
- Properties: `name`, optional `entering`, `exiting`, `changed` notification names
42+
- Methods:
43+
- `defineTransition(action: string, target: string)` — define an action that leads to another state
44+
- `getTarget(action: string)` — resolve the target state name for an action
45+
46+
- FSM JSON schema (for `FSMInjector`)
47+
- Types exported as `FSM`, `StateDef`, `Transition`
48+
- Shape:
49+
```ts
50+
type Transition = { action: string; target: string };
51+
type StateDef = {
52+
name: string;
53+
entering?: string; // notification to send on entering
54+
exiting?: string; // notification to send on exiting
55+
changed?: string; // notification to send after state becomes current
56+
transitions?: Transition[];
57+
};
58+
type FSM = { initial: string; states: StateDef[] };
59+
```
60+
61+
---
62+
63+
### Transition lifecycle
64+
65+
When an actor sends `StateMachine.ACTION` with the desired action in the notification type, the `StateMachine` checks the current state for a matching transition:
66+
67+
1. If found, an exit sequence starts:
68+
- If the current state has `exiting`, it sends that notification with:
69+
- body = any `data` passed in the ACTION
70+
- type = next state name (so observers know where were going)
71+
- Any observer can cancel the transition by sending `StateMachine.CANCEL` while handling the exit notification.
72+
2. If not canceled, an enter sequence starts for the next state:
73+
- If the next state has `entering`, it sends that notification with body = the same `data`.
74+
- Observers may also cancel here by sending `StateMachine.CANCEL`.
75+
3. If not canceled:
76+
- `currentState` becomes the next state.
77+
- If the state has `changed`, it sends that notification with body = the same `data`.
78+
- Then `StateMachine.CHANGED` is sent with body = the new `State` and type = its name.
79+
80+
Notes:
81+
- Cancelation resets the internal flag and the `currentState` remains unchanged.
82+
- The `StateMachine` is a `Mediator`; it should be registered with your `Facade`.
83+
- On registration (`onRegister`), if an initial state is set, it transitions to it immediately (running the enter/changed notifications for that state only).
84+
85+
---
86+
87+
### Programmatic setup example
88+
89+
This minimal example shows how to create states, define transitions, register the `StateMachine`, and fire actions.
90+
91+
```ts
92+
import {
93+
Facade,
94+
Notifier,
95+
INotification,
96+
Mediator,
97+
} from '@puremvc/puremvc-typescript-multicore-framework';
98+
import { StateMachine, State } from '@puremvc/puremvc-typescript-util-state-machine';
99+
100+
const MULTITON_KEY = 'com.example.app';
101+
102+
// 1) Build your states
103+
const opened = new State(
104+
'OPENED',
105+
'note/opening', // entering
106+
null, // exiting
107+
'note/opened', // changed
108+
);
109+
const closed = new State(
110+
'CLOSED',
111+
'note/closing', // entering
112+
);
113+
114+
// 2) Define transitions
115+
opened.defineTransition('CLOSE', 'CLOSED');
116+
closed.defineTransition('OPEN', 'OPENED');
117+
118+
// 3) Create and register the StateMachine
119+
const fsm = new StateMachine();
120+
fsm.initializeNotifier(MULTITON_KEY);
121+
fsm.registerState(opened /* initial? */);
122+
fsm.registerState(closed, /* initial = */ true);
123+
124+
Facade.getInstance(MULTITON_KEY).registerMediator(fsm as Mediator);
125+
126+
// 4) Listen for changes and lifecycle notifications elsewhere
127+
class DoorMediator extends Mediator {
128+
public static NAME = 'DoorMediator';
129+
constructor() { super(DoorMediator.NAME); }
130+
131+
listNotificationInterests(): string[] {
132+
return [
133+
'note/opening',
134+
'note/opened',
135+
'note/closing',
136+
StateMachine.CHANGED,
137+
];
138+
}
139+
140+
handleNotification(note: INotification): void {
141+
switch (note.name) {
142+
case 'note/opening':
143+
console.log('Entering OPENED with data:', note.body);
144+
break;
145+
case 'note/opened':
146+
console.log('Changed to OPENED');
147+
break;
148+
case 'note/closing':
149+
console.log('Entering CLOSED with data:', note.body);
150+
break;
151+
case StateMachine.CHANGED:
152+
// note.body is the new State
153+
console.log('FSM changed to:', (note.body as State).name);
154+
break;
155+
}
156+
}
157+
}
158+
159+
const facade = Facade.getInstance(MULTITON_KEY);
160+
facade.registerMediator(new DoorMediator());
161+
162+
// 5) Trigger transitions from any Notifier-aware actor
163+
class UserAction extends Notifier {
164+
constructor() { super(); this.initializeNotifier(MULTITON_KEY); }
165+
openDoor() {
166+
this.sendNotification(StateMachine.ACTION, { by: 'user' }, 'OPEN');
167+
}
168+
closeDoor() {
169+
this.sendNotification(StateMachine.ACTION, { by: 'user' }, 'CLOSE');
170+
}
171+
}
172+
173+
const user = new UserAction();
174+
user.openDoor(); // CLOSED -> OPENED
175+
user.closeDoor(); // OPENED -> CLOSED
176+
```
177+
178+
Tip: The action string is passed in the notification `type`. The optional `body` payload flows through exiting/entering/changed notifications.
179+
180+
---
181+
182+
### Canceling a transition
183+
184+
Any observer of an `exiting` or `entering` notification can veto the move by sending `StateMachine.CANCEL` before the sequence completes.
185+
186+
```ts
187+
class GuardMediator extends Mediator {
188+
public static NAME = 'GuardMediator';
189+
constructor() { super(GuardMediator.NAME); }
190+
191+
listNotificationInterests(): string[] {
192+
return ['note/unlocking']; // suppose this is an exiting/entering note
193+
}
194+
195+
handleNotification(note: INotification): void {
196+
const shouldBlock = /* your rule */ false;
197+
if (shouldBlock) {
198+
this.facade.sendNotification(StateMachine.CANCEL);
199+
}
200+
}
201+
}
202+
```
203+
204+
When canceled, the FSM remains in the current state and no `changed` or `StateMachine.CHANGED` notifications are sent.
205+
206+
---
207+
208+
### JSON-driven setup with FSMInjector
209+
210+
Use `FSMInjector` when you want to drive the FSM from configuration rather than code.
211+
212+
```ts
213+
import {
214+
Facade,
215+
} from '@puremvc/puremvc-typescript-multicore-framework';
216+
import {
217+
FSMInjector,
218+
StateMachine,
219+
type FSM,
220+
} from '@puremvc/puremvc-typescript-util-state-machine';
221+
222+
const MULTITON_KEY = 'com.example.app';
223+
224+
const fsmConfig: FSM = {
225+
initial: 'CLOSED',
226+
states: [
227+
{
228+
name: 'OPENED',
229+
entering: 'note/opening',
230+
changed: 'note/opened',
231+
transitions: [
232+
{ action: 'CLOSE', target: 'CLOSED' },
233+
],
234+
},
235+
{
236+
name: 'CLOSED',
237+
entering: 'note/closing',
238+
transitions: [
239+
{ action: 'OPEN', target: 'OPENED' },
240+
{ action: 'LOCK', target: 'LOCKED' },
241+
],
242+
},
243+
{
244+
name: 'LOCKED',
245+
entering: 'note/locking',
246+
exiting: 'note/unlocking',
247+
transitions: [
248+
{ action: 'UNLOCK', target: 'CLOSED' },
249+
],
250+
},
251+
],
252+
};
253+
254+
// Build + register the StateMachine from JSON
255+
const injector = new FSMInjector(MULTITON_KEY, fsmConfig);
256+
const fsm = injector.inject(); // returns the created & registered StateMachine
257+
258+
// Listen for global CHANGED notifications
259+
Facade.getInstance(MULTITON_KEY).registerMediator(new (class extends Mediator {
260+
constructor() { super('LogFSM'); }
261+
listNotificationInterests() { return [StateMachine.CHANGED]; }
262+
handleNotification(note: INotification) {
263+
console.log('FSM CHANGED ->', note.type); // state name
264+
}
265+
})());
266+
267+
// Fire actions as before
268+
Facade.getInstance(MULTITON_KEY).sendNotification(StateMachine.ACTION, undefined, 'OPEN');
269+
```
270+
271+
How it works under the hood:
272+
- `FSMInjector.inject()` creates a new `StateMachine`, initializes it with your Multiton key, builds `State` instances from each `StateDef`, registers initial state, and registers the `StateMachine` with your `Facade`.
273+
- On `onRegister()`, the `StateMachine` automatically transitions to the initial state if one was provided.
274+
275+
---
276+
277+
### Practical tips
278+
279+
- Keep action strings short and consistent (e.g., `OPEN`, `CLOSE`). Consider centralizing them in a `const` enum or constants module.
280+
- Use the `exiting` notification’s type (which the FSM sets to the target state name) to detect where you’re headed, useful for preloading or validation.
281+
- Avoid side effects in `CHANGED` handlers that could recursively trigger more actions unless you explicitly want chained transitions.
282+
- If a transition is often canceled, consider checking preconditions before sending `StateMachine.ACTION` to reduce churn.
283+
- Unit tests: mock observers can assert the exact order of notifications: exiting -> entering -> changed -> StateMachine.CHANGED.
284+
285+
---
286+
287+
### Troubleshooting
288+
289+
- Nothing happens when you send `StateMachine.ACTION`:
290+
- Ensure the `StateMachine` is registered with the same Multiton key as the sender.
291+
- Ensure the current state defines a transition for the given action string.
292+
- Verify you passed the action in `type`, not `body`.
293+
294+
- Transition always aborts:
295+
- Some observer may be sending `StateMachine.CANCEL` while handling `exiting` or `entering`. Add logging to those handlers to find the culprit.
296+
297+
- Didn’t enter the initial state:
298+
- The initial state is set by passing `initial=true` to `registerState(...)` or via the JSON `initial` property.
299+
- Confirm `onRegister()` is being called (i.e., the `StateMachine` is actually registered as a Mediator).
300+
301+
---
302+
303+
### API quick reference
304+
305+
- StateMachine
306+
- Constants: `NAME`, `ACTION`, `CHANGED`, `CANCEL`
307+
- Methods:
308+
- `registerState(state: State, initial?: boolean): void`
309+
- `getState(name: string): State | undefined`
310+
- `removeState(name: string): void`
311+
- `viewComponent: State | null` — current state
312+
313+
- State
314+
- Constructor: `new State(name, entering?, exiting?, changed?)`
315+
- `defineTransition(action, target)`
316+
- `getTarget(action)`
317+
318+
- FSMInjector
319+
- Constructor: `new FSMInjector(multitonKey: string, fsm: FSM)`
320+
- `inject(): StateMachine` — creates states, registers the `StateMachine` with the Facade
321+
322+
---
323+
324+
If you need deeper details, see the source in `src/fsm` and the type definitions in `src/types.ts`.

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
## [PureMVC](http://puremvc.github.com/) [Typescript](https://github.com/PureMVC/puremvc-typescript-multicore-framework) Utility: State Machine
22
This utility provides a simple yet effective Finite State Machine implementation, which allows the definition of discrete states, and the valid transitions to other states available from any given state, and the actions which trigger the transitions. A mechanism is provided for defining the entire state machine in JSON and having a fully populated StateMachine injected into the PureMVC app.
33

4+
## Dev Guide
5+
* [PureMVC TypeScript State Machine Utility — Developer Guide](DEV_GUIDE.md)
6+
47
## Installation
58
```shell
69
npm install @puremvc/puremvc-typescript-multicore-framework

0 commit comments

Comments
 (0)