@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doeixd/machine",
3
- "version": "0.0.21",
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
+ }