@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/README.md +71 -0
- package/dist/cjs/development/core.js.map +1 -1
- package/dist/cjs/development/index.js +175 -0
- package/dist/cjs/development/index.js.map +4 -4
- package/dist/cjs/production/index.js +3 -3
- package/dist/esm/development/core.js.map +1 -1
- package/dist/esm/development/index.js +175 -0
- package/dist/esm/development/index.js.map +4 -4
- package/dist/esm/production/index.js +3 -3
- package/dist/types/actor.d.ts +153 -0
- package/dist/types/actor.d.ts.map +1 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/matcher.d.ts +16 -9
- package/dist/types/matcher.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/actor.ts +284 -0
- package/src/index.ts +16 -2
- package/src/matcher.ts +37 -21
- package/src/react.ts +95 -124
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
|
-
}
|
|
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]
|
|
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
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
):
|
|
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>]:
|
|
172
|
-
machine:
|
|
173
|
-
) => machine is
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
535
|
-
(m:
|
|
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,
|