@doeixd/machine 1.0.3 → 1.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.
- package/README.md +48 -0
- package/dist/cjs/development/core.js.map +1 -1
- package/dist/cjs/development/delegate.js +89 -0
- package/dist/cjs/development/delegate.js.map +7 -0
- package/dist/cjs/development/index.js +383 -158
- package/dist/cjs/development/index.js.map +4 -4
- package/dist/cjs/development/minimal.js +172 -0
- package/dist/cjs/development/minimal.js.map +7 -0
- package/dist/cjs/development/react.js.map +1 -1
- package/dist/cjs/production/delegate.js +1 -0
- package/dist/cjs/production/index.js +3 -3
- package/dist/cjs/production/minimal.js +1 -0
- package/dist/esm/development/core.js.map +1 -1
- package/dist/esm/development/delegate.js +68 -0
- package/dist/esm/development/delegate.js.map +7 -0
- package/dist/esm/development/index.js +389 -158
- package/dist/esm/development/index.js.map +4 -4
- package/dist/esm/development/minimal.js +149 -0
- package/dist/esm/development/minimal.js.map +7 -0
- package/dist/esm/development/react.js.map +1 -1
- package/dist/esm/production/delegate.js +1 -0
- package/dist/esm/production/index.js +3 -3
- package/dist/esm/production/minimal.js +1 -0
- package/dist/types/delegate.d.ts +101 -0
- package/dist/types/delegate.d.ts.map +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/minimal.d.ts +95 -0
- package/dist/types/minimal.d.ts.map +1 -0
- package/dist/types/types.d.ts +110 -0
- package/dist/types/types.d.ts.map +1 -0
- package/package.json +25 -1
- package/src/delegate.ts +267 -0
- package/src/index.ts +13 -0
- package/src/middleware.ts +1049 -1050
- package/src/minimal.ts +269 -0
- package/src/types.ts +129 -0
package/src/minimal.ts
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview A minimal, type-safe typestate library.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// CORE TYPES (MUST BE IN THIS FILE FOR INFERENCE)
|
|
7
|
+
// ============================================================================
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A state machine combining context (state data) with transitions (methods).
|
|
11
|
+
*/
|
|
12
|
+
export type Machine<C extends object, T> = C & T;
|
|
13
|
+
|
|
14
|
+
// Re-export utilities
|
|
15
|
+
export * from './types';
|
|
16
|
+
import { type Tagged, type TagOf, type Cleanup } from './types';
|
|
17
|
+
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// CORE: machine()
|
|
20
|
+
// ============================================================================
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Creates a state machine by bundling context with transitions.
|
|
24
|
+
*
|
|
25
|
+
* Note: 'any' is used in the 'next' callback signature to break recursive
|
|
26
|
+
* inference cycles. This is required for TypeScript to correctly infer
|
|
27
|
+
* transitions 'T' from the return object of the factory.
|
|
28
|
+
*/
|
|
29
|
+
export function machine<C extends object, T>(
|
|
30
|
+
context: C,
|
|
31
|
+
factory: (ctx: C, next: (context: C) => any) => T
|
|
32
|
+
): C & T {
|
|
33
|
+
const next = (newContext: C) => machine(newContext, factory);
|
|
34
|
+
const transitions = factory(context, next);
|
|
35
|
+
return Object.assign({}, context, transitions) as C & T;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ============================================================================
|
|
39
|
+
// PATTERN MATCHING: match()
|
|
40
|
+
// ============================================================================
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Handler functions for each case in a tagged union.
|
|
44
|
+
*/
|
|
45
|
+
export type MatchCases<T extends Tagged, R> = {
|
|
46
|
+
[K in TagOf<T>]: (state: Extract<T, { tag: K }>) => R;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Exhaustive pattern matching on tagged unions.
|
|
51
|
+
*/
|
|
52
|
+
export function match<T extends Tagged, R>(
|
|
53
|
+
state: T,
|
|
54
|
+
cases: MatchCases<T, R>
|
|
55
|
+
): R {
|
|
56
|
+
const handler = cases[state.tag as TagOf<T>];
|
|
57
|
+
return handler(state as Extract<T, { tag: typeof state.tag }>);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ============================================================================
|
|
61
|
+
// EFFECTS: runnable() + run()
|
|
62
|
+
// ============================================================================
|
|
63
|
+
|
|
64
|
+
export interface Lifecycle<E extends string = string> {
|
|
65
|
+
onEnter?: (send: Send<E>) => Cleanup;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type Send<E extends string = string> = (event: E, ...args: unknown[]) => void;
|
|
69
|
+
|
|
70
|
+
export type LifecycleMap<Tags extends string> = {
|
|
71
|
+
[K in Tags]?: Lifecycle<string>;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const LIFECYCLE = Symbol('lifecycle');
|
|
75
|
+
|
|
76
|
+
export type RunnableMachine<M, Tags extends string> = M & {
|
|
77
|
+
[LIFECYCLE]?: LifecycleMap<Tags>;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export function runnable<
|
|
81
|
+
M extends Tagged,
|
|
82
|
+
Tags extends string = TagOf<M>
|
|
83
|
+
>(
|
|
84
|
+
initialMachine: M,
|
|
85
|
+
lifecycles: LifecycleMap<Tags>
|
|
86
|
+
): RunnableMachine<M, Tags> {
|
|
87
|
+
const result = { ...initialMachine } as RunnableMachine<M, Tags>;
|
|
88
|
+
result[LIFECYCLE] = lifecycles;
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface Runner<M> {
|
|
93
|
+
get: () => M;
|
|
94
|
+
send: Send<string>;
|
|
95
|
+
stop: () => void;
|
|
96
|
+
subscribe: (listener: (state: M) => void) => () => void;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function run<M extends Tagged>(
|
|
100
|
+
initial: RunnableMachine<M, string>
|
|
101
|
+
): Runner<M> {
|
|
102
|
+
let current: RunnableMachine<M, string> = initial;
|
|
103
|
+
let cleanup: Cleanup | null = null;
|
|
104
|
+
const listeners = new Set<(state: M) => void>();
|
|
105
|
+
|
|
106
|
+
const notify = () => {
|
|
107
|
+
listeners.forEach((fn) => fn(current as M));
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const enter = () => {
|
|
111
|
+
if (cleanup) {
|
|
112
|
+
cleanup();
|
|
113
|
+
cleanup = null;
|
|
114
|
+
}
|
|
115
|
+
const lifecycles = current[LIFECYCLE];
|
|
116
|
+
const tagValue = (current as Tagged).tag;
|
|
117
|
+
const lifecycle = lifecycles?.[tagValue];
|
|
118
|
+
if (lifecycle?.onEnter) {
|
|
119
|
+
cleanup = lifecycle.onEnter(send);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const send: Send<string> = (event, ...args) => {
|
|
124
|
+
const transition = (current as Record<string, unknown>)[event];
|
|
125
|
+
if (typeof transition === 'function') {
|
|
126
|
+
const nextValue = (transition as (...a: unknown[]) => unknown)(...args);
|
|
127
|
+
if (nextValue && typeof nextValue === 'object' && 'tag' in nextValue) {
|
|
128
|
+
const nextMachine = nextValue as RunnableMachine<M, string>;
|
|
129
|
+
if (!nextMachine[LIFECYCLE] && current[LIFECYCLE]) {
|
|
130
|
+
nextMachine[LIFECYCLE] = current[LIFECYCLE];
|
|
131
|
+
}
|
|
132
|
+
current = nextMachine;
|
|
133
|
+
enter();
|
|
134
|
+
notify();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
enter();
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
get: () => current as M,
|
|
143
|
+
send,
|
|
144
|
+
stop: () => {
|
|
145
|
+
if (cleanup) {
|
|
146
|
+
cleanup();
|
|
147
|
+
cleanup = null;
|
|
148
|
+
}
|
|
149
|
+
listeners.clear();
|
|
150
|
+
},
|
|
151
|
+
subscribe: (listener) => {
|
|
152
|
+
listeners.add(listener);
|
|
153
|
+
return () => listeners.delete(listener);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ============================================================================
|
|
159
|
+
// COMPOSITION: withChildren()
|
|
160
|
+
// ============================================================================
|
|
161
|
+
|
|
162
|
+
export type ParentMachine<P extends object, C extends Record<string, object>> =
|
|
163
|
+
P & { [K in keyof C]: ChildProxy<P, C, C[K]> };
|
|
164
|
+
|
|
165
|
+
type ChildProxy<P extends object, C extends Record<string, object>, Child extends object> = {
|
|
166
|
+
[K in keyof Child]: Child[K] extends (...args: infer A) => unknown
|
|
167
|
+
? (...args: A) => ParentMachine<P, C>
|
|
168
|
+
: Child[K];
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
export function withChildren<
|
|
172
|
+
P extends object,
|
|
173
|
+
C extends Record<string, object>
|
|
174
|
+
>(
|
|
175
|
+
parent: P,
|
|
176
|
+
children: C
|
|
177
|
+
): ParentMachine<P, C> {
|
|
178
|
+
const result = { ...parent } as ParentMachine<P, C>;
|
|
179
|
+
|
|
180
|
+
for (const key of Object.keys(children) as Array<keyof C>) {
|
|
181
|
+
const child = children[key];
|
|
182
|
+
|
|
183
|
+
const childProxy = new Proxy(child, {
|
|
184
|
+
get(target, prop: string | symbol) {
|
|
185
|
+
const value = (target as Record<string | symbol, unknown>)[prop];
|
|
186
|
+
|
|
187
|
+
if (typeof value === 'function') {
|
|
188
|
+
return (...args: unknown[]) => {
|
|
189
|
+
const nextChild = (value as (...a: unknown[]) => unknown)(...args);
|
|
190
|
+
return withChildren(
|
|
191
|
+
{ ...parent },
|
|
192
|
+
{ ...children, [key]: nextChild as object } as C
|
|
193
|
+
);
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return value;
|
|
198
|
+
}
|
|
199
|
+
}) as unknown as ChildProxy<P, C, C[keyof C]>;
|
|
200
|
+
(result as Record<string, unknown>)[key as string] = childProxy;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ============================================================================
|
|
207
|
+
// UTILITIES
|
|
208
|
+
// ============================================================================
|
|
209
|
+
|
|
210
|
+
// Re-exports from types.ts are enough
|
|
211
|
+
|
|
212
|
+
export function factory<C extends object>() {
|
|
213
|
+
return <T>(
|
|
214
|
+
transitionFactory: (ctx: C, next: (context: C) => any) => T
|
|
215
|
+
) => (context: C): C & T => machine(context, transitionFactory);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Extracts the union of all possible machine states from a union factory.
|
|
220
|
+
*/
|
|
221
|
+
export type UnionOf<F extends (...args: any[]) => any> = ReturnType<F>;
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Creates a union factory that routes to different transition factories based on a tag.
|
|
225
|
+
* This is the primary way to define multi-state machines (Type-States) in the minimal API.
|
|
226
|
+
*
|
|
227
|
+
* @param factories - A map of tags to transition factories.
|
|
228
|
+
* @returns A single factory function that produces the correct machine based on the input context's tag.
|
|
229
|
+
*
|
|
230
|
+
* @example
|
|
231
|
+
* const auth = union({
|
|
232
|
+
* idle: (ctx, next) => ({ login: () => next({ tag: 'loggedIn', user: 'alice' }) }),
|
|
233
|
+
* loggedIn: (ctx, next) => ({ logout: () => next({ tag: 'idle' }) })
|
|
234
|
+
* });
|
|
235
|
+
*
|
|
236
|
+
* const m = auth({ tag: 'idle' });
|
|
237
|
+
* const next = m.login(); // Transition to loggedIn state
|
|
238
|
+
*/
|
|
239
|
+
/**
|
|
240
|
+
* Creates a union factory that routes to different transition factories based on a tag.
|
|
241
|
+
* This is the primary way to define multi-state machines (Type-States) in the minimal API.
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* const auth = union<AuthState>()({
|
|
245
|
+
* idle: (ctx, next) => ({ login: () => next({ tag: 'loggedIn', user: 'alice' }) }),
|
|
246
|
+
* loggedIn: (ctx, next) => ({ logout: () => next({ tag: 'idle' }) })
|
|
247
|
+
* });
|
|
248
|
+
*
|
|
249
|
+
* const m = auth({ tag: 'idle' });
|
|
250
|
+
* const next = m.login(); // Transition to loggedIn state
|
|
251
|
+
*/
|
|
252
|
+
export function union<C extends Tagged>() {
|
|
253
|
+
return <F extends { [K in TagOf<C>]: (ctx: Extract<C, { tag: K }>, next: (c: C) => any) => any }>(
|
|
254
|
+
factories: F
|
|
255
|
+
) => {
|
|
256
|
+
type MachineMap = {
|
|
257
|
+
[K in TagOf<C> & keyof F]: F[K] extends (ctx: any, next: any) => infer T
|
|
258
|
+
? Extract<C, { tag: K }> & T
|
|
259
|
+
: never
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const resultFactory = <T extends C>(context: T): MachineMap[T['tag'] & keyof MachineMap] => {
|
|
263
|
+
const factoryFn = (factories as any)[(context as any).tag];
|
|
264
|
+
return machine(context as any, (ctx: any, _next: any) => factoryFn(ctx as any, resultFactory as any)) as any;
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
return resultFactory;
|
|
268
|
+
};
|
|
269
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Utility types for the machine library.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Extracts the context type from a machine (works with regular and minimal machines).
|
|
7
|
+
*/
|
|
8
|
+
export type Context<M> = M extends { readonly context: infer C } ? C : M;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Extracts the transitions type from a machine (works with regular and minimal machines).
|
|
12
|
+
*/
|
|
13
|
+
export type Transitions<M> = M extends { readonly context: any } & infer T ? T : M;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A discriminated union type representing an event that can be dispatched to a machine.
|
|
17
|
+
*/
|
|
18
|
+
export type Tagged<T extends string = string> = { readonly tag: T };
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Extracts the tag literal type from a tagged object.
|
|
22
|
+
*/
|
|
23
|
+
export type TagOf<T extends Tagged> = T['tag'];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Utility to define a union of tagged states from a mapping type.
|
|
27
|
+
* @example
|
|
28
|
+
* type PickMode = States<{
|
|
29
|
+
* idle: {},
|
|
30
|
+
* active: { isCloseMode: boolean; timeoutId: number }
|
|
31
|
+
* }>;
|
|
32
|
+
*/
|
|
33
|
+
export type States<M extends Record<string, object>> = {
|
|
34
|
+
[K in keyof M]: { readonly tag: K } & M[K]
|
|
35
|
+
}[keyof M];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Cleanup function returned from onEnter.
|
|
39
|
+
*/
|
|
40
|
+
export type Cleanup = () => void;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Extracts the return type from a factory function.
|
|
44
|
+
* @example
|
|
45
|
+
* const factory = () => machine({ count: 0 }, { ... });
|
|
46
|
+
* type MyMachine = InferMachine<typeof factory>;
|
|
47
|
+
*/
|
|
48
|
+
export type InferMachine<F extends (...args: any[]) => any> = ReturnType<F>;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Alias for InferMachine, more descriptive for state machine contexts.
|
|
52
|
+
*/
|
|
53
|
+
export type MachineOf<F extends (...args: any[]) => any> = InferMachine<F>;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Creates a tagged object or adds a tag to an existing object.
|
|
57
|
+
*/
|
|
58
|
+
export function tag<T extends string>(name: T): { tag: T };
|
|
59
|
+
export function tag<T extends string, O extends object>(name: T, props: O): { tag: T } & O;
|
|
60
|
+
export function tag<const T extends { tag: string }>(obj: T): T;
|
|
61
|
+
export function tag<T extends string, O extends object>(
|
|
62
|
+
nameOrObj: T | { tag: string },
|
|
63
|
+
props?: O
|
|
64
|
+
): { tag: T } | ({ tag: T } & O) | { tag: string } {
|
|
65
|
+
if (typeof nameOrObj === 'object') {
|
|
66
|
+
return nameOrObj;
|
|
67
|
+
}
|
|
68
|
+
if (props) {
|
|
69
|
+
return { ...props, tag: nameOrObj };
|
|
70
|
+
}
|
|
71
|
+
return { tag: nameOrObj };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Namespace for tag factory utility.
|
|
76
|
+
*/
|
|
77
|
+
export namespace tag {
|
|
78
|
+
/**
|
|
79
|
+
* Creates a pre-bound tag factory for a specific state.
|
|
80
|
+
*
|
|
81
|
+
* @typeParam C - Context (data) type
|
|
82
|
+
* @typeParam T - Transitions type (optional, for machine return types)
|
|
83
|
+
* @param name - The tag name for this state
|
|
84
|
+
* @returns A function that takes context data and returns a tagged object
|
|
85
|
+
*
|
|
86
|
+
* @typeParam C - Context (data) type
|
|
87
|
+
* @typeParam T - Transitions type (optional)
|
|
88
|
+
* @param name - The tag name
|
|
89
|
+
* @example const idle = tag.factory<{ count: number }>('idle');
|
|
90
|
+
*/
|
|
91
|
+
export function factory<C extends object, T extends object = {}>(name: string): (props: C) => { readonly tag: string } & C & T;
|
|
92
|
+
/**
|
|
93
|
+
* Creates a curried tag factory, ideal for use with the States utility.
|
|
94
|
+
* @example const state = tag.factory<AppState>()('idle')({ count: 0 });
|
|
95
|
+
*/
|
|
96
|
+
export function factory<C extends object, T extends object = {}>(): <K extends string>(name: K) => (props: Omit<Extract<C, { tag: K }>, 'tag'>) => (Extract<C, { tag: K }> extends never ? { readonly tag: K } & C : Extract<C, { tag: K }>) & T;
|
|
97
|
+
|
|
98
|
+
export function factory(name?: string) {
|
|
99
|
+
if (name) {
|
|
100
|
+
return (props: any) => tag(name, props);
|
|
101
|
+
}
|
|
102
|
+
return (name: string) => (props: any) => tag(name, props);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Type guard to check if a machine or object is in a specific state.
|
|
108
|
+
*/
|
|
109
|
+
export function isState<M extends Tagged, Tag extends TagOf<M>>(
|
|
110
|
+
machine: M,
|
|
111
|
+
tagValue: Tag
|
|
112
|
+
): machine is Extract<M, { tag: Tag }> {
|
|
113
|
+
return (machine as Tagged).tag === tagValue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Recursively freezes an object and its properties.
|
|
118
|
+
*/
|
|
119
|
+
export function freeze<T extends object>(obj: T): Readonly<T> {
|
|
120
|
+
Object.freeze(obj);
|
|
121
|
+
if (typeof (Object as any).values === 'function') {
|
|
122
|
+
for (const value of Object.values(obj)) {
|
|
123
|
+
if (value && typeof value === 'object') {
|
|
124
|
+
freeze(value);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return obj;
|
|
129
|
+
}
|