@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/README.md +19 -0
- package/dist/cjs/development/core.js.map +1 -1
- package/dist/cjs/development/index.js +277 -0
- package/dist/cjs/development/index.js.map +4 -4
- package/dist/cjs/production/index.js +4 -4
- package/dist/esm/development/core.js.map +1 -1
- package/dist/esm/development/index.js +277 -0
- package/dist/esm/development/index.js.map +4 -4
- package/dist/esm/production/index.js +4 -4
- package/dist/types/actor.d.ts +153 -0
- package/dist/types/actor.d.ts.map +1 -0
- package/dist/types/index.d.ts +3 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/mixins.d.ts +118 -0
- package/dist/types/mixins.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/actor.ts +284 -0
- package/src/index.ts +18 -2
- package/src/mixins.ts +308 -0
- 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).
|
|
@@ -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
|
+
}
|