@doeixd/machine 0.0.21 → 0.0.23
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 +21 -0
- package/dist/cjs/development/core.js.map +1 -1
- package/dist/cjs/development/index.js +102 -0
- package/dist/cjs/development/index.js.map +3 -3
- package/dist/cjs/development/react.js +1969 -0
- package/dist/cjs/development/react.js.map +7 -0
- package/dist/cjs/production/index.js +3 -3
- package/dist/cjs/production/react.js +1 -0
- package/dist/esm/development/core.js.map +1 -1
- package/dist/esm/development/index.js +102 -0
- package/dist/esm/development/index.js.map +3 -3
- package/dist/esm/development/react.js +1955 -0
- package/dist/esm/development/react.js.map +7 -0
- package/dist/esm/production/index.js +3 -3
- package/dist/esm/production/react.js +1 -0
- package/dist/types/entry-react.d.ts +6 -0
- package/dist/types/entry-react.d.ts.map +1 -0
- package/dist/types/index.d.ts +1 -0
- 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/dist/types/react.d.ts +133 -0
- package/dist/types/react.d.ts.map +1 -0
- package/package.json +13 -1
- package/src/index.ts +2 -0
- package/src/mixins.ts +308 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@doeixd/machine",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.23",
|
|
4
4
|
"files": [
|
|
5
5
|
"dist",
|
|
6
6
|
"src"
|
|
@@ -124,6 +124,15 @@
|
|
|
124
124
|
},
|
|
125
125
|
"require": "./dist/cjs/production/extract.js",
|
|
126
126
|
"import": "./dist/esm/production/extract.js"
|
|
127
|
+
},
|
|
128
|
+
"./react": {
|
|
129
|
+
"types": "./dist/types/entry-react.d.ts",
|
|
130
|
+
"development": {
|
|
131
|
+
"require": "./dist/cjs/development/react.js",
|
|
132
|
+
"import": "./dist/esm/development/react.js"
|
|
133
|
+
},
|
|
134
|
+
"require": "./dist/cjs/production/react.js",
|
|
135
|
+
"import": "./dist/esm/production/react.js"
|
|
127
136
|
}
|
|
128
137
|
},
|
|
129
138
|
"typesVersions": {
|
|
@@ -133,6 +142,9 @@
|
|
|
133
142
|
],
|
|
134
143
|
"extract": [
|
|
135
144
|
"./dist/types/extract.d.ts"
|
|
145
|
+
],
|
|
146
|
+
"react": [
|
|
147
|
+
"./dist/types/entry-react.d.ts"
|
|
136
148
|
]
|
|
137
149
|
}
|
|
138
150
|
},
|
package/src/index.ts
CHANGED
|
@@ -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
|
// =============================================================================
|
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
|
+
}
|