@doeixd/machine 0.0.19 → 0.0.21

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).
@@ -1148,4 +1148,18 @@ export {
1148
1148
  type IsExhaustive,
1149
1149
  type WhenBuilder,
1150
1150
  type Matcher
1151
- } from './matcher';
1151
+ } from './matcher';
1152
+
1153
+ // =============================================================================
1154
+ // SECTION: ACTOR MODEL
1155
+ // =============================================================================
1156
+
1157
+ export {
1158
+ Actor,
1159
+ createActor,
1160
+ spawn,
1161
+ fromPromise,
1162
+ fromObservable,
1163
+ type ActorRef,
1164
+ type InspectionEvent
1165
+ } from './actor';
package/src/matcher.ts CHANGED
@@ -69,7 +69,7 @@ type CaseToName<C> = C extends MatcherCase<infer Name, any, any> ? Name : never;
69
69
  * Builds a mapping from case names to their machine types.
70
70
  */
71
71
  export type CasesToMapping<Cases extends readonly MatcherCase<any, any, any>[]> = {
72
- [C in Cases[number] as CaseToName<C>]: CaseToMachine<C>;
72
+ [C in Cases[number]as CaseToName<C>]: CaseToMachine<C>;
73
73
  };
74
74
 
75
75
  /**
@@ -106,10 +106,16 @@ export type ExhaustivenessMarker = {
106
106
  /**
107
107
  * Extracts machine types from an array of case handlers.
108
108
  */
109
- type HandledMachines<Handlers extends readonly any[]> =
110
- Handlers extends readonly [infer H, ...infer Rest]
111
- ? (H extends CaseHandler<any, infer M, any> ? M : never) | HandledMachines<Rest>
112
- : never;
109
+ type ExtractHandledMachines<H extends readonly any[]> =
110
+ H extends readonly [infer First, ...infer Rest]
111
+ ? (First extends CaseHandler<any, infer M, any> ? M : never) | ExtractHandledMachines<Rest>
112
+ : never;
113
+
114
+ /**
115
+ * Extracts return types from an array of case handlers.
116
+ */
117
+ type ExtractHandlerReturn<H extends readonly any[]> =
118
+ H extends readonly CaseHandler<any, any, infer R>[] ? R : never;
113
119
 
114
120
  /**
115
121
  * Checks if all machine types in Union have been handled.
@@ -117,11 +123,11 @@ type HandledMachines<Handlers extends readonly any[]> =
117
123
  */
118
124
  export type IsExhaustive<Union, Handled> =
119
125
  Exclude<Union, Handled> extends never
120
- ? true
121
- : {
122
- readonly __error: 'Non-exhaustive match - missing cases';
123
- readonly __missing: Exclude<Union, Handled>;
124
- };
126
+ ? true
127
+ : {
128
+ readonly __error: 'Non-exhaustive match - missing cases';
129
+ readonly __missing: Exclude<Union, Handled>;
130
+ };
125
131
 
126
132
  /**
127
133
  * Pattern matching builder returned by matcher.when().
@@ -146,11 +152,21 @@ export interface WhenBuilder<
146
152
  * );
147
153
  * ```
148
154
  */
155
+ /**
156
+ * Overload 1: Infer return type from handlers (Enables exhaustiveness checking).
157
+ */
158
+ is<H extends readonly CaseHandler<CaseNames<_Cases>, any, any>[]>(
159
+ ...handlers: [...H, ExhaustivenessMarker]
160
+ ): IsExhaustive<M, ExtractHandledMachines<H>> extends true
161
+ ? ExtractHandlerReturn<H>
162
+ : IsExhaustive<M, ExtractHandledMachines<H>>;
163
+
164
+ /**
165
+ * Overload 2: Explicit return type (No exhaustiveness checking).
166
+ */
149
167
  is<R>(
150
- ...handlers: [...any[], ExhaustivenessMarker]
151
- ): IsExhaustive<M, HandledMachines<typeof handlers>> extends true
152
- ? R
153
- : IsExhaustive<M, HandledMachines<typeof handlers>>;
168
+ ...handlers: [...CaseHandler<CaseNames<_Cases>, any, R>[], ExhaustivenessMarker]
169
+ ): R;
154
170
  }
155
171
 
156
172
  /**
@@ -168,9 +184,9 @@ export interface Matcher<Cases extends readonly MatcherCase<any, any, any>[]> {
168
184
  * ```
169
185
  */
170
186
  readonly is: {
171
- [Name in CaseNames<Cases>]: <M>(
172
- machine: M
173
- ) => machine is Extract<M, CasesToMapping<Cases>[Name]>;
187
+ [Name in CaseNames<Cases>]: (
188
+ machine: any
189
+ ) => machine is CasesToMapping<Cases>[Name];
174
190
  };
175
191
 
176
192
  /**
@@ -265,7 +281,7 @@ export function createMatcher<
265
281
  // API 1: Type Guards (using Proxy for dynamic property access)
266
282
  const isProxy = new Proxy({} as any, {
267
283
  get(_target, prop: string) {
268
- return function isGuard<M>(machine: M): machine is any {
284
+ return function isGuard(machine: any): machine is any {
269
285
  const caseConfig = nameToCase.get(prop);
270
286
  if (!caseConfig) {
271
287
  const available = Array.from(nameToCase.keys()).join(', ');
@@ -515,7 +531,7 @@ export function customCase<
515
531
  * ```
516
532
  */
517
533
  export function forContext<C extends object>() {
518
- type MachineWithContext = { readonly context: C };
534
+
519
535
 
520
536
  return {
521
537
  /**
@@ -531,8 +547,8 @@ export function forContext<C extends object>() {
531
547
  value: V
532
548
  ): MatcherCase<
533
549
  Name,
534
- MachineWithContext & { context: Extract<C, { [P in K]: V }> },
535
- (m: MachineWithContext) => m is MachineWithContext & { context: Extract<C, { [P in K]: V }> }
550
+ { readonly context: Extract<C, { [P in K]: V }> },
551
+ (m: { readonly context: C }) => m is { readonly context: Extract<C, { [P in K]: V }> }
536
552
  > {
537
553
  return [
538
554
  name,