@pyreon/machine 0.5.0 → 0.7.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/lib/analysis/index.js.html +5406 -0
- package/lib/index.js +104 -0
- package/lib/index.js.map +1 -0
- package/lib/types/index.d.ts +104 -0
- package/lib/types/index.d.ts.map +1 -0
- package/package.json +1 -6
package/lib/index.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { signal } from "@pyreon/reactivity";
|
|
2
|
+
|
|
3
|
+
//#region src/machine.ts
|
|
4
|
+
/**
|
|
5
|
+
* Create a reactive state machine — a constrained signal with type-safe transitions.
|
|
6
|
+
*
|
|
7
|
+
* The returned instance is callable (reads like a signal) and exposes
|
|
8
|
+
* `send()`, `matches()`, `can()`, and listeners for state changes.
|
|
9
|
+
*
|
|
10
|
+
* @param config - Machine definition with initial state and state configs
|
|
11
|
+
* @returns A reactive machine instance
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* const machine = createMachine({
|
|
16
|
+
* initial: 'idle',
|
|
17
|
+
* states: {
|
|
18
|
+
* idle: { on: { FETCH: 'loading' } },
|
|
19
|
+
* loading: { on: { SUCCESS: 'done', ERROR: 'error' } },
|
|
20
|
+
* done: {},
|
|
21
|
+
* error: { on: { RETRY: 'loading' } },
|
|
22
|
+
* },
|
|
23
|
+
* })
|
|
24
|
+
*
|
|
25
|
+
* machine() // 'idle'
|
|
26
|
+
* machine.send('FETCH')
|
|
27
|
+
* machine() // 'loading'
|
|
28
|
+
*
|
|
29
|
+
* // Reactive in JSX
|
|
30
|
+
* {() => machine.matches('loading') && <Spinner />}
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
function createMachine(config) {
|
|
34
|
+
const { initial, states } = config;
|
|
35
|
+
if (!(initial in states)) throw new Error(`[@pyreon/machine] Initial state '${initial}' is not defined in states`);
|
|
36
|
+
const current = signal(initial);
|
|
37
|
+
const enterListeners = /* @__PURE__ */ new Map();
|
|
38
|
+
const transitionListeners = /* @__PURE__ */ new Set();
|
|
39
|
+
function resolveTransition(event, payload) {
|
|
40
|
+
const stateConfig = states[current.peek()];
|
|
41
|
+
if (!stateConfig?.on) return null;
|
|
42
|
+
const transition = stateConfig.on[event];
|
|
43
|
+
if (!transition) return null;
|
|
44
|
+
if (typeof transition === "string") return transition;
|
|
45
|
+
if (transition.guard && !transition.guard(payload)) return null;
|
|
46
|
+
return transition.target;
|
|
47
|
+
}
|
|
48
|
+
function machine() {
|
|
49
|
+
return current();
|
|
50
|
+
}
|
|
51
|
+
machine.send = (event, payload) => {
|
|
52
|
+
const target = resolveTransition(event, payload);
|
|
53
|
+
if (target === null) return;
|
|
54
|
+
const from = current.peek();
|
|
55
|
+
const machineEvent = {
|
|
56
|
+
type: event,
|
|
57
|
+
payload
|
|
58
|
+
};
|
|
59
|
+
current.set(target);
|
|
60
|
+
for (const cb of transitionListeners) cb(from, target, machineEvent);
|
|
61
|
+
const listeners = enterListeners.get(target);
|
|
62
|
+
if (listeners) for (const cb of listeners) cb(machineEvent);
|
|
63
|
+
};
|
|
64
|
+
machine.matches = (...matchStates) => {
|
|
65
|
+
const state = current();
|
|
66
|
+
return matchStates.includes(state);
|
|
67
|
+
};
|
|
68
|
+
machine.can = (event) => {
|
|
69
|
+
const stateConfig = states[current()];
|
|
70
|
+
if (!stateConfig?.on) return false;
|
|
71
|
+
if (!stateConfig.on[event]) return false;
|
|
72
|
+
return true;
|
|
73
|
+
};
|
|
74
|
+
machine.nextEvents = () => {
|
|
75
|
+
const stateConfig = states[current()];
|
|
76
|
+
if (!stateConfig?.on) return [];
|
|
77
|
+
return Object.keys(stateConfig.on);
|
|
78
|
+
};
|
|
79
|
+
machine.reset = () => {
|
|
80
|
+
current.set(initial);
|
|
81
|
+
};
|
|
82
|
+
machine.onEnter = (state, callback) => {
|
|
83
|
+
if (!enterListeners.has(state)) enterListeners.set(state, /* @__PURE__ */ new Set());
|
|
84
|
+
enterListeners.get(state).add(callback);
|
|
85
|
+
return () => {
|
|
86
|
+
enterListeners.get(state)?.delete(callback);
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
machine.onTransition = (callback) => {
|
|
90
|
+
transitionListeners.add(callback);
|
|
91
|
+
return () => {
|
|
92
|
+
transitionListeners.delete(callback);
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
machine.dispose = () => {
|
|
96
|
+
enterListeners.clear();
|
|
97
|
+
transitionListeners.clear();
|
|
98
|
+
};
|
|
99
|
+
return machine;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
//#endregion
|
|
103
|
+
export { createMachine };
|
|
104
|
+
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/machine.ts"],"sourcesContent":["import { signal } from '@pyreon/reactivity'\nimport type {\n EnterCallback,\n InferEvents,\n InferStates,\n Machine,\n MachineConfig,\n MachineEvent,\n TransitionCallback,\n TransitionConfig,\n} from './types'\n\n/**\n * Create a reactive state machine — a constrained signal with type-safe transitions.\n *\n * The returned instance is callable (reads like a signal) and exposes\n * `send()`, `matches()`, `can()`, and listeners for state changes.\n *\n * @param config - Machine definition with initial state and state configs\n * @returns A reactive machine instance\n *\n * @example\n * ```tsx\n * const machine = createMachine({\n * initial: 'idle',\n * states: {\n * idle: { on: { FETCH: 'loading' } },\n * loading: { on: { SUCCESS: 'done', ERROR: 'error' } },\n * done: {},\n * error: { on: { RETRY: 'loading' } },\n * },\n * })\n *\n * machine() // 'idle'\n * machine.send('FETCH')\n * machine() // 'loading'\n *\n * // Reactive in JSX\n * {() => machine.matches('loading') && <Spinner />}\n * ```\n */\nexport function createMachine<\n const TConfig extends MachineConfig<string, string>,\n>(config: TConfig): Machine<InferStates<TConfig>, InferEvents<TConfig>> {\n type TState = InferStates<TConfig>\n type TEvent = InferEvents<TConfig>\n\n const { initial, states } = config as unknown as MachineConfig<TState, TEvent>\n\n // Validate initial state\n if (!(initial in states)) {\n throw new Error(\n `[@pyreon/machine] Initial state '${initial}' is not defined in states`,\n )\n }\n\n const current = signal<TState>(initial)\n const enterListeners = new Map<TState, Set<EnterCallback<TEvent>>>()\n const transitionListeners = new Set<TransitionCallback<TState, TEvent>>()\n\n function resolveTransition(event: TEvent, payload?: unknown): TState | null {\n const stateConfig = states[current.peek()]\n if (!stateConfig?.on) return null\n\n const transition = stateConfig.on[event] as\n | TransitionConfig<TState>\n | undefined\n if (!transition) return null\n\n if (typeof transition === 'string') {\n return transition\n }\n\n // Guarded transition\n if (transition.guard && !transition.guard(payload)) {\n return null\n }\n\n return transition.target\n }\n\n // The machine instance — callable like a signal\n function machine(): TState {\n return current()\n }\n\n machine.send = (event: TEvent, payload?: unknown): void => {\n const target = resolveTransition(event, payload)\n if (target === null) return\n\n const from = current.peek()\n const machineEvent: MachineEvent<TEvent> = { type: event, payload }\n\n current.set(target)\n\n // Fire transition listeners\n for (const cb of transitionListeners) {\n cb(from, target, machineEvent)\n }\n\n // Fire enter listeners for the target state\n const listeners = enterListeners.get(target)\n if (listeners) {\n for (const cb of listeners) {\n cb(machineEvent)\n }\n }\n }\n\n machine.matches = (...matchStates: TState[]): boolean => {\n const state = current()\n return matchStates.includes(state)\n }\n\n machine.can = (event: TEvent): boolean => {\n const stateConfig = states[current()]\n if (!stateConfig?.on) return false\n\n const transition = stateConfig.on[event]\n if (!transition) return false\n\n // For guarded transitions, we can't know without payload\n // Return true if the event exists (guard may still reject)\n return true\n }\n\n machine.nextEvents = (): TEvent[] => {\n const stateConfig = states[current()]\n if (!stateConfig?.on) return []\n return Object.keys(stateConfig.on) as TEvent[]\n }\n\n machine.reset = (): void => {\n current.set(initial)\n }\n\n machine.onEnter = (\n state: TState,\n callback: EnterCallback<TEvent>,\n ): (() => void) => {\n if (!enterListeners.has(state)) {\n enterListeners.set(state, new Set())\n }\n enterListeners.get(state)!.add(callback)\n\n return () => {\n enterListeners.get(state)?.delete(callback)\n }\n }\n\n machine.onTransition = (\n callback: TransitionCallback<TState, TEvent>,\n ): (() => void) => {\n transitionListeners.add(callback)\n return () => {\n transitionListeners.delete(callback)\n }\n }\n\n machine.dispose = (): void => {\n enterListeners.clear()\n transitionListeners.clear()\n }\n\n return machine as Machine<TState, TEvent>\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,SAAgB,cAEd,QAAsE;CAItE,MAAM,EAAE,SAAS,WAAW;AAG5B,KAAI,EAAE,WAAW,QACf,OAAM,IAAI,MACR,oCAAoC,QAAQ,4BAC7C;CAGH,MAAM,UAAU,OAAe,QAAQ;CACvC,MAAM,iCAAiB,IAAI,KAAyC;CACpE,MAAM,sCAAsB,IAAI,KAAyC;CAEzE,SAAS,kBAAkB,OAAe,SAAkC;EAC1E,MAAM,cAAc,OAAO,QAAQ,MAAM;AACzC,MAAI,CAAC,aAAa,GAAI,QAAO;EAE7B,MAAM,aAAa,YAAY,GAAG;AAGlC,MAAI,CAAC,WAAY,QAAO;AAExB,MAAI,OAAO,eAAe,SACxB,QAAO;AAIT,MAAI,WAAW,SAAS,CAAC,WAAW,MAAM,QAAQ,CAChD,QAAO;AAGT,SAAO,WAAW;;CAIpB,SAAS,UAAkB;AACzB,SAAO,SAAS;;AAGlB,SAAQ,QAAQ,OAAe,YAA4B;EACzD,MAAM,SAAS,kBAAkB,OAAO,QAAQ;AAChD,MAAI,WAAW,KAAM;EAErB,MAAM,OAAO,QAAQ,MAAM;EAC3B,MAAM,eAAqC;GAAE,MAAM;GAAO;GAAS;AAEnE,UAAQ,IAAI,OAAO;AAGnB,OAAK,MAAM,MAAM,oBACf,IAAG,MAAM,QAAQ,aAAa;EAIhC,MAAM,YAAY,eAAe,IAAI,OAAO;AAC5C,MAAI,UACF,MAAK,MAAM,MAAM,UACf,IAAG,aAAa;;AAKtB,SAAQ,WAAW,GAAG,gBAAmC;EACvD,MAAM,QAAQ,SAAS;AACvB,SAAO,YAAY,SAAS,MAAM;;AAGpC,SAAQ,OAAO,UAA2B;EACxC,MAAM,cAAc,OAAO,SAAS;AACpC,MAAI,CAAC,aAAa,GAAI,QAAO;AAG7B,MAAI,CADe,YAAY,GAAG,OACjB,QAAO;AAIxB,SAAO;;AAGT,SAAQ,mBAA6B;EACnC,MAAM,cAAc,OAAO,SAAS;AACpC,MAAI,CAAC,aAAa,GAAI,QAAO,EAAE;AAC/B,SAAO,OAAO,KAAK,YAAY,GAAG;;AAGpC,SAAQ,cAAoB;AAC1B,UAAQ,IAAI,QAAQ;;AAGtB,SAAQ,WACN,OACA,aACiB;AACjB,MAAI,CAAC,eAAe,IAAI,MAAM,CAC5B,gBAAe,IAAI,uBAAO,IAAI,KAAK,CAAC;AAEtC,iBAAe,IAAI,MAAM,CAAE,IAAI,SAAS;AAExC,eAAa;AACX,kBAAe,IAAI,MAAM,EAAE,OAAO,SAAS;;;AAI/C,SAAQ,gBACN,aACiB;AACjB,sBAAoB,IAAI,SAAS;AACjC,eAAa;AACX,uBAAoB,OAAO,SAAS;;;AAIxC,SAAQ,gBAAsB;AAC5B,iBAAe,OAAO;AACtB,sBAAoB,OAAO;;AAG7B,QAAO"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* A transition target — either a state name or an object with target and guard.
|
|
4
|
+
*/
|
|
5
|
+
type TransitionConfig<TState extends string> = TState | {
|
|
6
|
+
target: TState;
|
|
7
|
+
guard: (payload?: unknown) => boolean;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* State definition — maps event names to transition configs.
|
|
11
|
+
*/
|
|
12
|
+
interface StateConfig<TState extends string, TEvent extends string> {
|
|
13
|
+
on?: Partial<Record<TEvent, TransitionConfig<TState>>>;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Machine definition — initial state and state configs.
|
|
17
|
+
*/
|
|
18
|
+
interface MachineConfig<TState extends string, TEvent extends string> {
|
|
19
|
+
initial: TState;
|
|
20
|
+
states: Record<TState, StateConfig<TState, TEvent>>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Event object passed to listeners.
|
|
24
|
+
*/
|
|
25
|
+
interface MachineEvent<TEvent extends string = string> {
|
|
26
|
+
type: TEvent;
|
|
27
|
+
payload?: unknown;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Callback for onEnter — receives the event that caused the transition.
|
|
31
|
+
*/
|
|
32
|
+
type EnterCallback<TEvent extends string = string> = (event: MachineEvent<TEvent>) => void;
|
|
33
|
+
/**
|
|
34
|
+
* Callback for onTransition — receives from state, to state, and the event.
|
|
35
|
+
*/
|
|
36
|
+
type TransitionCallback<TState extends string = string, TEvent extends string = string> = (from: TState, to: TState, event: MachineEvent<TEvent>) => void;
|
|
37
|
+
/**
|
|
38
|
+
* The machine instance returned by `createMachine()`.
|
|
39
|
+
*/
|
|
40
|
+
interface Machine<TState extends string, TEvent extends string> {
|
|
41
|
+
/** Read current state — reactive in effects/computeds/JSX */
|
|
42
|
+
(): TState;
|
|
43
|
+
/** Send an event to trigger a transition */
|
|
44
|
+
send: (event: TEvent, payload?: unknown) => void;
|
|
45
|
+
/** Check if the machine is in one of the given states — reactive */
|
|
46
|
+
matches: (...states: TState[]) => boolean;
|
|
47
|
+
/** Check if an event would trigger a valid transition from current state */
|
|
48
|
+
can: (event: TEvent) => boolean;
|
|
49
|
+
/** Get all valid events from the current state — reactive */
|
|
50
|
+
nextEvents: () => TEvent[];
|
|
51
|
+
/** Reset to initial state */
|
|
52
|
+
reset: () => void;
|
|
53
|
+
/** Register a callback for when the machine enters a specific state */
|
|
54
|
+
onEnter: (state: TState, callback: EnterCallback<TEvent>) => () => void;
|
|
55
|
+
/** Register a callback for any state transition */
|
|
56
|
+
onTransition: (callback: TransitionCallback<TState, TEvent>) => () => void;
|
|
57
|
+
/** Remove all listeners and clean up */
|
|
58
|
+
dispose: () => void;
|
|
59
|
+
}
|
|
60
|
+
/** Extract state names from a machine config */
|
|
61
|
+
type InferStates<T> = T extends {
|
|
62
|
+
states: Record<infer S, unknown>;
|
|
63
|
+
} ? S & string : never;
|
|
64
|
+
/** Extract event names from a machine config */
|
|
65
|
+
type InferEvents<T> = T extends {
|
|
66
|
+
states: Record<string, {
|
|
67
|
+
on?: Partial<Record<infer E, unknown>>;
|
|
68
|
+
}>;
|
|
69
|
+
} ? E & string : never;
|
|
70
|
+
//#endregion
|
|
71
|
+
//#region src/machine.d.ts
|
|
72
|
+
/**
|
|
73
|
+
* Create a reactive state machine — a constrained signal with type-safe transitions.
|
|
74
|
+
*
|
|
75
|
+
* The returned instance is callable (reads like a signal) and exposes
|
|
76
|
+
* `send()`, `matches()`, `can()`, and listeners for state changes.
|
|
77
|
+
*
|
|
78
|
+
* @param config - Machine definition with initial state and state configs
|
|
79
|
+
* @returns A reactive machine instance
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```tsx
|
|
83
|
+
* const machine = createMachine({
|
|
84
|
+
* initial: 'idle',
|
|
85
|
+
* states: {
|
|
86
|
+
* idle: { on: { FETCH: 'loading' } },
|
|
87
|
+
* loading: { on: { SUCCESS: 'done', ERROR: 'error' } },
|
|
88
|
+
* done: {},
|
|
89
|
+
* error: { on: { RETRY: 'loading' } },
|
|
90
|
+
* },
|
|
91
|
+
* })
|
|
92
|
+
*
|
|
93
|
+
* machine() // 'idle'
|
|
94
|
+
* machine.send('FETCH')
|
|
95
|
+
* machine() // 'loading'
|
|
96
|
+
*
|
|
97
|
+
* // Reactive in JSX
|
|
98
|
+
* {() => machine.matches('loading') && <Spinner />}
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
declare function createMachine<const TConfig extends MachineConfig<string, string>>(config: TConfig): Machine<InferStates<TConfig>, InferEvents<TConfig>>;
|
|
102
|
+
//#endregion
|
|
103
|
+
export { type EnterCallback, type InferEvents, type InferStates, type Machine, type MachineConfig, type MachineEvent, type StateConfig, type TransitionCallback, type TransitionConfig, createMachine };
|
|
104
|
+
//# sourceMappingURL=index2.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index2.d.ts","names":[],"sources":["../../../src/types.ts","../../../src/machine.ts"],"mappings":";;AAGA;;KAAY,gBAAA,0BACR,MAAA;EACE,MAAA,EAAQ,MAAA;EAAQ,KAAA,GAAQ,OAAA;AAAA;;;;UAKb,WAAA;EACf,EAAA,GAAK,OAAA,CAAQ,MAAA,CAAO,MAAA,EAAQ,gBAAA,CAAiB,MAAA;AAAA;AAD/C;;;AAAA,UAOiB,aAAA;EACf,OAAA,EAAS,MAAA;EACT,MAAA,EAAQ,MAAA,CAAO,MAAA,EAAQ,WAAA,CAAY,MAAA,EAAQ,MAAA;AAAA;;;;UAM5B,YAAA;EACf,IAAA,EAAM,MAAA;EACN,OAAA;AAAA;;;;KAMU,aAAA,oCACV,KAAA,EAAO,YAAA,CAAa,MAAA;;;AAjBtB;KAuBY,kBAAA,oEAGP,IAAA,EAAM,MAAA,EAAQ,EAAA,EAAI,MAAA,EAAQ,KAAA,EAAO,YAAA,CAAa,MAAA;;;;UAKlC,OAAA;EA7B4B;EAAA,IA+BvC,MAAA;EA/BI;EAkCR,IAAA,GAAO,KAAA,EAAO,MAAA,EAAQ,OAAA;EAlCR;EAqCd,OAAA,MAAa,MAAA,EAAQ,MAAA;EAvC+B;EA0CpD,GAAA,GAAM,KAAA,EAAO,MAAA;EAzCJ;EA4CT,UAAA,QAAkB,MAAA;EA3CV;EA8CR,KAAA;EA9CuB;EAiDvB,OAAA,GAAU,KAAA,EAAO,MAAA,EAAQ,QAAA,EAAU,aAAA,CAAc,MAAA;EAjDN;EAoD3C,YAAA,GAAe,QAAA,EAAU,kBAAA,CAAmB,MAAA,EAAQ,MAAA;EApDH;EAuDjD,OAAA;AAAA;;KAMU,WAAA,MAAiB,CAAA;EAAY,MAAA,EAAQ,MAAA;AAAA,IAC7C,CAAA;;KAIQ,WAAA,MAAiB,CAAA;EAC3B,MAAA,EAAQ,MAAA;IAAiB,EAAA,GAAK,OAAA,CAAQ,MAAA;EAAA;AAAA,IAEpC,CAAA;;;AArFJ;;;;;;;;;;;;AAOA;;;;;;;;;;;;;;;;;AAPA,iBCsCgB,aAAA,uBACQ,aAAA,iBAAA,CACtB,MAAA,EAAQ,OAAA,GAAU,OAAA,CAAQ,WAAA,CAAY,OAAA,GAAU,WAAA,CAAY,OAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/machine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Reactive state machines for Pyreon — constrained signals with type-safe transitions",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -41,10 +41,5 @@
|
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"@pyreon/reactivity": ">=0.5.0 <1.0.0"
|
|
44
|
-
},
|
|
45
|
-
"devDependencies": {
|
|
46
|
-
"@happy-dom/global-registrator": "^20.8.3",
|
|
47
|
-
"@pyreon/reactivity": ">=0.5.0 <1.0.0",
|
|
48
|
-
"@vitus-labs/tools-lint": "^1.11.0"
|
|
49
44
|
}
|
|
50
45
|
}
|