@doeixd/machine 0.0.4

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/src/index.ts ADDED
@@ -0,0 +1,528 @@
1
+ /**
2
+ * @file A tiny, immutable, and type-safe state machine library for TypeScript.
3
+ * @author doeixd
4
+ * @version 1.0.0
5
+ */
6
+
7
+ // =============================================================================
8
+ // SECTION: CORE TYPES & INTERFACES
9
+ // =============================================================================
10
+
11
+ /**
12
+ * A utility type that represents either a value of type T or a Promise that resolves to T.
13
+ * @template T - The value type.
14
+ */
15
+ export type MaybePromise<T> = T | Promise<T>;
16
+
17
+ /**
18
+ * The fundamental shape of any machine: a `context` object for state, and methods for transitions.
19
+ * @template C - The context (state) object type.
20
+ */
21
+ export type Machine<C extends object> = {
22
+ /** The readonly state of the machine. */
23
+ readonly context: C;
24
+ } & Record<string, (...args: any[]) => Machine<any>>;
25
+
26
+ /**
27
+ * The shape of an asynchronous machine, where transitions can return Promises.
28
+ * @template C - The context (state) object type.
29
+ */
30
+ export type AsyncMachine<C extends object> = {
31
+ /** The readonly state of the machine. */
32
+ readonly context: C;
33
+ } & Record<string, (...args: any[]) => MaybePromise<AsyncMachine<any>>>;
34
+
35
+
36
+ // =============================================================================
37
+ // SECTION: TYPE UTILITIES & INTROSPECTION
38
+ // =============================================================================
39
+
40
+ /**
41
+ * Extracts the context type `C` from a machine type `M`.
42
+ * @template M - The machine type.
43
+ * @example type Ctx = Context<Machine<{ count: number }>> // { count: number }
44
+ */
45
+ export type Context<M extends { context: any }> = M["context"];
46
+
47
+ /**
48
+ * Extracts the transition function signatures from a machine, excluding the context property.
49
+ * @template M - The machine type.
50
+ */
51
+ export type Transitions<M extends BaseMachine<any>> = Omit<M, "context">;
52
+
53
+ /**
54
+ * Extracts the argument types for a specific transition function in a Machine.
55
+ * @template M - The machine type.
56
+ * @template K - The transition function name.
57
+ */
58
+ export type TransitionArgs<M extends Machine<any>, K extends keyof M & string> =
59
+ M[K] extends (...args: infer A) => any ? A : never;
60
+
61
+ /**
62
+ * Extracts the names of all transitions as a string union type.
63
+ * @template M - The machine type.
64
+ * @example
65
+ * type Names = TransitionNames<Machine<{ count: number }> & { increment: () => any }>
66
+ * // Names = "increment"
67
+ */
68
+ export type TransitionNames<M extends BaseMachine<any>> = keyof Omit<M, "context"> & string;
69
+
70
+ /**
71
+ * Base machine type that both Machine and AsyncMachine extend from.
72
+ * @template C - The context object type.
73
+ */
74
+ export type BaseMachine<C extends object> = {
75
+ /** The readonly state of the machine. */
76
+ readonly context: C;
77
+ } & Record<string, (...args: any[]) => any>;
78
+
79
+ /**
80
+ * Helper to make a type deeply readonly (freezes nested objects).
81
+ * Useful for ensuring immutability of context at the type level.
82
+ * @template T - The type to make readonly.
83
+ */
84
+ export type DeepReadonly<T> = {
85
+ readonly [P in keyof T]: T[P] extends object
86
+ ? T[P] extends (...args: any[]) => any
87
+ ? T[P]
88
+ : DeepReadonly<T[P]>
89
+ : T[P];
90
+ };
91
+
92
+ /**
93
+ * Infers the machine type from a machine factory function.
94
+ * @template F - The factory function type.
95
+ * @example
96
+ * const factory = () => createMachine({ count: 0 }, { ... });
97
+ * type MyMachine = InferMachine<typeof factory>; // Extracts the return type
98
+ */
99
+ export type InferMachine<F extends (...args: any[]) => any> = ReturnType<F>;
100
+
101
+ /**
102
+ * A discriminated union type representing an event that can be dispatched to a machine.
103
+ * This is automatically generated from a machine's type signature, ensuring full type safety.
104
+ * @template M - The machine type.
105
+ * @example
106
+ * type CounterEvent = Event<Machine<{ count: number }>& { add: (n: number) => any }>
107
+ * // CounterEvent = { type: "add"; args: [number] }
108
+ */
109
+ export type Event<M extends BaseMachine<any>> = {
110
+ [K in keyof Omit<M, "context"> & string]: M[K] extends (...args: infer A) => any
111
+ ? { type: K; args: A }
112
+ : never
113
+ }[keyof Omit<M, "context"> & string];
114
+
115
+
116
+ // =============================================================================
117
+ // SECTION: MACHINE CREATION (FUNCTIONAL & OOP)
118
+ // =============================================================================
119
+
120
+ /**
121
+ * Creates a synchronous state machine from a context and transition functions.
122
+ * This is the core factory for the functional approach.
123
+ *
124
+ * @template C - The context object type.
125
+ * @param context - The initial state context.
126
+ * @param fns - An object containing transition function definitions.
127
+ * @returns A new machine instance.
128
+ */
129
+ export function createMachine<C extends object, T extends Record<string, (this: C, ...args: any[]) => any>>(
130
+ context: C,
131
+ fns: T
132
+ ): { context: C } & T {
133
+ return Object.assign({ context }, fns);
134
+ }
135
+
136
+ /**
137
+ * Creates an asynchronous state machine from a context and async transition functions.
138
+ *
139
+ * @template C - The context object type.
140
+ * @param context - The initial state context.
141
+ * @param fns - An object containing async transition function definitions.
142
+ * @returns A new async machine instance.
143
+ */
144
+ export function createAsyncMachine<C extends object, T extends Record<string, (this: C, ...args: any[]) => any>>(
145
+ context: C,
146
+ fns: T
147
+ ): { context: C } & T {
148
+ return Object.assign({ context }, fns);
149
+ }
150
+
151
+ /**
152
+ * Creates a machine factory - a higher-order function that simplifies machine creation.
153
+ * Instead of writing transition logic that creates new machines, you just write
154
+ * pure context transformation functions.
155
+ *
156
+ * @template C - The context object type.
157
+ * @returns A factory configurator function.
158
+ *
159
+ * @example
160
+ * const counterFactory = createMachineFactory<{ count: number }>()({
161
+ * increment: (ctx) => ({ count: ctx.count + 1 }),
162
+ * add: (ctx, n: number) => ({ count: ctx.count + n })
163
+ * });
164
+ *
165
+ * const counter = counterFactory({ count: 0 });
166
+ * const next = counter.increment(); // Returns new machine with count: 1
167
+ */
168
+ export function createMachineFactory<C extends object>() {
169
+ return <T extends Record<string, (ctx: C, ...args: any[]) => C>>(
170
+ transformers: T
171
+ ) => {
172
+ type MachineFns = {
173
+ [K in keyof T]: (
174
+ this: C,
175
+ ...args: T[K] extends (ctx: C, ...args: infer A) => C ? A : never
176
+ ) => Machine<C>;
177
+ };
178
+
179
+ const fns = Object.fromEntries(
180
+ Object.entries(transformers).map(([key, transform]) => [
181
+ key,
182
+ function (this: C, ...args: any[]) {
183
+ const newContext = (transform as any)(this, ...args);
184
+ return createMachine(newContext, fns as any);
185
+ },
186
+ ])
187
+ ) as MachineFns;
188
+
189
+ return (initialContext: C): Machine<C> & MachineFns => {
190
+ return createMachine(initialContext, fns);
191
+ };
192
+ };
193
+ }
194
+
195
+
196
+ // =============================================================================
197
+ // SECTION: ADVANCED CREATION & IMMUTABLE HELPERS
198
+ // =============================================================================
199
+
200
+ /**
201
+ * Creates a new machine instance with an updated context, preserving all original transitions.
202
+ * This is the primary, type-safe utility for applying state changes.
203
+ *
204
+ * @template M - The machine type.
205
+ * @param machine - The original machine instance.
206
+ * @param newContextOrFn - The new context object or an updater function.
207
+ * @returns A new machine instance of the same type with the updated context.
208
+ */
209
+ export function setContext<M extends Machine<any>>(
210
+ machine: M,
211
+ newContextOrFn: Context<M> | ((ctx: Readonly<Context<M>>) => Context<M>)
212
+ ): M {
213
+ const { context, ...transitions } = machine;
214
+ const newContext =
215
+ typeof newContextOrFn === "function"
216
+ ? (newContextOrFn as (ctx: Readonly<Context<M>>) => Context<M>)(context)
217
+ : newContextOrFn;
218
+
219
+ return createMachine(newContext, transitions) as M;
220
+ }
221
+
222
+ /**
223
+ * Creates a new machine by overriding or adding transition functions to an existing machine.
224
+ * Ideal for mocking in tests or decorating functionality. The original machine is unchanged.
225
+ *
226
+ * @template M - The original machine type.
227
+ * @template T - An object of new or overriding transition functions.
228
+ * @param machine - The base machine instance.
229
+ * @param overrides - An object containing the transitions to add or overwrite.
230
+ * @returns A new machine instance with the merged transitions.
231
+ */
232
+ export function overrideTransitions<
233
+ M extends Machine<any>,
234
+ T extends Record<string, (this: Context<M>, ...args: any[]) => any>
235
+ >(
236
+ machine: M,
237
+ overrides: T
238
+ ): Machine<Context<M>> & Omit<Transitions<M>, keyof T> & T {
239
+ const { context, ...originalTransitions } = machine;
240
+ const newTransitions = { ...originalTransitions, ...overrides };
241
+ return createMachine(context, newTransitions) as any;
242
+ }
243
+
244
+ /**
245
+ * Creates a new machine by adding new transition functions.
246
+ * This utility will produce a compile-time error if you attempt to add a
247
+ * transition that already exists, preventing accidental overrides.
248
+ *
249
+ * @template M - The original machine type.
250
+ * @template T - An object of new transition functions, whose keys must not exist in M.
251
+ * @param machine - The base machine instance.
252
+ * @param newTransitions - An object containing the new transitions to add.
253
+ * @returns A new machine instance with the combined original and new transitions.
254
+ */
255
+ export function extendTransitions<
256
+ M extends Machine<any>,
257
+ T extends Record<string, (this: Context<M>, ...args: any[]) => any> & {
258
+ [K in keyof T]: K extends keyof M ? never : T[K];
259
+ }
260
+ >(machine: M, newTransitions: T): M & T {
261
+ const { context, ...originalTransitions } = machine;
262
+ const combinedTransitions = { ...originalTransitions, ...newTransitions };
263
+ return createMachine(context, combinedTransitions) as M & T;
264
+ }
265
+
266
+ /**
267
+ * Creates a builder function from a "template" machine instance.
268
+ * This captures the behavior of a machine and returns a factory that can stamp out
269
+ * new instances with different initial contexts. Excellent for class-based machines.
270
+ *
271
+ * @template M - The machine type.
272
+ * @param templateMachine - An instance of a machine to use as the template.
273
+ * @returns A function that builds new machines of type M.
274
+ */
275
+ export function createMachineBuilder<M extends Machine<any>>(
276
+ templateMachine: M
277
+ ): (context: Context<M>) => M {
278
+ const { context, ...transitions } = templateMachine;
279
+ return (newContext: Context<M>): M => {
280
+ return createMachine(newContext, transitions) as M;
281
+ };
282
+ }
283
+
284
+ /**
285
+ * Pattern match on a machine's state based on a discriminant property in the context.
286
+ * This provides type-safe exhaustive matching for state machines.
287
+ *
288
+ * @template M - The machine type.
289
+ * @template K - The discriminant key in the context.
290
+ * @template R - The return type.
291
+ * @param machine - The machine to match against.
292
+ * @param discriminantKey - The key in the context to use for matching (e.g., "status").
293
+ * @param handlers - An object mapping each possible value to a handler function.
294
+ * @returns The result of the matched handler.
295
+ *
296
+ * @example
297
+ * const result = matchMachine(
298
+ * machine,
299
+ * 'status',
300
+ * {
301
+ * idle: (ctx) => "Machine is idle",
302
+ * loading: (ctx) => "Loading...",
303
+ * success: (ctx) => `Success: ${ctx.data}`,
304
+ * error: (ctx) => `Error: ${ctx.error}`
305
+ * }
306
+ * );
307
+ */
308
+ export function matchMachine<
309
+ M extends Machine<any>,
310
+ K extends keyof Context<M> & string,
311
+ R
312
+ >(
313
+ machine: M,
314
+ discriminantKey: K,
315
+ handlers: {
316
+ [V in Context<M>[K] & string]: (ctx: Context<M>) => R;
317
+ }
318
+ ): R {
319
+ const discriminant = machine.context[discriminantKey] as Context<M>[K] & string;
320
+ const handler = handlers[discriminant];
321
+ if (!handler) {
322
+ throw new Error(`No handler found for state: ${String(discriminant)}`);
323
+ }
324
+ return handler(machine.context);
325
+ }
326
+
327
+ /**
328
+ * Type-safe helper to assert that a machine's context has a specific discriminant value.
329
+ * This narrows the type of the context based on the discriminant.
330
+ *
331
+ * @template M - The machine type.
332
+ * @template K - The discriminant key.
333
+ * @template V - The discriminant value.
334
+ * @param machine - The machine to check.
335
+ * @param key - The discriminant key to check.
336
+ * @param value - The expected value.
337
+ * @returns True if the discriminant matches, with type narrowing.
338
+ *
339
+ * @example
340
+ * if (hasState(machine, 'status', 'loading')) {
341
+ * // machine.context.status is narrowed to 'loading'
342
+ * }
343
+ */
344
+ export function hasState<
345
+ M extends Machine<any>,
346
+ K extends keyof Context<M>,
347
+ V extends Context<M>[K]
348
+ >(
349
+ machine: M,
350
+ key: K,
351
+ value: V
352
+ ): machine is M & { context: Context<M> & { [P in K]: V } } {
353
+ return machine.context[key] === value;
354
+ }
355
+
356
+
357
+ // =============================================================================
358
+ // SECTION: RUNTIME & EVENT DISPATCHER
359
+ // =============================================================================
360
+
361
+ /**
362
+ * Runs an asynchronous state machine with a managed lifecycle and event dispatch capability.
363
+ * This is the "interpreter" for async machines, handling state updates and side effects.
364
+ *
365
+ * @template M - The initial machine type.
366
+ * @param initial - The initial machine state.
367
+ * @param onChange - Optional callback invoked with the new machine state after every transition.
368
+ * @returns An object with a `state` getter for the current context and an async `dispatch` function.
369
+ */
370
+ export function runMachine<M extends AsyncMachine<any>>(
371
+ initial: M,
372
+ onChange?: (m: M) => void
373
+ ) {
374
+ let current = initial;
375
+
376
+ async function dispatch<E extends Event<typeof current>>(event: E): Promise<M> {
377
+ const fn = (current as any)[event.type];
378
+ if (typeof fn !== 'function') {
379
+ throw new Error(`[Machine] Unknown event type '${String(event.type)}' on current state.`);
380
+ }
381
+ const nextState = await fn.apply(current.context, event.args);
382
+ current = nextState;
383
+ onChange?.(current);
384
+ return current;
385
+ }
386
+
387
+ return {
388
+ /** Gets the context of the current state of the machine. */
389
+ get state(): Context<M> {
390
+ return current.context;
391
+ },
392
+ /** Dispatches a type-safe event to the machine, triggering a transition. */
393
+ dispatch,
394
+ };
395
+ }
396
+
397
+ /**
398
+ * An optional base class for creating machines using an Object-Oriented style.
399
+ *
400
+ * This class provides the fundamental structure required by the library: a `context`
401
+ * property to hold the state. By extending `MachineBase`, you get a clear and
402
+ * type-safe starting point for defining states and transitions as classes and methods.
403
+ *
404
+ * Transitions should be implemented as methods that return a new instance of a
405
+ * state machine class (often `new MyClass(...)` or by using a `createMachineBuilder`).
406
+ * The `context` is marked `readonly` to enforce the immutable update pattern.
407
+ *
408
+ * @template C - The context object type that defines the state for this machine.
409
+ *
410
+ * @example
411
+ * // Define a simple counter state
412
+ * class Counter extends MachineBase<{ readonly count: number }> {
413
+ * constructor(count = 0) {
414
+ * super({ count });
415
+ * }
416
+ *
417
+ * increment(): Counter {
418
+ * // Return a new instance for the next state
419
+ * return new Counter(this.context.count + 1);
420
+ * }
421
+ *
422
+ * add(n: number): Counter {
423
+ * return new Counter(this.context.count + n);
424
+ * }
425
+ * }
426
+ *
427
+ * const machine = new Counter(5);
428
+ * const nextState = machine.increment(); // Returns a new Counter instance
429
+ *
430
+ * console.log(machine.context.count); // 5 (original is unchanged)
431
+ * console.log(nextState.context.count); // 6 (new state)
432
+ */
433
+ export class MachineBase<C extends object> {
434
+ /**
435
+ * The immutable state of the machine.
436
+ * To change the state, a transition method must return a new machine instance
437
+ * with a new context object.
438
+ */
439
+ public readonly context: C;
440
+
441
+ /**
442
+ * Initializes a new machine instance with its starting context.
443
+ * @param context - The initial state of the machine.
444
+ */
445
+ constructor(context: C) {
446
+ this.context = context;
447
+ // Object.freeze can provide additional runtime safety against accidental mutation,
448
+ // though it comes with a minor performance cost. It's a good practice for ensuring purity.
449
+ // Object.freeze(this.context);
450
+ }
451
+ }
452
+
453
+
454
+ /**
455
+ * Applies an update function to a machine's context, returning a new machine.
456
+ * This is a simpler alternative to `setContext` when you always use an updater function.
457
+ *
458
+ * @template C - The context object type.
459
+ * @param m - The machine to update.
460
+ * @param update - A function that takes the current context and returns the new context.
461
+ * @returns A new machine with the updated context.
462
+ *
463
+ * @example
464
+ * const updated = next(counter, (ctx) => ({ count: ctx.count + 1 }));
465
+ */
466
+ export function next<C extends object>(
467
+ m: Machine<C>,
468
+ update: (ctx: Readonly<C>) => C
469
+ ): Machine<C> {
470
+ const { context, ...transitions } = m;
471
+ return createMachine(update(context), transitions) as Machine<C>;
472
+ }
473
+
474
+ /**
475
+ * A type representing either a synchronous Machine or a Promise that resolves to a Machine.
476
+ * Useful for functions that can return either sync or async machines.
477
+ *
478
+ * @template C - The context object type.
479
+ *
480
+ * @example
481
+ * function getMachine(): MachineLike<{ count: number }> {
482
+ * if (Math.random() > 0.5) {
483
+ * return createMachine({ count: 0 }, { ... });
484
+ * } else {
485
+ * return Promise.resolve(createMachine({ count: 0 }, { ... }));
486
+ * }
487
+ * }
488
+ */
489
+ export type MachineLike<C extends object> =
490
+ | Machine<C>
491
+ | Promise<Machine<C>>;
492
+
493
+ /**
494
+ * A type representing the result of a machine transition.
495
+ * Can be either:
496
+ * - A new machine state
497
+ * - A tuple of [machine, cleanup function] where cleanup is called when leaving the state
498
+ *
499
+ * This enables state machines with side effects that need cleanup (e.g., subscriptions, timers).
500
+ *
501
+ * @template C - The context object type.
502
+ *
503
+ * @example
504
+ * function transition(): MachineResult<{ count: number }> {
505
+ * const interval = setInterval(() => console.log("tick"), 1000);
506
+ * const machine = createMachine({ count: 0 }, { ... });
507
+ * return [machine, () => clearInterval(interval)];
508
+ * }
509
+ */
510
+ export type MachineResult<C extends object> =
511
+ | Machine<C>
512
+ | [Machine<C>, () => void | Promise<void>];
513
+
514
+
515
+ // =============================================================================
516
+ // SECTION: GENERATOR-BASED COMPOSITION
517
+ // =============================================================================
518
+
519
+ export {
520
+ run,
521
+ step,
522
+ yieldMachine,
523
+ runSequence,
524
+ createFlow,
525
+ runWithDebug,
526
+ runAsync,
527
+ stepAsync
528
+ } from './generators';