@doeixd/machine 0.0.20 → 0.0.22

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/actor.ts ADDED
@@ -0,0 +1,284 @@
1
+ import {
2
+ Event,
3
+ BaseMachine,
4
+ TransitionNames,
5
+ TransitionArgs,
6
+ MaybePromise,
7
+ createMachine
8
+ } from './index';
9
+
10
+ // =============================================================================
11
+ // TYPES
12
+ // =============================================================================
13
+
14
+ /**
15
+ * A standard interface for interacting with any actor-like entity.
16
+ */
17
+ export interface ActorRef<T> {
18
+ dispatch: (event: any) => void;
19
+ getSnapshot: () => T;
20
+ subscribe: (observer: (state: T) => void) => () => void;
21
+ }
22
+
23
+ /**
24
+ * Inspection event type.
25
+ */
26
+ export type InspectionEvent = {
27
+ type: '@actor/send'; // Extendable for lifecycle
28
+ actor: ActorRef<any>;
29
+ event: any;
30
+ snapshot: any;
31
+ };
32
+
33
+ // =============================================================================
34
+ // ACTOR CLASS
35
+ // =============================================================================
36
+
37
+ /**
38
+ * A reactive container for a state machine that handles dispatching,
39
+ * queueing of async transitions, and state observability.
40
+ */
41
+ export class Actor<M extends BaseMachine<any>> implements ActorRef<M> {
42
+ private _state: M;
43
+ private _observers: Set<(state: M) => void> = new Set();
44
+ private _queue: Array<Event<M>> = [];
45
+ private _processing = false;
46
+
47
+ // Global inspector
48
+ private static _inspector: ((event: InspectionEvent) => void) | null = null;
49
+
50
+ /**
51
+ * Registers a global inspector.
52
+ */
53
+ static inspect(inspector: (event: InspectionEvent) => void) {
54
+ Actor._inspector = inspector;
55
+ }
56
+
57
+ /**
58
+ * The "Magic" Dispatcher.
59
+ * Maps machine transition names to callable functions.
60
+ */
61
+ readonly send: {
62
+ [K in TransitionNames<M>]: (...args: TransitionArgs<M, K>) => void;
63
+ };
64
+
65
+ /**
66
+ * A stable reference to the dispatch method, useful for passing around.
67
+ */
68
+ readonly ref: {
69
+ send: (event: Event<M>) => void;
70
+ };
71
+
72
+ constructor(initialMachine: M) {
73
+ this._state = initialMachine;
74
+
75
+ // Pattern B: Reference to self for event-based dispatch
76
+ this.ref = {
77
+ send: (event) => this.dispatch(event)
78
+ };
79
+
80
+ // Pattern A: Proxy for RPC-style dispatch
81
+ this.send = new Proxy({} as any, {
82
+ get: (_target, prop) => {
83
+ return (...args: any[]) => {
84
+ this.dispatch({ type: prop as any, args: args as any } as unknown as Event<M>);
85
+ };
86
+ }
87
+ });
88
+ }
89
+
90
+ /**
91
+ * Returns the current immutable snapshot of the machine.
92
+ */
93
+ getSnapshot(): M {
94
+ return this._state;
95
+ }
96
+
97
+ /**
98
+ * Subscribes to state changes.
99
+ * @param observer Callback function to be invoked on every state change.
100
+ * @returns Unsubscribe function.
101
+ */
102
+ subscribe(observer: (state: M) => void): () => void {
103
+ this._observers.add(observer);
104
+ return () => {
105
+ this._observers.delete(observer);
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Selects a slice of the state.
111
+ */
112
+ select<T>(selector: (state: M) => T): T {
113
+ return selector(this._state);
114
+ }
115
+
116
+ /**
117
+ * Starts the actor.
118
+ */
119
+ start(): this {
120
+ return this;
121
+ }
122
+
123
+ /**
124
+ * Stops the actor.
125
+ */
126
+ stop(): void {
127
+ this._observers.clear();
128
+ }
129
+
130
+ /**
131
+ * Dispatches an event to the actor.
132
+ * Handles both sync and async transitions.
133
+ */
134
+ dispatch(event: Event<M>): void {
135
+ // Inspection
136
+ if (Actor._inspector) {
137
+ Actor._inspector({
138
+ type: '@actor/send',
139
+ actor: this,
140
+ event,
141
+ snapshot: this._state
142
+ });
143
+ }
144
+
145
+ if (this._processing) {
146
+ this._queue.push(event);
147
+ return;
148
+ }
149
+
150
+ this._processing = true;
151
+ this._queue.push(event);
152
+ this._flush();
153
+ }
154
+
155
+ private _flush() {
156
+ while (this._queue.length > 0) {
157
+ const event = this._queue[0];
158
+ this._queue.shift();
159
+
160
+ const transitions = this._state as any;
161
+ const fn = transitions[event.type];
162
+
163
+ if (typeof fn !== 'function') {
164
+ console.warn(`[Actor] Transition '${String(event.type)}' not found.`);
165
+ continue;
166
+ }
167
+
168
+ let result: MaybePromise<M>;
169
+ try {
170
+ result = fn.apply(this._state.context, event.args);
171
+ } catch (error) {
172
+ console.error(`[Actor] Error in transition '${String(event.type)}':`, error);
173
+ continue;
174
+ }
175
+
176
+ if (result instanceof Promise) {
177
+ result.then((nextState) => {
178
+ this._state = nextState;
179
+ this._notify();
180
+ this._flush();
181
+ }).catch((error) => {
182
+ console.error(`[Actor] Async error in transition '${String(event.type)}':`, error);
183
+ this._flush();
184
+ });
185
+ return;
186
+ } else {
187
+ this._state = result;
188
+ this._notify();
189
+ }
190
+ }
191
+ this._processing = false;
192
+ }
193
+
194
+ private _notify() {
195
+ const snapshot = this.getSnapshot();
196
+ this._observers.forEach(obs => obs(snapshot));
197
+ }
198
+ }
199
+
200
+ // =============================================================================
201
+ // INTEROP & HELPERS
202
+ // =============================================================================
203
+
204
+ /**
205
+ * Creates a new Actor instance from a machine.
206
+ */
207
+ export function createActor<M extends BaseMachine<any>>(machine: M): Actor<M> {
208
+ return new Actor(machine);
209
+ }
210
+
211
+ /**
212
+ * Spawns an actor from a machine. Alias for createActor.
213
+ */
214
+ export function spawn<M extends BaseMachine<any>>(machine: M): ActorRef<M> {
215
+ return createActor(machine);
216
+ }
217
+
218
+ /**
219
+ * Creates an actor-like machine from a Promise.
220
+ */
221
+ export function fromPromise<T>(promiseFn: () => Promise<T>) {
222
+ type PromiseContext =
223
+ | { status: 'pending'; data: undefined; error: undefined }
224
+ | { status: 'resolved'; data: T; error: undefined }
225
+ | { status: 'rejected'; data: undefined; error: any };
226
+
227
+ const initial: PromiseContext = { status: 'pending', data: undefined, error: undefined };
228
+
229
+ const machine = createMachine<PromiseContext>(initial,
230
+ (next) => ({
231
+ resolve(data: T) {
232
+ return next({ status: 'resolved' as const, data, error: undefined });
233
+ },
234
+ reject(error: any) {
235
+ return next({ status: 'rejected' as const, error, data: undefined });
236
+ }
237
+ })
238
+ );
239
+
240
+ const actor = createActor(machine);
241
+
242
+ promiseFn()
243
+ .then(data => (actor.send as any).resolve(data))
244
+ .catch(err => (actor.send as any).reject(err));
245
+
246
+ return actor;
247
+ }
248
+
249
+ /**
250
+ * Creates an actor-like machine from an Observable.
251
+ */
252
+ export function fromObservable<T>(observable: { subscribe: (next: (val: T) => void, error?: (err: any) => void, complete?: () => void) => { unsubscribe: () => void } }) {
253
+ type ObsContext =
254
+ | { status: 'active'; value: undefined; error: undefined }
255
+ | { status: 'active'; value: T; error: undefined }
256
+ | { status: 'done'; value: undefined; error: undefined }
257
+ | { status: 'error'; value: undefined; error: any };
258
+
259
+ const initial: ObsContext = { status: 'active', value: undefined, error: undefined };
260
+
261
+ const machine = createMachine<ObsContext>(initial,
262
+ (next) => ({
263
+ next(value: T) {
264
+ return next({ status: 'active' as const, value, error: undefined });
265
+ },
266
+ error(error: any) {
267
+ return next({ status: 'error' as const, error, value: undefined });
268
+ },
269
+ complete() {
270
+ return next({ status: 'done' as const, value: undefined, error: undefined });
271
+ }
272
+ })
273
+ );
274
+
275
+ const actor = createActor(machine);
276
+
277
+ observable.subscribe(
278
+ (val) => (actor.send as any).next(val),
279
+ (err) => (actor.send as any).error(err),
280
+ () => (actor.send as any).complete()
281
+ );
282
+
283
+ return actor;
284
+ }
package/src/index.ts CHANGED
@@ -127,7 +127,7 @@ export type TransitionNames<M extends BaseMachine<any>> = keyof Omit<M, "context
127
127
  export type BaseMachine<C extends object> = {
128
128
  /** The readonly state of the machine. */
129
129
  readonly context: C;
130
- } & Record<string, (...args: any[]) => any>;
130
+ };
131
131
 
132
132
  /**
133
133
  * Helper to make a type deeply readonly (freezes nested objects).
@@ -1102,6 +1102,8 @@ export * from './higher-order'
1102
1102
 
1103
1103
  export * from './middleware/index';
1104
1104
 
1105
+ export * from './mixins';
1106
+
1105
1107
  // =============================================================================
1106
1108
  // SECTION: UTILITIES & HELPERS
1107
1109
  // =============================================================================
@@ -1148,4 +1150,18 @@ export {
1148
1150
  type IsExhaustive,
1149
1151
  type WhenBuilder,
1150
1152
  type Matcher
1151
- } from './matcher';
1153
+ } from './matcher';
1154
+
1155
+ // =============================================================================
1156
+ // SECTION: ACTOR MODEL
1157
+ // =============================================================================
1158
+
1159
+ export {
1160
+ Actor,
1161
+ createActor,
1162
+ spawn,
1163
+ fromPromise,
1164
+ fromObservable,
1165
+ type ActorRef,
1166
+ type InspectionEvent
1167
+ } from './actor';
package/src/mixins.ts ADDED
@@ -0,0 +1,308 @@
1
+ import { MachineBase } from './index';
2
+
3
+ // =============================================================================
4
+ // HELPER TYPES
5
+ // =============================================================================
6
+
7
+ export type Constructor<T = any> = new (...args: any[]) => T;
8
+
9
+ /**
10
+ * Helper to convert a tuple of types into an intersection of those types.
11
+ * e.g. [A, B] -> A & B
12
+ */
13
+ export type UnionToIntersection<U> =
14
+ (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
15
+
16
+ /**
17
+ * Extracts the instance type from a constructor.
18
+ */
19
+ export type Instance<T> = T extends new (...args: any[]) => infer R ? R : never;
20
+
21
+ /**
22
+ * Extracts the Context type from a MachineBase subclass.
23
+ */
24
+ export type ExtractContext<T> = T extends MachineBase<infer C> ? C : never;
25
+
26
+ /**
27
+ * Combined context type for a union of machines.
28
+ */
29
+ export type CombinedContext<T extends Constructor[]> = UnionToIntersection<ExtractContext<Instance<T[number]>>> & object;
30
+
31
+ /**
32
+ * Combined instance type for a union of machines.
33
+ */
34
+ export type CombinedInstance<T extends Constructor[]> = UnionToIntersection<Instance<T[number]>>;
35
+
36
+ /**
37
+ * The instance type of a MachineUnion, with methods remapped to return the union type.
38
+ */
39
+ export type MachineUnionInstance<T extends Constructor[]> = {
40
+ [K in keyof CombinedInstance<T>]: CombinedInstance<T>[K] extends (...args: infer Args) => any
41
+ ? (...args: Args) => MachineUnionInstance<T>
42
+ : CombinedInstance<T>[K]
43
+ } & CombinedInstance<T>;
44
+
45
+ /**
46
+ * The constructor type for a MachineUnion.
47
+ */
48
+ export type MachineUnionConstructor<T extends Constructor[]> = new (context: CombinedContext<T>) => MachineUnionInstance<T>;
49
+
50
+ // =============================================================================
51
+ // HELPERS
52
+ // =============================================================================
53
+
54
+ function getAllPropertyDescriptors(obj: any) {
55
+ const descriptors: PropertyDescriptorMap = {};
56
+ let current = obj;
57
+ while (current && current !== Object.prototype) {
58
+ const props = Object.getOwnPropertyDescriptors(current);
59
+ for (const [key, desc] of Object.entries(props)) {
60
+ if (key === 'constructor') continue;
61
+ // Don't overwrite properties from child classes (which we visited first)
62
+ if (!(key in descriptors)) {
63
+ descriptors[key] = desc;
64
+ }
65
+ }
66
+ current = Object.getPrototypeOf(current);
67
+ }
68
+ return descriptors;
69
+ }
70
+
71
+ // =============================================================================
72
+ // MACHINE UNION
73
+ // =============================================================================
74
+
75
+ /**
76
+ * Creates a new class that combines the functionality of multiple Machine classes.
77
+ *
78
+ * This utility effectively implements multiple inheritance for State Machines.
79
+ * It merges the prototypes of all provided classes into a single new class,
80
+ * preserving the type safety of contexts and methods.
81
+ *
82
+ * Crucially, it **wraps** inherited methods to ensure they return instances
83
+ * of the *Combined* machine, enabling fluent method chaining across different
84
+ * mixed-in capabilities.
85
+ *
86
+ * @param machines - A list of Machine classes to combine.
87
+ * @returns A new class constructor that inherits from all input classes.
88
+ *
89
+ * @example
90
+ * ```typescript
91
+ * class A extends MachineBase<{ a: number }> {
92
+ * incA() { return new A({ a: this.context.a + 1 }); }
93
+ * }
94
+ * class B extends MachineBase<{ b: number }> {
95
+ * incB() { return new B({ b: this.context.b + 1 }); }
96
+ * }
97
+ *
98
+ * class AB extends MachineUnion(A, B) {}
99
+ *
100
+ * const machine = new AB({ a: 0, b: 0 });
101
+ * machine.incA().incB(); // Type-safe chaining!
102
+ * ```
103
+ */
104
+ export function MachineUnion<T extends Constructor[]>(...machines: T): MachineUnionConstructor<T> {
105
+ // calculate the combined context type (intersection of all contexts)
106
+ type Context = CombinedContext<T>;
107
+
108
+ // The base class to extend.
109
+ const Base = machines[0] as unknown as Constructor<MachineBase<Context>>;
110
+
111
+ class CombinedMachine extends Base {
112
+ constructor(context: Context) {
113
+ super(context);
114
+ }
115
+ }
116
+
117
+ // Helper to wrap methods
118
+ const wrapMethod = (fn: Function) => {
119
+ return function (this: CombinedMachine, ...args: any[]) {
120
+ // 1. Call the original method. It will return an instance of the *original* class (e.g. A)
121
+ // with the updated context FOR A.
122
+ // Inheritance means 'this' is the CombinedMachine, which matches A's expectations
123
+ // (covariance) for input, but the output is typed as A.
124
+ const result = fn.apply(this, args);
125
+
126
+ // 2. Check if the result is a Machine (has context)
127
+ if (result && typeof result === 'object' && 'context' in result) {
128
+ // 3. Create a NEW CombinedMachine instance.
129
+ // We merge the current context (to keep props from B)
130
+ // with the result context (updates from A).
131
+ // Using Object.assign or spread for performance/safety.
132
+ const newContext = { ...this.context, ...result.context };
133
+ return new CombinedMachine(newContext);
134
+ }
135
+
136
+ // If not a machine, returns raw result
137
+ return result;
138
+ };
139
+ };
140
+
141
+ // Mixin logic: Copy properties from all prototypes.
142
+ // We process ALL machines (including the first one) to ensure wrapping logic is applied to all.
143
+ for (const machine of machines) {
144
+ const descriptors = getAllPropertyDescriptors(machine.prototype);
145
+
146
+ for (const [key, descriptor] of Object.entries(descriptors)) {
147
+ if (key === 'constructor') continue;
148
+
149
+ // Logic: If it's a function (method), wrap it to return CombinedMachine.
150
+ if (typeof descriptor.value === 'function') {
151
+ const originalFn = descriptor.value;
152
+ const wrappedFn = wrapMethod(originalFn);
153
+
154
+ Object.defineProperty(CombinedMachine.prototype, key, {
155
+ ...descriptor,
156
+ value: wrappedFn,
157
+ });
158
+ } else {
159
+ // Copy getters/setters/values as is
160
+ Object.defineProperty(CombinedMachine.prototype, key, descriptor);
161
+ }
162
+ }
163
+ }
164
+
165
+ return CombinedMachine as unknown as MachineUnionConstructor<T>;
166
+ }
167
+
168
+ // =============================================================================
169
+ // MACHINE EXCLUDE
170
+ // =============================================================================
171
+
172
+ /**
173
+ * Creates a new class that extends a Source machine but excludes methods defined in one or more Excluded classes.
174
+ *
175
+ * This is useful for "subtracting" functionality from a combined machine or
176
+ * creating a restricted view of a larger machine.
177
+ *
178
+ * @param Source - The class to extend and extract methods from.
179
+ * @param Excluded - One or more classes defining methods to remove.
180
+ * @returns A new class with the subset of methods.
181
+ *
182
+ * @example
183
+ * ```typescript
184
+ * class Admin extends MachineUnion(Viewer, Editor, Moderator) {}
185
+ * class Guest extends MachineExclude(Admin, Editor, Moderator) {}
186
+ * ```
187
+ */
188
+ export function MachineExclude<
189
+ S extends Constructor,
190
+ E extends Constructor[]
191
+ >(Source: S, ...Excluded: E) {
192
+ // The resulting type: Instance of Source Omit keys of Instance of Excluded[number]
193
+ // But we still need checking for Context compatibility
194
+ type SourceInstance = Instance<S>;
195
+ type ExcludedUnion = Instance<E[number]>;
196
+ // We must EXCLUDE 'context' from the keys to omit, otherwise we remove the context property!
197
+ type ResultInstance = Omit<SourceInstance, Exclude<keyof ExcludedUnion, 'context'>>;
198
+ type ResultContext = ExtractContext<SourceInstance>;
199
+
200
+ class ExcludedMachine extends MachineBase<ResultContext> {
201
+ constructor(context: ResultContext) {
202
+ super(context);
203
+ }
204
+ }
205
+
206
+ // 1. Copy everything from Source (flattened)
207
+ const sourceDescriptors = getAllPropertyDescriptors(Source.prototype);
208
+ for (const [key, descriptor] of Object.entries(sourceDescriptors)) {
209
+ if (key === 'constructor') continue;
210
+ // We bind/wrap methods if source was NOT already wrapped (e.g. if Source is plain A).
211
+ // If Source is already a MachineUnion, its methods are already wrapped to return Source.
212
+
213
+ if (typeof descriptor.value === 'function') {
214
+ const originalFn = descriptor.value;
215
+
216
+ // We wrap to ensure return type is ExcludedMachine (security/safety)
217
+ // Otherwise calling an allowed method might return the Source type,
218
+ // which would expose Excluded methods ("leaking" capabilities).
219
+ const wrappedFn = function (this: ExcludedMachine, ...args: any[]) {
220
+ const result = originalFn.apply(this, args);
221
+
222
+ // Re-wrap to ExcludedMachine to maintain restriction chain
223
+ if (result && typeof result === 'object' && 'context' in result) {
224
+ return new ExcludedMachine({ ...this.context, ...result.context });
225
+ }
226
+ return result;
227
+ }
228
+ Object.defineProperty(ExcludedMachine.prototype, key, { ...descriptor, value: wrappedFn });
229
+ } else {
230
+ Object.defineProperty(ExcludedMachine.prototype, key, descriptor);
231
+ }
232
+ }
233
+
234
+ // 2. Remove things from ALL Excluded classes
235
+ for (const Excl of Excluded) {
236
+ const excludedDescriptors = getAllPropertyDescriptors(Excl.prototype);
237
+ for (const key of Object.keys(excludedDescriptors)) {
238
+ if (Object.prototype.hasOwnProperty.call(ExcludedMachine.prototype, key)) {
239
+ // Technically strict delete, though wrapping above already protects return types.
240
+ // This cleaning is for runtime safety (property won't exist).
241
+ delete (ExcludedMachine.prototype as any)[key];
242
+ }
243
+ }
244
+ }
245
+
246
+ return ExcludedMachine as unknown as new (context: ResultContext) => ResultInstance;
247
+ }
248
+
249
+ // =============================================================================
250
+ // FUNCTIONAL HELPERS
251
+ // =============================================================================
252
+
253
+ /**
254
+ * Functional helper to combine multiple Machine instances into a single union instance.
255
+ *
256
+ * Automatically merges the contexts of all provided instances and creates a new
257
+ * `MachineUnion` class on the fly.
258
+ *
259
+ * @param instances - Variadic list of machine instances to combine.
260
+ * @returns A new instance of the combined machine.
261
+ *
262
+ * @example
263
+ * ```typescript
264
+ * const counter = new Counter({ count: 0 });
265
+ * const toggler = new Toggler({ active: true });
266
+ *
267
+ * const app = machineUnion(counter, toggler);
268
+ * app.increment().toggle(); // Works! logic merged.
269
+ * ```
270
+ */
271
+ export function machineUnion<T extends MachineBase<any>[]>(
272
+ ...instances: T
273
+ ): Instance<MachineUnionConstructor<{ [K in keyof T]: T[K] extends MachineBase<any> ? Constructor<T[K]> : never }>> {
274
+ const constructors = instances.map(i => i.constructor as Constructor);
275
+ const contexts = instances.map(i => i.context);
276
+ const mergedContext = Object.assign({}, ...contexts); // Shallow merge
277
+
278
+ const CombinedClass = MachineUnion(...constructors);
279
+ return new CombinedClass(mergedContext) as any;
280
+ }
281
+
282
+ /**
283
+ * Functional helper to create a restricted machine instance by excluding behaviors
284
+ * defined in other machine instances.
285
+ *
286
+ * @param source - The source machine instance.
287
+ * @param excluded - Variadic list of machine instances whose methods should be excluded from source.
288
+ * @returns A new instance restricted to the source's capabilities minus excluded ones.
289
+ *
290
+ * @example
291
+ * ```typescript
292
+ * const fullApp = new AppMachine({ count: 0, active: true });
293
+ * const guestApp = machineExclude(fullApp, new Toggler({ active: false }));
294
+ * // guestApp.toggle(); // Error!
295
+ * ```
296
+ */
297
+ export function machineExclude<S extends MachineBase<any>, E extends MachineBase<any>[]>(
298
+ source: S,
299
+ ...excluded: E
300
+ ) {
301
+ const sourceCtor = source.constructor as Constructor<S>;
302
+ const excludedCtors = excluded.map(e => e.constructor as Constructor<E[number]>);
303
+
304
+ const ExcludedClass = MachineExclude(sourceCtor, ...excludedCtors);
305
+
306
+ // Create instance with source's context (exclusions check prototype, not context)
307
+ return new ExcludedClass(source.context);
308
+ }