@doeixd/machine 0.0.13 → 0.0.18
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 +77 -25
- package/dist/cjs/development/core.js +1852 -0
- package/dist/cjs/development/core.js.map +7 -0
- package/dist/cjs/development/index.js +1377 -1372
- package/dist/cjs/development/index.js.map +4 -4
- package/dist/cjs/production/core.js +1 -0
- package/dist/cjs/production/index.js +5 -5
- package/dist/esm/development/core.js +1829 -0
- package/dist/esm/development/core.js.map +7 -0
- package/dist/esm/development/index.js +1377 -1372
- package/dist/esm/development/index.js.map +4 -4
- package/dist/esm/production/core.js +1 -0
- package/dist/esm/production/index.js +5 -5
- package/dist/types/core.d.ts +18 -0
- package/dist/types/core.d.ts.map +1 -0
- package/dist/types/extract.d.ts +15 -1
- package/dist/types/extract.d.ts.map +1 -1
- package/dist/types/functional-combinators.d.ts +3 -5
- package/dist/types/functional-combinators.d.ts.map +1 -1
- package/dist/types/index.d.ts +254 -18
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/middleware/composition.d.ts +460 -0
- package/dist/types/middleware/composition.d.ts.map +1 -0
- package/dist/types/middleware/core.d.ts +196 -0
- package/dist/types/middleware/core.d.ts.map +1 -0
- package/dist/types/middleware/history.d.ts +54 -0
- package/dist/types/middleware/history.d.ts.map +1 -0
- package/dist/types/middleware/index.d.ts +10 -0
- package/dist/types/middleware/index.d.ts.map +1 -0
- package/dist/types/middleware/snapshot.d.ts +63 -0
- package/dist/types/middleware/snapshot.d.ts.map +1 -0
- package/dist/types/middleware/time-travel.d.ts +81 -0
- package/dist/types/middleware/time-travel.d.ts.map +1 -0
- package/package.json +19 -6
- package/src/core.ts +167 -0
- package/src/entry-react.ts +9 -0
- package/src/entry-solid.ts +9 -0
- package/src/extract.ts +61 -61
- package/src/functional-combinators.ts +3 -3
- package/src/generators.ts +6 -6
- package/src/index.ts +389 -101
- package/src/middleware/composition.ts +944 -0
- package/src/middleware/core.ts +573 -0
- package/src/middleware/history.ts +104 -0
- package/src/middleware/index.ts +13 -0
- package/src/middleware/snapshot.ts +153 -0
- package/src/middleware/time-travel.ts +236 -0
- package/src/middleware.ts +735 -1614
- package/src/prototype_functional.ts +46 -0
- package/src/reproduce_issue.ts +26 -0
- package/dist/types/middleware.d.ts +0 -1048
- package/dist/types/middleware.d.ts.map +0 -1
- package/dist/types/runtime-extract.d.ts +0 -53
- package/dist/types/runtime-extract.d.ts.map +0 -1
package/src/middleware.ts
CHANGED
|
@@ -57,12 +57,6 @@ export interface MiddlewareError<C extends object> {
|
|
|
57
57
|
* All hooks are optional - provide only the ones you need.
|
|
58
58
|
* @template C - The context object type
|
|
59
59
|
*/
|
|
60
|
-
/**
|
|
61
|
-
* Strongly typed middleware hooks with precise context and return types.
|
|
62
|
-
* All hooks are optional - provide only the ones you need.
|
|
63
|
-
*
|
|
64
|
-
* @template C - The machine context type for precise type inference
|
|
65
|
-
*/
|
|
66
60
|
export interface MiddlewareHooks<C extends object> {
|
|
67
61
|
/**
|
|
68
62
|
* Called before a transition executes.
|
|
@@ -70,15 +64,6 @@ export interface MiddlewareHooks<C extends object> {
|
|
|
70
64
|
*
|
|
71
65
|
* @param ctx - Transition context with machine state and transition details
|
|
72
66
|
* @returns void to continue, CANCEL to abort silently, or Promise for async validation
|
|
73
|
-
*
|
|
74
|
-
* @example
|
|
75
|
-
* ```typescript
|
|
76
|
-
* before: ({ transitionName, args, context }) => {
|
|
77
|
-
* if (transitionName === 'withdraw' && context.balance < args[0]) {
|
|
78
|
-
* throw new Error('Insufficient funds');
|
|
79
|
-
* }
|
|
80
|
-
* }
|
|
81
|
-
* ```
|
|
82
67
|
*/
|
|
83
68
|
before?: (ctx: MiddlewareContext<C>) => void | typeof CANCEL | Promise<void | typeof CANCEL>;
|
|
84
69
|
|
|
@@ -88,1112 +73,541 @@ export interface MiddlewareHooks<C extends object> {
|
|
|
88
73
|
* Cannot prevent the transition (it already happened).
|
|
89
74
|
*
|
|
90
75
|
* @param result - Transition result with before/after contexts and transition details
|
|
91
|
-
*
|
|
92
|
-
* @example
|
|
93
|
-
* ```typescript
|
|
94
|
-
* after: ({ transitionName, before, after }) => {
|
|
95
|
-
* console.log(`${transitionName}: ${before.count} -> ${after.count}`);
|
|
96
|
-
* }
|
|
97
|
-
* ```
|
|
98
76
|
*/
|
|
99
77
|
after?: (result: MiddlewareResult<C>) => void | Promise<void>;
|
|
100
78
|
|
|
101
79
|
/**
|
|
102
|
-
* Called
|
|
103
|
-
* Can be used for error
|
|
104
|
-
*
|
|
105
|
-
* @param error - Error context with transition details and the thrown error
|
|
106
|
-
* @returns
|
|
107
|
-
* - void/undefined/null: Re-throw the original error (default)
|
|
108
|
-
* - BaseMachine: Use this as fallback state instead of throwing
|
|
109
|
-
* - throw new Error: Transform the error
|
|
110
|
-
*
|
|
111
|
-
* @example
|
|
112
|
-
* ```typescript
|
|
113
|
-
* error: ({ transitionName, error, context }) => {
|
|
114
|
-
* // Log to error reporting service
|
|
115
|
-
* reportError(error, { transitionName, context });
|
|
80
|
+
* Called when a transition throws an error.
|
|
81
|
+
* Can be used for error reporting, recovery, etc.
|
|
116
82
|
*
|
|
117
|
-
*
|
|
118
|
-
* if (error.message.includes('network')) {
|
|
119
|
-
* return createMachine({ ...context, error: 'offline' }, transitions);
|
|
120
|
-
* }
|
|
121
|
-
* }
|
|
122
|
-
* ```
|
|
83
|
+
* @param error - Error context with transition details and error information
|
|
123
84
|
*/
|
|
124
|
-
error?: (error: MiddlewareError<C>) => void |
|
|
85
|
+
error?: (error: MiddlewareError<C>) => void | Promise<void>;
|
|
125
86
|
}
|
|
126
87
|
|
|
127
88
|
/**
|
|
128
|
-
* Options for middleware
|
|
89
|
+
* Options for configuring middleware behavior.
|
|
129
90
|
*/
|
|
130
91
|
export interface MiddlewareOptions {
|
|
131
|
-
/**
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
* Note: 'auto' mode provides the best of both worlds - zero overhead for sync transitions
|
|
138
|
-
* while seamlessly handling async ones when they occur.
|
|
139
|
-
* @default 'auto'
|
|
140
|
-
*/
|
|
141
|
-
mode?: 'sync' | 'async' | 'auto';
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Properties to exclude from middleware interception.
|
|
145
|
-
* Useful for excluding utility methods or getters.
|
|
146
|
-
* @default ['context']
|
|
147
|
-
*/
|
|
148
|
-
exclude?: string[];
|
|
92
|
+
/** Whether to continue execution if a hook throws an error */
|
|
93
|
+
continueOnError?: boolean;
|
|
94
|
+
/** Whether to log errors from hooks */
|
|
95
|
+
logErrors?: boolean;
|
|
96
|
+
/** Custom error handler for hook errors */
|
|
97
|
+
onError?: (error: Error, hookName: string, ctx: any) => void;
|
|
149
98
|
}
|
|
150
99
|
|
|
151
|
-
// =============================================================================
|
|
152
|
-
// SECTION: CANCELLATION SUPPORT
|
|
153
|
-
// =============================================================================
|
|
154
|
-
|
|
155
100
|
/**
|
|
156
|
-
*
|
|
157
|
-
* When returned, the transition will not execute and the current machine state is preserved.
|
|
158
|
-
*
|
|
159
|
-
* @example
|
|
160
|
-
* createMiddleware(machine, {
|
|
161
|
-
* before: ({ transitionName, context }) => {
|
|
162
|
-
* if (shouldCancel(context)) {
|
|
163
|
-
* return CANCEL; // Abort transition without throwing
|
|
164
|
-
* }
|
|
165
|
-
* }
|
|
166
|
-
* });
|
|
101
|
+
* Symbol used to cancel a transition from a before hook.
|
|
167
102
|
*/
|
|
168
103
|
export const CANCEL = Symbol('CANCEL');
|
|
169
104
|
|
|
170
105
|
// =============================================================================
|
|
171
|
-
// SECTION:
|
|
172
|
-
// =============================================================================
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Augmented machine type with history tracking capabilities.
|
|
176
|
-
* @template M - The base machine type
|
|
177
|
-
* @template C - The context type
|
|
178
|
-
*/
|
|
179
|
-
export type WithHistory<M extends BaseMachine<any>> = M & {
|
|
180
|
-
/** Array of recorded transition history entries */
|
|
181
|
-
history: HistoryEntry[];
|
|
182
|
-
/** Clear all history entries */
|
|
183
|
-
clearHistory: () => void;
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Augmented machine type with snapshot tracking capabilities.
|
|
188
|
-
* @template M - The base machine type
|
|
189
|
-
* @template C - The context type
|
|
190
|
-
*/
|
|
191
|
-
export type WithSnapshot<M extends BaseMachine<any>, C extends object = Context<M>> = M & {
|
|
192
|
-
/** Array of recorded context snapshots */
|
|
193
|
-
snapshots: ContextSnapshot<C>[];
|
|
194
|
-
/** Clear all snapshots */
|
|
195
|
-
clearSnapshots: () => void;
|
|
196
|
-
/** Restore machine to a previous context state */
|
|
197
|
-
restoreSnapshot: (context: C) => M;
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Augmented machine type with full time-travel debugging capabilities.
|
|
202
|
-
* Combines both history and snapshot tracking.
|
|
203
|
-
* @template M - The base machine type
|
|
204
|
-
* @template C - The context type
|
|
205
|
-
*/
|
|
206
|
-
export type WithTimeTravel<M extends BaseMachine<any>, C extends object = Context<M>> = M & {
|
|
207
|
-
/** Array of recorded transition history entries */
|
|
208
|
-
history: HistoryEntry[];
|
|
209
|
-
/** Array of recorded context snapshots */
|
|
210
|
-
snapshots: ContextSnapshot<C>[];
|
|
211
|
-
/** Clear all history and snapshots */
|
|
212
|
-
clearTimeTravel: () => void;
|
|
213
|
-
/** Restore machine to a previous context state */
|
|
214
|
-
restoreSnapshot: (context: C) => M;
|
|
215
|
-
/** Replay all transitions from a specific snapshot */
|
|
216
|
-
replayFrom: (snapshotIndex: number) => M;
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
// =============================================================================
|
|
220
|
-
// SECTION: CORE MIDDLEWARE FUNCTION
|
|
106
|
+
// SECTION: CORE MIDDLEWARE FUNCTIONS
|
|
221
107
|
// =============================================================================
|
|
222
108
|
|
|
223
109
|
/**
|
|
224
|
-
*
|
|
225
|
-
* Uses direct property wrapping for optimal performance (3x faster than Proxy).
|
|
226
|
-
*
|
|
227
|
-
* The middleware preserves:
|
|
228
|
-
* - Full type safety (return type matches input machine)
|
|
229
|
-
* - `this` binding for transitions
|
|
230
|
-
* - Async and sync transitions
|
|
231
|
-
* - Machine immutability
|
|
110
|
+
* Creates a middleware function that wraps machine transitions with hooks.
|
|
232
111
|
*
|
|
233
112
|
* @template M - The machine type
|
|
234
|
-
* @param machine - The machine to
|
|
235
|
-
* @param hooks - Middleware hooks
|
|
236
|
-
* @param options -
|
|
113
|
+
* @param machine - The machine to instrument
|
|
114
|
+
* @param hooks - Middleware hooks to execute
|
|
115
|
+
* @param options - Middleware configuration options
|
|
237
116
|
* @returns A new machine with middleware applied
|
|
238
|
-
*
|
|
239
|
-
* @example
|
|
240
|
-
* const instrumented = createMiddleware(counter, {
|
|
241
|
-
* before: ({ transitionName, context, args }) => {
|
|
242
|
-
* console.log(`→ ${transitionName}`, args);
|
|
243
|
-
* },
|
|
244
|
-
* after: ({ transitionName, prevContext, nextContext }) => {
|
|
245
|
-
* console.log(`✓ ${transitionName}`, nextContext);
|
|
246
|
-
* },
|
|
247
|
-
* error: ({ transitionName, error }) => {
|
|
248
|
-
* console.error(`✗ ${transitionName}:`, error);
|
|
249
|
-
* }
|
|
250
|
-
* });
|
|
251
117
|
*/
|
|
252
118
|
export function createMiddleware<M extends BaseMachine<any>>(
|
|
253
119
|
machine: M,
|
|
254
120
|
hooks: MiddlewareHooks<Context<M>>,
|
|
255
121
|
options: MiddlewareOptions = {}
|
|
256
122
|
): M {
|
|
257
|
-
const {
|
|
123
|
+
const { continueOnError = false, logErrors = true, onError } = options;
|
|
258
124
|
|
|
259
|
-
//
|
|
260
|
-
const
|
|
125
|
+
// Create a wrapped machine that intercepts all transition calls
|
|
126
|
+
const wrappedMachine: any = { ...machine };
|
|
261
127
|
|
|
262
|
-
// Copy
|
|
128
|
+
// Copy any extra properties from the original machine (for middleware composition)
|
|
263
129
|
for (const prop in machine) {
|
|
264
130
|
if (!Object.prototype.hasOwnProperty.call(machine, prop)) continue;
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
// Always copy context
|
|
269
|
-
if (prop === 'context') {
|
|
270
|
-
wrapped.context = value;
|
|
271
|
-
continue;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Skip excluded properties
|
|
275
|
-
if (exclude.includes(prop)) {
|
|
276
|
-
wrapped[prop] = value;
|
|
277
|
-
continue;
|
|
131
|
+
if (prop !== 'context' && typeof machine[prop] !== 'function') {
|
|
132
|
+
wrappedMachine[prop] = machine[prop];
|
|
278
133
|
}
|
|
279
|
-
|
|
280
|
-
// Skip non-functions and private methods
|
|
281
|
-
if (typeof value !== 'function' || prop.startsWith('_')) {
|
|
282
|
-
wrapped[prop] = value;
|
|
283
|
-
continue;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// Wrap transition function
|
|
287
|
-
wrapped[prop] = createTransitionWrapper(
|
|
288
|
-
prop,
|
|
289
|
-
value,
|
|
290
|
-
machine,
|
|
291
|
-
hooks,
|
|
292
|
-
mode
|
|
293
|
-
);
|
|
294
134
|
}
|
|
295
135
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
function
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
)
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
if (beforeResult === CANCEL) {
|
|
330
|
-
return machine; // Return current machine unchanged
|
|
331
|
-
}
|
|
332
|
-
// If before hook returns a promise in sync mode, throw
|
|
333
|
-
if (beforeResult instanceof Promise) {
|
|
334
|
-
throw new Error(
|
|
335
|
-
`Middleware mode is 'sync' but before hook returned Promise for transition: ${transitionName}`
|
|
336
|
-
);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// Execute the actual transition
|
|
341
|
-
const result = originalFn.call(this, ...args);
|
|
342
|
-
|
|
343
|
-
// If result is async, switch to async handling
|
|
344
|
-
if (result instanceof Promise) {
|
|
345
|
-
return handleAsyncResult(result, context);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// Call after hook (must be sync or throw)
|
|
349
|
-
if (hooks.after) {
|
|
350
|
-
const middlewareResult: MiddlewareResult<Context<M>> = {
|
|
351
|
-
transitionName,
|
|
352
|
-
prevContext: context,
|
|
353
|
-
nextContext: result.context,
|
|
354
|
-
args
|
|
355
|
-
};
|
|
356
|
-
const afterResult = hooks.after(middlewareResult);
|
|
357
|
-
if (afterResult instanceof Promise) {
|
|
358
|
-
throw new Error(
|
|
359
|
-
`Middleware mode is 'sync' but after hook returned Promise for transition: ${transitionName}`
|
|
360
|
-
);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
return result;
|
|
365
|
-
} catch (err) {
|
|
366
|
-
// Call error hook and check for fallback state
|
|
367
|
-
if (hooks.error) {
|
|
368
|
-
const middlewareError: MiddlewareError<Context<M>> = {
|
|
369
|
-
transitionName,
|
|
370
|
-
context,
|
|
371
|
-
args,
|
|
372
|
-
error: err as Error
|
|
373
|
-
};
|
|
374
|
-
const errorResult = hooks.error(middlewareError);
|
|
375
|
-
|
|
376
|
-
// Handle async error hook in sync mode
|
|
377
|
-
if (errorResult instanceof Promise) {
|
|
378
|
-
// Fire-and-forget for async error hooks in sync mode
|
|
379
|
-
errorResult.catch(() => {});
|
|
380
|
-
throw err; // Re-throw original error
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// Check if error hook returned a fallback machine
|
|
384
|
-
if (errorResult && typeof errorResult === 'object' && 'context' in errorResult) {
|
|
385
|
-
return errorResult as M; // Return fallback state
|
|
136
|
+
// Wrap each transition function
|
|
137
|
+
for (const prop in machine) {
|
|
138
|
+
if (!Object.prototype.hasOwnProperty.call(machine, prop)) continue;
|
|
139
|
+
const value = machine[prop];
|
|
140
|
+
if (typeof value === 'function' && prop !== 'context') {
|
|
141
|
+
wrappedMachine[prop] = function (this: any, ...args: any[]) {
|
|
142
|
+
const transitionName = prop;
|
|
143
|
+
const context = wrappedMachine.context;
|
|
144
|
+
|
|
145
|
+
// Helper function to execute the transition and after hooks
|
|
146
|
+
const executeTransition = () => {
|
|
147
|
+
// 2. Execute the actual transition
|
|
148
|
+
let nextMachine: any;
|
|
149
|
+
try {
|
|
150
|
+
nextMachine = value.apply(this, args);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
// 3. Execute error hooks if transition failed
|
|
153
|
+
if (hooks.error) {
|
|
154
|
+
try {
|
|
155
|
+
// Error hooks are called synchronously for now
|
|
156
|
+
hooks.error({
|
|
157
|
+
transitionName,
|
|
158
|
+
context,
|
|
159
|
+
args: [...args],
|
|
160
|
+
error: error as Error
|
|
161
|
+
});
|
|
162
|
+
} catch (hookError) {
|
|
163
|
+
if (!continueOnError) throw hookError;
|
|
164
|
+
if (logErrors) console.error(`Middleware error hook error for ${transitionName}:`, hookError);
|
|
165
|
+
onError?.(hookError as Error, 'error', { transitionName, context, args, error });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
throw error; // Re-throw the original error
|
|
386
169
|
}
|
|
387
|
-
}
|
|
388
170
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
171
|
+
// Check if the transition is async (returns a Promise)
|
|
172
|
+
if (nextMachine && typeof nextMachine.then === 'function') {
|
|
173
|
+
// For async transitions, we need to handle the after hooks after the promise resolves
|
|
174
|
+
const asyncResult = nextMachine.then((resolvedMachine: any) => {
|
|
175
|
+
// Execute after hooks with the resolved machine
|
|
176
|
+
if (hooks.after) {
|
|
177
|
+
try {
|
|
178
|
+
const result = hooks.after({
|
|
179
|
+
transitionName,
|
|
180
|
+
prevContext: context,
|
|
181
|
+
nextContext: resolvedMachine.context,
|
|
182
|
+
args: [...args]
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Handle async after hooks
|
|
186
|
+
if (result && typeof result.then === 'function') {
|
|
187
|
+
return result.then(() => resolvedMachine);
|
|
188
|
+
}
|
|
189
|
+
} catch (error) {
|
|
190
|
+
if (!continueOnError) throw error;
|
|
191
|
+
if (logErrors) console.error(`Middleware after hook error for ${transitionName}:`, error);
|
|
192
|
+
onError?.(error as Error, 'after', {
|
|
193
|
+
transitionName,
|
|
194
|
+
prevContext: context,
|
|
195
|
+
nextContext: resolvedMachine.context,
|
|
196
|
+
args
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return resolvedMachine;
|
|
201
|
+
});
|
|
393
202
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
203
|
+
return asyncResult;
|
|
204
|
+
} else {
|
|
205
|
+
// Synchronous transition
|
|
206
|
+
// 4. Execute after hooks
|
|
207
|
+
if (hooks.after) {
|
|
208
|
+
try {
|
|
209
|
+
const result = hooks.after({
|
|
210
|
+
transitionName,
|
|
211
|
+
prevContext: context,
|
|
212
|
+
nextContext: nextMachine.context,
|
|
213
|
+
args: [...args]
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Handle async after hooks
|
|
217
|
+
if (result && typeof result === 'object' && result && 'then' in result) {
|
|
218
|
+
return result.then(() => nextMachine).catch((error: Error) => {
|
|
219
|
+
if (!continueOnError) throw error;
|
|
220
|
+
if (logErrors) console.error(`Middleware after hook error for ${transitionName}:`, error);
|
|
221
|
+
onError?.(error, 'after', {
|
|
222
|
+
transitionName,
|
|
223
|
+
prevContext: context,
|
|
224
|
+
nextContext: nextMachine.context,
|
|
225
|
+
args
|
|
226
|
+
});
|
|
227
|
+
return nextMachine;
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
} catch (error) {
|
|
231
|
+
if (!continueOnError) throw error;
|
|
232
|
+
if (logErrors) console.error(`Middleware after hook error for ${transitionName}:`, error);
|
|
233
|
+
onError?.(error as Error, 'after', {
|
|
234
|
+
transitionName,
|
|
235
|
+
prevContext: context,
|
|
236
|
+
nextContext: nextMachine.context,
|
|
237
|
+
args
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
409
241
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
// Call error hook and check for fallback state
|
|
413
|
-
if (hooks.error) {
|
|
414
|
-
const middlewareError: MiddlewareError<Context<M>> = {
|
|
415
|
-
transitionName,
|
|
416
|
-
context: ctx,
|
|
417
|
-
args,
|
|
418
|
-
error: err as Error
|
|
419
|
-
};
|
|
420
|
-
const errorResult = await hooks.error(middlewareError);
|
|
421
|
-
|
|
422
|
-
// Check if error hook returned a fallback machine
|
|
423
|
-
if (errorResult && typeof errorResult === 'object' && 'context' in errorResult) {
|
|
424
|
-
return errorResult as M; // Return fallback state
|
|
242
|
+
// 5. Return the next machine state
|
|
243
|
+
return nextMachine;
|
|
425
244
|
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// Re-throw the error
|
|
429
|
-
throw err;
|
|
430
|
-
}
|
|
431
|
-
};
|
|
245
|
+
};
|
|
432
246
|
|
|
433
|
-
|
|
434
|
-
const executeAsyncTransition = async () => {
|
|
435
|
-
try {
|
|
436
|
-
// Call before hook
|
|
247
|
+
// 1. Execute before hooks synchronously if possible
|
|
437
248
|
if (hooks.before) {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
249
|
+
try {
|
|
250
|
+
const result = hooks.before({
|
|
251
|
+
transitionName,
|
|
252
|
+
context,
|
|
253
|
+
args: [...args]
|
|
254
|
+
});
|
|
444
255
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
256
|
+
// Handle async hooks
|
|
257
|
+
if (result && typeof result === 'object' && result && 'then' in result) {
|
|
258
|
+
// For async hooks, return a promise that executes the transition after
|
|
259
|
+
return result.then((hookResult: any) => {
|
|
260
|
+
if (hookResult === CANCEL) {
|
|
261
|
+
return wrappedMachine;
|
|
262
|
+
}
|
|
263
|
+
return executeTransition();
|
|
264
|
+
}).catch((error: Error) => {
|
|
265
|
+
if (!continueOnError) throw error;
|
|
266
|
+
if (logErrors) console.error(`Middleware before hook error for ${transitionName}:`, error);
|
|
267
|
+
onError?.(error, 'before', { transitionName, context, args });
|
|
268
|
+
return executeTransition();
|
|
269
|
+
});
|
|
270
|
+
}
|
|
458
271
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
args
|
|
467
|
-
error: err as Error
|
|
468
|
-
};
|
|
469
|
-
const errorResult = await hooks.error(middlewareError);
|
|
470
|
-
|
|
471
|
-
// Check if error hook returned a fallback machine
|
|
472
|
-
if (errorResult && typeof errorResult === 'object' && 'context' in errorResult) {
|
|
473
|
-
return errorResult as M; // Return fallback state
|
|
272
|
+
// Check if transition should be cancelled
|
|
273
|
+
if (result === CANCEL) {
|
|
274
|
+
return wrappedMachine; // Return the same machine instance
|
|
275
|
+
}
|
|
276
|
+
} catch (error) {
|
|
277
|
+
if (!continueOnError) throw error;
|
|
278
|
+
if (logErrors) console.error(`Middleware before hook error for ${transitionName}:`, error);
|
|
279
|
+
onError?.(error as Error, 'before', { transitionName, context, args });
|
|
474
280
|
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
// Re-throw the error
|
|
478
|
-
throw err;
|
|
479
|
-
}
|
|
480
|
-
};
|
|
481
|
-
|
|
482
|
-
// Choose execution mode
|
|
483
|
-
if (mode === 'async') {
|
|
484
|
-
// Force async execution
|
|
485
|
-
return executeAsyncTransition();
|
|
486
|
-
} else if (mode === 'sync') {
|
|
487
|
-
// Force sync execution
|
|
488
|
-
return executeSyncTransition();
|
|
489
|
-
} else {
|
|
490
|
-
// Auto mode (adaptive): Starts synchronously for zero overhead,
|
|
491
|
-
// but automatically switches to async if the transition returns a Promise.
|
|
492
|
-
// This provides optimal performance for sync transitions while
|
|
493
|
-
// seamlessly handling async ones when they occur.
|
|
494
|
-
return executeSyncTransition();
|
|
281
|
+
};
|
|
495
282
|
}
|
|
496
|
-
}
|
|
497
|
-
}
|
|
283
|
+
}
|
|
498
284
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
// =============================================================================
|
|
285
|
+
return wrappedMachine;
|
|
286
|
+
}
|
|
502
287
|
|
|
503
288
|
/**
|
|
504
|
-
*
|
|
505
|
-
* Useful for debugging and development.
|
|
289
|
+
* Creates a simple logging middleware that logs all transitions.
|
|
506
290
|
*
|
|
507
291
|
* @template M - The machine type
|
|
508
292
|
* @param machine - The machine to add logging to
|
|
509
|
-
* @param options -
|
|
293
|
+
* @param options - Logging configuration options
|
|
510
294
|
* @returns A new machine with logging middleware
|
|
511
|
-
*
|
|
512
|
-
* @example
|
|
513
|
-
* const logged = withLogging(counter);
|
|
514
|
-
* logged.increment(); // Console: "→ increment []" then "✓ increment { count: 1 }"
|
|
515
295
|
*/
|
|
516
296
|
export function withLogging<M extends BaseMachine<any>>(
|
|
517
297
|
machine: M,
|
|
518
298
|
options: {
|
|
519
|
-
|
|
520
|
-
logger?: (message: string, ...args: any[]) => void;
|
|
521
|
-
/** Include context in logs (default: true) */
|
|
522
|
-
includeContext?: boolean;
|
|
523
|
-
/** Include arguments in logs (default: true) */
|
|
299
|
+
logger?: (message: string) => void;
|
|
524
300
|
includeArgs?: boolean;
|
|
301
|
+
includeContext?: boolean;
|
|
525
302
|
} = {}
|
|
526
303
|
): M {
|
|
527
|
-
const {
|
|
528
|
-
logger = console.log,
|
|
529
|
-
includeContext = true,
|
|
530
|
-
includeArgs = true
|
|
531
|
-
} = options;
|
|
304
|
+
const { logger = console.log, includeArgs = false, includeContext = true } = options;
|
|
532
305
|
|
|
533
306
|
return createMiddleware(machine, {
|
|
534
307
|
before: ({ transitionName, args }) => {
|
|
535
|
-
const
|
|
536
|
-
logger(
|
|
308
|
+
const message = includeArgs ? `→ ${transitionName} [${args.join(', ')}]` : `→ ${transitionName}`;
|
|
309
|
+
logger(message);
|
|
537
310
|
},
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
311
|
+
after: ({ transitionName, nextContext }) => {
|
|
312
|
+
const contextStr = includeContext ? ` ${JSON.stringify(nextContext)}` : '';
|
|
313
|
+
logger(`✓ ${transitionName}${contextStr}`);
|
|
314
|
+
},
|
|
315
|
+
error: ({ transitionName, error }) => {
|
|
316
|
+
console.error(`[Machine] ${transitionName} failed:`, error);
|
|
541
317
|
}
|
|
542
318
|
});
|
|
543
319
|
}
|
|
544
320
|
|
|
545
321
|
/**
|
|
546
|
-
*
|
|
547
|
-
* Compatible with any analytics service (Segment, Mixpanel, GA, etc.).
|
|
322
|
+
* Creates analytics tracking middleware.
|
|
548
323
|
*
|
|
549
324
|
* @template M - The machine type
|
|
550
325
|
* @param machine - The machine to track
|
|
551
326
|
* @param track - Analytics tracking function
|
|
552
|
-
* @param options -
|
|
553
|
-
* @returns A new machine with analytics
|
|
554
|
-
*
|
|
555
|
-
* @example
|
|
556
|
-
* const tracked = withAnalytics(machine, (event, props) => {
|
|
557
|
-
* analytics.track(event, props);
|
|
558
|
-
* });
|
|
327
|
+
* @param options - Configuration options
|
|
328
|
+
* @returns A new machine with analytics tracking
|
|
559
329
|
*/
|
|
560
330
|
export function withAnalytics<M extends BaseMachine<any>>(
|
|
561
331
|
machine: M,
|
|
562
|
-
track: (event: string,
|
|
332
|
+
track: (event: string, data?: any) => void,
|
|
563
333
|
options: {
|
|
564
|
-
/** Prefix for event names (default: "state_transition") */
|
|
565
334
|
eventPrefix?: string;
|
|
566
|
-
/** Include previous context in properties (default: false) */
|
|
567
335
|
includePrevContext?: boolean;
|
|
568
|
-
/** Include arguments in properties (default: true) */
|
|
569
336
|
includeArgs?: boolean;
|
|
570
337
|
} = {}
|
|
571
338
|
): M {
|
|
572
|
-
const {
|
|
573
|
-
eventPrefix = 'state_transition',
|
|
574
|
-
includePrevContext = false,
|
|
575
|
-
includeArgs = true
|
|
576
|
-
} = options;
|
|
339
|
+
const { eventPrefix = 'state_transition', includePrevContext = false, includeArgs = false } = options;
|
|
577
340
|
|
|
578
341
|
return createMiddleware(machine, {
|
|
579
|
-
after:
|
|
580
|
-
const
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
properties.from = prevContext;
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
if (includeArgs && args.length > 0) {
|
|
590
|
-
properties.args = args;
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
await track(`${eventPrefix}.${transitionName}`, properties);
|
|
342
|
+
after: ({ transitionName, prevContext, nextContext, args }) => {
|
|
343
|
+
const event = `${eventPrefix}.${transitionName}`;
|
|
344
|
+
const data: any = { transition: transitionName };
|
|
345
|
+
if (includePrevContext) data.from = prevContext;
|
|
346
|
+
data.to = nextContext;
|
|
347
|
+
if (includeArgs) data.args = args;
|
|
348
|
+
track(event, data);
|
|
594
349
|
}
|
|
595
|
-
}
|
|
350
|
+
});
|
|
596
351
|
}
|
|
597
352
|
|
|
598
353
|
/**
|
|
599
|
-
*
|
|
600
|
-
* Throws an error if validation fails, preventing the transition.
|
|
354
|
+
* Creates validation middleware that runs before transitions.
|
|
601
355
|
*
|
|
602
356
|
* @template M - The machine type
|
|
603
357
|
* @param machine - The machine to validate
|
|
604
|
-
* @param
|
|
605
|
-
* @returns A new machine with validation
|
|
606
|
-
*
|
|
607
|
-
* @example
|
|
608
|
-
* const validated = withValidation(counter, ({ transitionName, context, args }) => {
|
|
609
|
-
* if (transitionName === 'decrement' && context.count === 0) {
|
|
610
|
-
* throw new Error('Cannot decrement below zero');
|
|
611
|
-
* }
|
|
612
|
-
* });
|
|
358
|
+
* @param validator - Validation function
|
|
359
|
+
* @returns A new machine with validation
|
|
613
360
|
*/
|
|
614
361
|
export function withValidation<M extends BaseMachine<any>>(
|
|
615
362
|
machine: M,
|
|
616
|
-
|
|
617
|
-
options?: Pick<MiddlewareOptions, 'mode'>
|
|
363
|
+
validator: (ctx: MiddlewareContext<Context<M>>) => boolean | void
|
|
618
364
|
): M {
|
|
619
365
|
return createMiddleware(machine, {
|
|
620
366
|
before: (ctx) => {
|
|
621
|
-
const result =
|
|
622
|
-
if (result instanceof Promise) {
|
|
623
|
-
return result.then(r => {
|
|
624
|
-
if (r === false) {
|
|
625
|
-
throw new Error(`Validation failed for transition: ${ctx.transitionName}`);
|
|
626
|
-
}
|
|
627
|
-
return undefined;
|
|
628
|
-
});
|
|
629
|
-
}
|
|
367
|
+
const result = validator(ctx);
|
|
630
368
|
if (result === false) {
|
|
631
369
|
throw new Error(`Validation failed for transition: ${ctx.transitionName}`);
|
|
632
370
|
}
|
|
633
|
-
return undefined;
|
|
634
371
|
}
|
|
635
|
-
}
|
|
372
|
+
});
|
|
636
373
|
}
|
|
637
374
|
|
|
638
375
|
/**
|
|
639
|
-
*
|
|
640
|
-
* Useful for implementing role-based access control (RBAC) in state machines.
|
|
376
|
+
* Creates permission-checking middleware.
|
|
641
377
|
*
|
|
642
378
|
* @template M - The machine type
|
|
643
379
|
* @param machine - The machine to protect
|
|
644
|
-
* @param
|
|
380
|
+
* @param checker - Permission checking function
|
|
645
381
|
* @returns A new machine with permission checks
|
|
646
|
-
*
|
|
647
|
-
* @example
|
|
648
|
-
* const protected = withPermissions(machine, (user) => ({ transitionName }) => {
|
|
649
|
-
* if (transitionName === 'delete' && user.role !== 'admin') {
|
|
650
|
-
* return false;
|
|
651
|
-
* }
|
|
652
|
-
* return true;
|
|
653
|
-
* });
|
|
654
382
|
*/
|
|
655
383
|
export function withPermissions<M extends BaseMachine<any>>(
|
|
656
384
|
machine: M,
|
|
657
|
-
|
|
658
|
-
options?: Pick<MiddlewareOptions, 'mode'>
|
|
385
|
+
checker: (ctx: MiddlewareContext<Context<M>>) => boolean
|
|
659
386
|
): M {
|
|
660
387
|
return createMiddleware(machine, {
|
|
661
388
|
before: (ctx) => {
|
|
662
|
-
|
|
663
|
-
if (result instanceof Promise) {
|
|
664
|
-
return result.then(allowed => {
|
|
665
|
-
if (!allowed) {
|
|
666
|
-
throw new Error(`Unauthorized transition: ${ctx.transitionName}`);
|
|
667
|
-
}
|
|
668
|
-
return undefined;
|
|
669
|
-
});
|
|
670
|
-
}
|
|
671
|
-
if (!result) {
|
|
389
|
+
if (!checker(ctx)) {
|
|
672
390
|
throw new Error(`Unauthorized transition: ${ctx.transitionName}`);
|
|
673
391
|
}
|
|
674
|
-
return undefined;
|
|
675
392
|
}
|
|
676
|
-
}
|
|
393
|
+
});
|
|
677
394
|
}
|
|
678
395
|
|
|
679
396
|
/**
|
|
680
|
-
*
|
|
681
|
-
* Compatible with Sentry, Bugsnag, Rollbar, etc.
|
|
397
|
+
* Creates error reporting middleware.
|
|
682
398
|
*
|
|
683
399
|
* @template M - The machine type
|
|
684
400
|
* @param machine - The machine to monitor
|
|
685
|
-
* @param
|
|
686
|
-
* @param options -
|
|
401
|
+
* @param reporter - Error reporting function
|
|
402
|
+
* @param options - Configuration options
|
|
687
403
|
* @returns A new machine with error reporting
|
|
688
|
-
*
|
|
689
|
-
* @example
|
|
690
|
-
* const monitored = withErrorReporting(machine, (error, context) => {
|
|
691
|
-
* Sentry.captureException(error, { extra: context });
|
|
692
|
-
* });
|
|
693
404
|
*/
|
|
694
405
|
export function withErrorReporting<M extends BaseMachine<any>>(
|
|
695
406
|
machine: M,
|
|
696
|
-
|
|
697
|
-
options: {
|
|
698
|
-
/** Include machine context in error report (default: true) */
|
|
699
|
-
includeContext?: boolean;
|
|
700
|
-
/** Include arguments in error report (default: true) */
|
|
701
|
-
includeArgs?: boolean;
|
|
702
|
-
/** Middleware execution mode */
|
|
703
|
-
mode?: MiddlewareOptions['mode'];
|
|
704
|
-
} = {}
|
|
407
|
+
reporter: (error: Error, ctx: any) => void,
|
|
408
|
+
options: { includeArgs?: boolean } = {}
|
|
705
409
|
): M {
|
|
706
|
-
const {
|
|
410
|
+
const { includeArgs = false } = options;
|
|
707
411
|
|
|
708
412
|
return createMiddleware(machine, {
|
|
709
|
-
error:
|
|
710
|
-
|
|
711
|
-
|
|
413
|
+
error: (errorCtx) => {
|
|
414
|
+
// Format the context to match test expectations
|
|
415
|
+
const formattedCtx = {
|
|
416
|
+
transition: errorCtx.transitionName,
|
|
417
|
+
context: errorCtx.context,
|
|
418
|
+
...(includeArgs && { args: errorCtx.args })
|
|
712
419
|
};
|
|
713
|
-
|
|
714
|
-
if (includeContext) {
|
|
715
|
-
errorContext.context = context;
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
if (includeArgs && args.length > 0) {
|
|
719
|
-
errorContext.args = args;
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
await Promise.resolve(captureError(error, errorContext));
|
|
420
|
+
reporter(errorCtx.error, formattedCtx);
|
|
723
421
|
}
|
|
724
|
-
}
|
|
422
|
+
});
|
|
725
423
|
}
|
|
726
424
|
|
|
727
425
|
/**
|
|
728
|
-
*
|
|
729
|
-
* Useful for identifying slow transitions and performance bottlenecks.
|
|
426
|
+
* Creates performance monitoring middleware.
|
|
730
427
|
*
|
|
731
428
|
* @template M - The machine type
|
|
732
429
|
* @param machine - The machine to monitor
|
|
733
|
-
* @param
|
|
430
|
+
* @param tracker - Performance tracking function
|
|
734
431
|
* @returns A new machine with performance monitoring
|
|
735
|
-
*
|
|
736
|
-
* @example
|
|
737
|
-
* const monitored = withPerformanceMonitoring(machine, ({ transition, duration }) => {
|
|
738
|
-
* if (duration > 100) {
|
|
739
|
-
* console.warn(`Slow transition: ${transition} took ${duration}ms`);
|
|
740
|
-
* }
|
|
741
|
-
* });
|
|
742
432
|
*/
|
|
743
433
|
export function withPerformanceMonitoring<M extends BaseMachine<any>>(
|
|
744
434
|
machine: M,
|
|
745
|
-
|
|
746
|
-
transitionName: string;
|
|
747
|
-
duration: number;
|
|
748
|
-
context: Readonly<Context<M>>;
|
|
749
|
-
}) => void | Promise<void>
|
|
435
|
+
tracker: (metric: { transitionName: string; duration: number; context: Context<M> }) => void
|
|
750
436
|
): M {
|
|
751
|
-
const
|
|
437
|
+
const startTimes = new Map<string, number>();
|
|
752
438
|
|
|
753
439
|
return createMiddleware(machine, {
|
|
754
|
-
before: (
|
|
755
|
-
|
|
756
|
-
return undefined;
|
|
440
|
+
before: (ctx) => {
|
|
441
|
+
startTimes.set(ctx.transitionName, Date.now());
|
|
757
442
|
},
|
|
758
|
-
after: (
|
|
759
|
-
const startTime =
|
|
443
|
+
after: (result) => {
|
|
444
|
+
const startTime = startTimes.get(result.transitionName);
|
|
760
445
|
if (startTime) {
|
|
761
|
-
const duration =
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
446
|
+
const duration = Date.now() - startTime;
|
|
447
|
+
startTimes.delete(result.transitionName);
|
|
448
|
+
// For test compatibility, pass a single object as expected
|
|
449
|
+
const testResult = {
|
|
450
|
+
transitionName: result.transitionName,
|
|
451
|
+
duration,
|
|
452
|
+
context: result.nextContext || result.prevContext
|
|
453
|
+
};
|
|
454
|
+
tracker(testResult);
|
|
767
455
|
}
|
|
768
|
-
return undefined;
|
|
769
456
|
}
|
|
770
|
-
}
|
|
457
|
+
});
|
|
771
458
|
}
|
|
772
459
|
|
|
773
460
|
/**
|
|
774
|
-
*
|
|
775
|
-
* Uses direct property wrapping for optimal performance.
|
|
776
|
-
* Useful for handling transient failures in async operations.
|
|
461
|
+
* Creates retry middleware for failed transitions.
|
|
777
462
|
*
|
|
778
463
|
* @template M - The machine type
|
|
779
464
|
* @param machine - The machine to add retry logic to
|
|
780
465
|
* @param options - Retry configuration
|
|
781
466
|
* @returns A new machine with retry logic
|
|
782
|
-
*
|
|
783
|
-
* @example
|
|
784
|
-
* const resilient = withRetry(machine, {
|
|
785
|
-
* maxRetries: 3,
|
|
786
|
-
* delay: 1000,
|
|
787
|
-
* shouldRetry: (error) => error.message.includes('network')
|
|
788
|
-
* });
|
|
789
467
|
*/
|
|
790
468
|
export function withRetry<M extends BaseMachine<any>>(
|
|
791
469
|
machine: M,
|
|
792
470
|
options: {
|
|
793
|
-
|
|
794
|
-
maxRetries?: number;
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
backoffMultiplier?: number;
|
|
799
|
-
|
|
800
|
-
shouldRetry?: (error: Error) => boolean;
|
|
801
|
-
/** Callback when retry occurs */
|
|
802
|
-
onRetry?: (attempt: number, error: Error) => void;
|
|
471
|
+
maxAttempts?: number;
|
|
472
|
+
maxRetries?: number; // alias for maxAttempts
|
|
473
|
+
shouldRetry?: (error: Error, attempt: number) => boolean;
|
|
474
|
+
backoffMs?: number | ((attempt: number) => number);
|
|
475
|
+
delay?: number | ((attempt: number) => number); // alias for backoffMs
|
|
476
|
+
backoffMultiplier?: number; // multiplier for exponential backoff
|
|
477
|
+
onRetry?: (error: Error, attempt: number) => void;
|
|
803
478
|
} = {}
|
|
804
479
|
): M {
|
|
805
480
|
const {
|
|
806
|
-
|
|
807
|
-
delay = 1000,
|
|
808
|
-
backoffMultiplier = 1,
|
|
481
|
+
maxAttempts = options.maxRetries ?? 3,
|
|
809
482
|
shouldRetry = () => true,
|
|
483
|
+
backoffMs = options.delay ?? 100,
|
|
484
|
+
backoffMultiplier = 2,
|
|
810
485
|
onRetry
|
|
811
486
|
} = options;
|
|
812
487
|
|
|
813
|
-
//
|
|
814
|
-
const
|
|
488
|
+
// Create a wrapped machine that adds retry logic
|
|
489
|
+
const wrappedMachine: any = { ...machine };
|
|
815
490
|
|
|
491
|
+
// Wrap each transition function with retry logic
|
|
816
492
|
for (const prop in machine) {
|
|
817
493
|
if (!Object.prototype.hasOwnProperty.call(machine, prop)) continue;
|
|
818
|
-
|
|
819
494
|
const value = machine[prop];
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
// Don't retry if shouldRetry returns false
|
|
843
|
-
if (!shouldRetry(lastError)) {
|
|
844
|
-
break;
|
|
495
|
+
if (typeof value === 'function' && prop !== 'context') {
|
|
496
|
+
wrappedMachine[prop] = async function (this: any, ...args: any[]) {
|
|
497
|
+
let lastError: Error;
|
|
498
|
+
let attempt = 0;
|
|
499
|
+
|
|
500
|
+
while (attempt < maxAttempts) {
|
|
501
|
+
try {
|
|
502
|
+
return await value.apply(this, args);
|
|
503
|
+
} catch (error) {
|
|
504
|
+
lastError = error as Error;
|
|
505
|
+
attempt++;
|
|
506
|
+
|
|
507
|
+
if (attempt < maxAttempts && shouldRetry(lastError, attempt)) {
|
|
508
|
+
onRetry?.(lastError, attempt);
|
|
509
|
+
const baseDelay = typeof backoffMs === 'function' ? backoffMs(attempt) : backoffMs;
|
|
510
|
+
const delay = baseDelay * Math.pow(backoffMultiplier, attempt - 1);
|
|
511
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
512
|
+
} else {
|
|
513
|
+
throw lastError;
|
|
514
|
+
}
|
|
845
515
|
}
|
|
846
|
-
|
|
847
|
-
// Call onRetry callback
|
|
848
|
-
onRetry?.(attempt + 1, lastError);
|
|
849
|
-
|
|
850
|
-
// Wait before retrying
|
|
851
|
-
const currentDelay = delay * Math.pow(backoffMultiplier, attempt);
|
|
852
|
-
await new Promise(resolve => setTimeout(resolve, currentDelay));
|
|
853
516
|
}
|
|
854
|
-
}
|
|
855
517
|
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
};
|
|
859
|
-
}
|
|
518
|
+
throw lastError!;
|
|
519
|
+
};
|
|
860
520
|
|
|
861
|
-
return wrapped as M;
|
|
862
|
-
}
|
|
863
521
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
/** Guard predicate function that returns true if transition is allowed */
|
|
869
|
-
guard: (ctx: MiddlewareContext<C>, ...args: any[]) => boolean | Promise<boolean>;
|
|
870
|
-
/**
|
|
871
|
-
* Action to take when guard fails.
|
|
872
|
-
* - 'throw': Throw an error (default)
|
|
873
|
-
* - 'ignore': Silently cancel the transition
|
|
874
|
-
*
|
|
875
|
-
* Note: For custom fallback machines, use the error hook in createMiddleware:
|
|
876
|
-
* @example
|
|
877
|
-
* createMiddleware(machine, {
|
|
878
|
-
* error: ({ error, context }) => {
|
|
879
|
-
* if (error.message.includes('Guard failed')) {
|
|
880
|
-
* return createMachine({ ...context, error: 'Unauthorized' }, machine);
|
|
881
|
-
* }
|
|
882
|
-
* }
|
|
883
|
-
* });
|
|
884
|
-
*/
|
|
885
|
-
onFail?: 'throw' | 'ignore';
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return wrappedMachine;
|
|
886
526
|
}
|
|
887
527
|
|
|
888
528
|
/**
|
|
889
|
-
*
|
|
890
|
-
* Implements fundamental FSM guard concept - transitions only occur when guards pass.
|
|
529
|
+
* Creates custom middleware from hooks.
|
|
891
530
|
*
|
|
892
531
|
* @template M - The machine type
|
|
893
|
-
* @param
|
|
894
|
-
* @param
|
|
895
|
-
* @returns A
|
|
896
|
-
*
|
|
897
|
-
* @example
|
|
898
|
-
* const guarded = withGuards(counter, {
|
|
899
|
-
* decrement: {
|
|
900
|
-
* guard: ({ context }) => context.count > 0,
|
|
901
|
-
* onFail: 'throw' // or 'ignore'
|
|
902
|
-
* },
|
|
903
|
-
* delete: {
|
|
904
|
-
* guard: ({ context }) => context.user?.isAdmin === true,
|
|
905
|
-
* onFail: 'throw'
|
|
906
|
-
* }
|
|
907
|
-
* });
|
|
908
|
-
*
|
|
909
|
-
* guarded.decrement(); // Throws if count === 0
|
|
910
|
-
*
|
|
911
|
-
* // For custom fallback machines, combine with error middleware:
|
|
912
|
-
* const guardedWithFallback = createMiddleware(guarded, {
|
|
913
|
-
* error: ({ error, context }) => {
|
|
914
|
-
* if (error.message.includes('Guard failed')) {
|
|
915
|
-
* return createMachine({ ...context, error: 'Unauthorized' }, machine);
|
|
916
|
-
* }
|
|
917
|
-
* }
|
|
918
|
-
* });
|
|
532
|
+
* @param hooks - Middleware hooks
|
|
533
|
+
* @param options - Middleware options
|
|
534
|
+
* @returns A middleware function
|
|
919
535
|
*/
|
|
920
|
-
export function
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
before: async (ctx) => {
|
|
927
|
-
const guardConfig = guards[ctx.transitionName];
|
|
928
|
-
if (!guardConfig) {
|
|
929
|
-
return undefined; // No guard for this transition
|
|
930
|
-
}
|
|
536
|
+
export function createCustomMiddleware<M extends BaseMachine<any>>(
|
|
537
|
+
hooks: MiddlewareHooks<Context<M>>,
|
|
538
|
+
options?: MiddlewareOptions
|
|
539
|
+
): (machine: M) => M {
|
|
540
|
+
return (machine: M) => createMiddleware(machine, hooks, options);
|
|
541
|
+
}
|
|
931
542
|
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
543
|
+
// =============================================================================
|
|
544
|
+
// SECTION: HISTORY TRACKING
|
|
545
|
+
// =============================================================================
|
|
935
546
|
|
|
936
|
-
|
|
937
|
-
|
|
547
|
+
/**
|
|
548
|
+
* A single history entry recording a transition.
|
|
549
|
+
*/
|
|
550
|
+
export interface HistoryEntry {
|
|
551
|
+
/** Unique ID for this history entry */
|
|
552
|
+
id: string;
|
|
553
|
+
/** Name of the transition that was called */
|
|
554
|
+
transitionName: string;
|
|
555
|
+
/** Arguments passed to the transition */
|
|
556
|
+
args: any[];
|
|
557
|
+
/** Timestamp when the transition occurred */
|
|
558
|
+
timestamp: number;
|
|
559
|
+
/** Optional serialized version of args for persistence */
|
|
560
|
+
serializedArgs?: string;
|
|
561
|
+
}
|
|
938
562
|
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
return undefined;
|
|
948
|
-
}
|
|
949
|
-
}, { mode: 'async', ...options });
|
|
563
|
+
/**
|
|
564
|
+
* Serializer interface for converting context/args to/from strings.
|
|
565
|
+
*/
|
|
566
|
+
export interface Serializer<T = any> {
|
|
567
|
+
serialize: (value: T) => string;
|
|
568
|
+
deserialize: (str: string) => T;
|
|
950
569
|
}
|
|
951
570
|
|
|
952
571
|
/**
|
|
953
|
-
* Creates
|
|
954
|
-
*
|
|
572
|
+
* Creates a machine with history tracking capabilities.
|
|
573
|
+
* Records all transitions that occur, allowing you to see the sequence of state changes.
|
|
955
574
|
*
|
|
956
575
|
* @template M - The machine type
|
|
957
|
-
* @param machine - The machine to
|
|
958
|
-
* @param
|
|
959
|
-
* @returns A new machine with
|
|
960
|
-
*
|
|
961
|
-
* @example
|
|
962
|
-
* const conditional = createConditionalMiddleware(counter, {
|
|
963
|
-
* only: ['delete', 'update'], // Only these transitions
|
|
964
|
-
* hooks: {
|
|
965
|
-
* before: ({ transitionName }) => console.log('Sensitive operation:', transitionName),
|
|
966
|
-
* after: ({ transitionName }) => auditLog(transitionName)
|
|
967
|
-
* }
|
|
968
|
-
* });
|
|
576
|
+
* @param machine - The machine to track
|
|
577
|
+
* @param options - Configuration options
|
|
578
|
+
* @returns A new machine with history tracking
|
|
969
579
|
*
|
|
970
580
|
* @example
|
|
971
|
-
*
|
|
972
|
-
*
|
|
973
|
-
* hooks: {
|
|
974
|
-
* before: ({ transitionName, args }) => validate(transitionName, args)
|
|
975
|
-
* }
|
|
976
|
-
* });
|
|
977
|
-
*/
|
|
978
|
-
export function createConditionalMiddleware<M extends BaseMachine<any>>(
|
|
979
|
-
machine: M,
|
|
980
|
-
config: {
|
|
981
|
-
/** Only apply to these transitions (mutually exclusive with except) */
|
|
982
|
-
only?: string[];
|
|
983
|
-
/** Apply to all except these transitions (mutually exclusive with only) */
|
|
984
|
-
except?: string[];
|
|
985
|
-
/** Middleware hooks to apply */
|
|
986
|
-
hooks: MiddlewareHooks<Context<M>>;
|
|
987
|
-
/** Middleware options */
|
|
988
|
-
options?: MiddlewareOptions;
|
|
989
|
-
}
|
|
990
|
-
): M {
|
|
991
|
-
const { only, except, hooks, options } = config;
|
|
992
|
-
|
|
993
|
-
if (only && except) {
|
|
994
|
-
throw new Error('Cannot specify both "only" and "except" - choose one');
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
// Create filter function
|
|
998
|
-
const shouldApply = (transitionName: string): boolean => {
|
|
999
|
-
if (only) {
|
|
1000
|
-
return only.includes(transitionName);
|
|
1001
|
-
}
|
|
1002
|
-
if (except) {
|
|
1003
|
-
return !except.includes(transitionName);
|
|
1004
|
-
}
|
|
1005
|
-
return true;
|
|
1006
|
-
};
|
|
1007
|
-
|
|
1008
|
-
// Wrap hooks to check filter
|
|
1009
|
-
const conditionalHooks: MiddlewareHooks<Context<M>> = {
|
|
1010
|
-
before: hooks.before
|
|
1011
|
-
? async (ctx) => {
|
|
1012
|
-
if (shouldApply(ctx.transitionName)) {
|
|
1013
|
-
return await hooks.before!(ctx);
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
: undefined,
|
|
1017
|
-
after: hooks.after
|
|
1018
|
-
? async (result) => {
|
|
1019
|
-
if (shouldApply(result.transitionName)) {
|
|
1020
|
-
return await hooks.after!(result);
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
: undefined,
|
|
1024
|
-
error: hooks.error
|
|
1025
|
-
? async (error) => {
|
|
1026
|
-
if (shouldApply(error.transitionName)) {
|
|
1027
|
-
return await hooks.error!(error);
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
: undefined
|
|
1031
|
-
};
|
|
1032
|
-
|
|
1033
|
-
return createMiddleware(machine, conditionalHooks, options);
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
/**
|
|
1037
|
-
* Creates state-dependent middleware that only applies when a predicate is true.
|
|
1038
|
-
* Allows middleware behavior to change based on current context/state.
|
|
1039
|
-
*
|
|
1040
|
-
* @template M - The machine type
|
|
1041
|
-
* @param machine - The machine to instrument
|
|
1042
|
-
* @param config - Configuration with predicate and hooks
|
|
1043
|
-
* @returns A new machine with state-dependent middleware
|
|
1044
|
-
*
|
|
1045
|
-
* @example
|
|
1046
|
-
* const stateful = createStateMiddleware(counter, {
|
|
1047
|
-
* when: (ctx) => ctx.debugMode === true,
|
|
1048
|
-
* hooks: {
|
|
1049
|
-
* before: (ctx) => console.log('Debug:', ctx),
|
|
1050
|
-
* after: (result) => console.log('Debug result:', result)
|
|
1051
|
-
* }
|
|
1052
|
-
* });
|
|
1053
|
-
*
|
|
1054
|
-
* // Logging only happens when context.debugMode === true
|
|
1055
|
-
*/
|
|
1056
|
-
export function createStateMiddleware<M extends BaseMachine<any>>(
|
|
1057
|
-
machine: M,
|
|
1058
|
-
config: {
|
|
1059
|
-
/** Predicate that determines if middleware should apply */
|
|
1060
|
-
when: (ctx: Context<M>) => boolean | Promise<boolean>;
|
|
1061
|
-
/** Middleware hooks to apply when predicate is true */
|
|
1062
|
-
hooks: MiddlewareHooks<Context<M>>;
|
|
1063
|
-
/** Middleware options */
|
|
1064
|
-
options?: MiddlewareOptions;
|
|
1065
|
-
}
|
|
1066
|
-
): M {
|
|
1067
|
-
const { when, hooks, options } = config;
|
|
1068
|
-
|
|
1069
|
-
// Wrap hooks to check predicate
|
|
1070
|
-
const conditionalHooks: MiddlewareHooks<Context<M>> = {
|
|
1071
|
-
before: hooks.before
|
|
1072
|
-
? async (ctx) => {
|
|
1073
|
-
if (await Promise.resolve(when(ctx.context))) {
|
|
1074
|
-
return await hooks.before!(ctx);
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
: undefined,
|
|
1078
|
-
after: hooks.after
|
|
1079
|
-
? async (result) => {
|
|
1080
|
-
if (await Promise.resolve(when(result.prevContext))) {
|
|
1081
|
-
return await hooks.after!(result);
|
|
1082
|
-
}
|
|
1083
|
-
}
|
|
1084
|
-
: undefined,
|
|
1085
|
-
error: hooks.error
|
|
1086
|
-
? async (error) => {
|
|
1087
|
-
if (await Promise.resolve(when(error.context))) {
|
|
1088
|
-
return await hooks.error!(error);
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
: undefined
|
|
1092
|
-
};
|
|
1093
|
-
|
|
1094
|
-
return createMiddleware(machine, conditionalHooks, options);
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
// =============================================================================
|
|
1098
|
-
// SECTION: HISTORY AND SNAPSHOT TRACKING
|
|
1099
|
-
// =============================================================================
|
|
1100
|
-
|
|
1101
|
-
/**
|
|
1102
|
-
* Represents a recorded transition call in the history.
|
|
1103
|
-
*/
|
|
1104
|
-
export interface HistoryEntry {
|
|
1105
|
-
/** Unique ID for this history entry */
|
|
1106
|
-
id: string;
|
|
1107
|
-
/** The transition that was called */
|
|
1108
|
-
transitionName: string;
|
|
1109
|
-
/** Arguments passed to the transition */
|
|
1110
|
-
args: any[];
|
|
1111
|
-
/** Timestamp when the transition was called */
|
|
1112
|
-
timestamp: number;
|
|
1113
|
-
/** Optional serialized version of args (if serializer provided) */
|
|
1114
|
-
serializedArgs?: string;
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
/**
|
|
1118
|
-
* Generic serializer/deserializer interface.
|
|
1119
|
-
* Used for serializing history arguments, context snapshots, etc.
|
|
1120
|
-
* @template T - The type being serialized
|
|
1121
|
-
*/
|
|
1122
|
-
export interface Serializer<T = any> {
|
|
1123
|
-
/** Serialize data to a string */
|
|
1124
|
-
serialize: (data: T) => string;
|
|
1125
|
-
/** Deserialize string back to data */
|
|
1126
|
-
deserialize: (serialized: string) => T;
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
/**
|
|
1130
|
-
* History tracking middleware that records all transition calls.
|
|
1131
|
-
* Useful for debugging, replay, undo/redo, and audit logging.
|
|
1132
|
-
*
|
|
1133
|
-
* @template M - The machine type
|
|
1134
|
-
* @param machine - The machine to track
|
|
1135
|
-
* @param options - Configuration options
|
|
1136
|
-
* @returns A new machine with history tracking and a history array
|
|
1137
|
-
*
|
|
1138
|
-
* Note: Arguments are shallow-cloned by default. If you need deep cloning or
|
|
1139
|
-
* serialization for persistence, provide a serializer:
|
|
1140
|
-
*
|
|
1141
|
-
* @example
|
|
1142
|
-
* const tracked = withHistory(counter, {
|
|
1143
|
-
* maxSize: 100,
|
|
1144
|
-
* serializer: {
|
|
1145
|
-
* serialize: (args) => JSON.stringify(args), // For deep clone or persistence
|
|
1146
|
-
* deserialize: (str) => JSON.parse(str)
|
|
1147
|
-
* }
|
|
1148
|
-
* });
|
|
1149
|
-
*
|
|
581
|
+
* ```typescript
|
|
582
|
+
* const tracked = withHistory(counter, { maxSize: 50 });
|
|
1150
583
|
* tracked.increment();
|
|
1151
|
-
* tracked.
|
|
1152
|
-
*
|
|
1153
|
-
* tracked.clearHistory(); // Clear history
|
|
584
|
+
* console.log(tracked.history); // [{ id: "entry-1", transitionName: "increment", ... }]
|
|
585
|
+
* ```
|
|
1154
586
|
*/
|
|
1155
587
|
export function withHistory<M extends BaseMachine<any>>(
|
|
1156
588
|
machine: M,
|
|
1157
589
|
options: {
|
|
1158
|
-
/** Maximum number of entries to keep (default: unlimited) */
|
|
590
|
+
/** Maximum number of history entries to keep (default: unlimited) */
|
|
1159
591
|
maxSize?: number;
|
|
1160
|
-
/** Optional serializer for arguments */
|
|
592
|
+
/** Optional serializer for transition arguments */
|
|
1161
593
|
serializer?: Serializer<any[]>;
|
|
1162
|
-
/**
|
|
1163
|
-
filter?: (transitionName: string, args: any[]) => boolean;
|
|
1164
|
-
/** Callback when new entry is added */
|
|
594
|
+
/** Callback when a transition occurs */
|
|
1165
595
|
onEntry?: (entry: HistoryEntry) => void;
|
|
1166
|
-
/** Internal flag to prevent rewrapping */
|
|
1167
|
-
_isRewrap?: boolean;
|
|
1168
596
|
} = {}
|
|
1169
|
-
):
|
|
1170
|
-
const {
|
|
1171
|
-
maxSize,
|
|
1172
|
-
serializer,
|
|
1173
|
-
filter,
|
|
1174
|
-
onEntry,
|
|
1175
|
-
_isRewrap = false
|
|
1176
|
-
} = options;
|
|
1177
|
-
|
|
597
|
+
): M & { history: HistoryEntry[]; clearHistory: () => void } {
|
|
598
|
+
const { maxSize, serializer, onEntry } = options;
|
|
1178
599
|
const history: HistoryEntry[] = [];
|
|
1179
600
|
let entryId = 0;
|
|
1180
601
|
|
|
1181
602
|
const instrumentedMachine = createMiddleware(machine, {
|
|
1182
603
|
before: ({ transitionName, args }) => {
|
|
1183
|
-
// Check filter
|
|
1184
|
-
if (filter && !filter(transitionName, args)) {
|
|
1185
|
-
return;
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
// Create entry
|
|
1189
604
|
const entry: HistoryEntry = {
|
|
1190
605
|
id: `entry-${entryId++}`,
|
|
1191
606
|
transitionName,
|
|
1192
|
-
args: [...args],
|
|
607
|
+
args: [...args],
|
|
1193
608
|
timestamp: Date.now()
|
|
1194
609
|
};
|
|
1195
610
|
|
|
1196
|
-
// Serialize if serializer provided
|
|
1197
611
|
if (serializer) {
|
|
1198
612
|
try {
|
|
1199
613
|
entry.serializedArgs = serializer.serialize(args);
|
|
@@ -1202,7 +616,6 @@ export function withHistory<M extends BaseMachine<any>>(
|
|
|
1202
616
|
}
|
|
1203
617
|
}
|
|
1204
618
|
|
|
1205
|
-
// Add to history
|
|
1206
619
|
history.push(entry);
|
|
1207
620
|
|
|
1208
621
|
// Enforce max size
|
|
@@ -1210,93 +623,28 @@ export function withHistory<M extends BaseMachine<any>>(
|
|
|
1210
623
|
history.shift();
|
|
1211
624
|
}
|
|
1212
625
|
|
|
1213
|
-
// Call callback
|
|
1214
626
|
onEntry?.(entry);
|
|
1215
627
|
}
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
// Override transitions to propagate history to returned machines
|
|
1219
|
-
if (!_isRewrap) {
|
|
1220
|
-
for (const prop in instrumentedMachine) {
|
|
1221
|
-
if (!Object.prototype.hasOwnProperty.call(instrumentedMachine, prop)) continue;
|
|
1222
|
-
const value = instrumentedMachine[prop];
|
|
1223
|
-
if (typeof value === 'function' && !prop.startsWith('_') && prop !== 'context' && !['history', 'clearHistory'].includes(prop)) {
|
|
1224
|
-
const originalFn = value;
|
|
1225
|
-
(instrumentedMachine as any)[prop] = function(this: any, ...args: any[]) {
|
|
1226
|
-
const result = originalFn.apply(this, args);
|
|
1227
|
-
// If result is a machine, re-wrap it with history tracking using the shared history array
|
|
1228
|
-
if (result && typeof result === 'object' && 'context' in result && !('history' in result)) {
|
|
1229
|
-
// Create a new wrapped machine that shares the same history array
|
|
1230
|
-
const rewrappedResult = createMiddleware(result, {
|
|
1231
|
-
before: ({ transitionName, args: transArgs }) => {
|
|
1232
|
-
// Check filter
|
|
1233
|
-
if (filter && !filter(transitionName, transArgs)) {
|
|
1234
|
-
return;
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
// Create entry
|
|
1238
|
-
const entry: HistoryEntry = {
|
|
1239
|
-
id: `entry-${entryId++}`,
|
|
1240
|
-
transitionName,
|
|
1241
|
-
args: [...transArgs],
|
|
1242
|
-
timestamp: Date.now()
|
|
1243
|
-
};
|
|
1244
|
-
|
|
1245
|
-
// Serialize if serializer provided
|
|
1246
|
-
if (serializer) {
|
|
1247
|
-
try {
|
|
1248
|
-
entry.serializedArgs = serializer.serialize(transArgs);
|
|
1249
|
-
} catch (err) {
|
|
1250
|
-
console.error('Failed to serialize history args:', err);
|
|
1251
|
-
}
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
// Add to history
|
|
1255
|
-
history.push(entry);
|
|
1256
|
-
|
|
1257
|
-
// Enforce max size
|
|
1258
|
-
if (maxSize && history.length > maxSize) {
|
|
1259
|
-
history.shift();
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
// Call callback
|
|
1263
|
-
onEntry?.(entry);
|
|
1264
|
-
}
|
|
1265
|
-
}, { exclude: ['context', 'history', 'clearHistory'] });
|
|
1266
|
-
|
|
1267
|
-
// Attach the shared history
|
|
1268
|
-
return Object.assign(rewrappedResult, {
|
|
1269
|
-
history,
|
|
1270
|
-
clearHistory: () => {
|
|
1271
|
-
history.length = 0;
|
|
1272
|
-
entryId = 0;
|
|
1273
|
-
}
|
|
1274
|
-
});
|
|
1275
|
-
}
|
|
1276
|
-
return result;
|
|
1277
|
-
};
|
|
1278
|
-
}
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
628
|
+
});
|
|
1281
629
|
|
|
1282
|
-
// Attach history
|
|
630
|
+
// Attach history properties to the machine
|
|
1283
631
|
return Object.assign(instrumentedMachine, {
|
|
1284
632
|
history,
|
|
1285
|
-
clearHistory: () => {
|
|
1286
|
-
history.length = 0;
|
|
1287
|
-
entryId = 0;
|
|
1288
|
-
}
|
|
633
|
+
clearHistory: () => { history.length = 0; entryId = 0; }
|
|
1289
634
|
});
|
|
1290
635
|
}
|
|
1291
636
|
|
|
637
|
+
// =============================================================================
|
|
638
|
+
// SECTION: SNAPSHOT TRACKING
|
|
639
|
+
// =============================================================================
|
|
640
|
+
|
|
1292
641
|
/**
|
|
1293
|
-
*
|
|
1294
|
-
* @template C - The context type
|
|
642
|
+
* A snapshot of machine context before and after a transition.
|
|
1295
643
|
*/
|
|
1296
|
-
export interface ContextSnapshot<C extends object
|
|
644
|
+
export interface ContextSnapshot<C extends object> {
|
|
1297
645
|
/** Unique ID for this snapshot */
|
|
1298
646
|
id: string;
|
|
1299
|
-
/**
|
|
647
|
+
/** Name of the transition that caused this snapshot */
|
|
1300
648
|
transitionName: string;
|
|
1301
649
|
/** Context before the transition */
|
|
1302
650
|
before: C;
|
|
@@ -1304,7 +652,7 @@ export interface ContextSnapshot<C extends object = any> {
|
|
|
1304
652
|
after: C;
|
|
1305
653
|
/** Timestamp of the snapshot */
|
|
1306
654
|
timestamp: number;
|
|
1307
|
-
/** Optional serialized
|
|
655
|
+
/** Optional serialized versions of contexts */
|
|
1308
656
|
serializedBefore?: string;
|
|
1309
657
|
serializedAfter?: string;
|
|
1310
658
|
/** Optional diff information */
|
|
@@ -1312,31 +660,27 @@ export interface ContextSnapshot<C extends object = any> {
|
|
|
1312
660
|
}
|
|
1313
661
|
|
|
1314
662
|
/**
|
|
1315
|
-
*
|
|
1316
|
-
*
|
|
663
|
+
* Creates a machine with snapshot tracking capabilities.
|
|
664
|
+
* Records context state before and after each transition for debugging and inspection.
|
|
1317
665
|
*
|
|
1318
666
|
* @template M - The machine type
|
|
1319
667
|
* @param machine - The machine to track
|
|
1320
668
|
* @param options - Configuration options
|
|
1321
|
-
* @returns A new machine with snapshot tracking
|
|
669
|
+
* @returns A new machine with snapshot tracking
|
|
1322
670
|
*
|
|
1323
671
|
* @example
|
|
672
|
+
* ```typescript
|
|
1324
673
|
* const tracked = withSnapshot(counter, {
|
|
1325
674
|
* maxSize: 50,
|
|
1326
675
|
* serializer: {
|
|
1327
676
|
* serialize: (ctx) => JSON.stringify(ctx),
|
|
1328
677
|
* deserialize: (str) => JSON.parse(str)
|
|
1329
|
-
* }
|
|
1330
|
-
* captureSnapshot: (before, after) => ({
|
|
1331
|
-
* changed: before.count !== after.count
|
|
1332
|
-
* })
|
|
678
|
+
* }
|
|
1333
679
|
* });
|
|
1334
680
|
*
|
|
1335
681
|
* tracked.increment();
|
|
1336
682
|
* console.log(tracked.snapshots); // [{ before: { count: 0 }, after: { count: 1 }, ... }]
|
|
1337
|
-
*
|
|
1338
|
-
* // Time-travel: restore to previous state
|
|
1339
|
-
* const previousState = tracked.restoreSnapshot(tracked.snapshots[0].before);
|
|
683
|
+
* ```
|
|
1340
684
|
*/
|
|
1341
685
|
export function withSnapshot<M extends BaseMachine<any>>(
|
|
1342
686
|
machine: M,
|
|
@@ -1348,26 +692,18 @@ export function withSnapshot<M extends BaseMachine<any>>(
|
|
|
1348
692
|
/** Custom function to capture additional snapshot data */
|
|
1349
693
|
captureSnapshot?: (before: Context<M>, after: Context<M>) => any;
|
|
1350
694
|
/** Only capture snapshots where context actually changed */
|
|
1351
|
-
|
|
1352
|
-
/** Filter function to exclude certain transitions from snapshots */
|
|
1353
|
-
filter?: (transitionName: string) => boolean;
|
|
1354
|
-
/** Callback when new snapshot is taken */
|
|
1355
|
-
onSnapshot?: (snapshot: ContextSnapshot<Context<M>>) => void;
|
|
1356
|
-
/** Additional properties to exclude from middleware (for composition) */
|
|
1357
|
-
_extraExclusions?: string[];
|
|
1358
|
-
/** Internal flag to prevent rewrapping */
|
|
1359
|
-
_isRewrap?: boolean;
|
|
695
|
+
onlyOnChange?: boolean;
|
|
1360
696
|
} = {}
|
|
1361
|
-
):
|
|
697
|
+
): M & {
|
|
698
|
+
snapshots: ContextSnapshot<Context<M>>[];
|
|
699
|
+
clearSnapshots: () => void;
|
|
700
|
+
restoreSnapshot: (snapshot: ContextSnapshot<Context<M>>['before']) => M;
|
|
701
|
+
} {
|
|
1362
702
|
const {
|
|
1363
703
|
maxSize,
|
|
1364
704
|
serializer,
|
|
1365
705
|
captureSnapshot,
|
|
1366
|
-
|
|
1367
|
-
filter,
|
|
1368
|
-
onSnapshot,
|
|
1369
|
-
_extraExclusions = [],
|
|
1370
|
-
_isRewrap = false
|
|
706
|
+
onlyOnChange = false
|
|
1371
707
|
} = options;
|
|
1372
708
|
|
|
1373
709
|
const snapshots: ContextSnapshot<Context<M>>[] = [];
|
|
@@ -1375,33 +711,24 @@ export function withSnapshot<M extends BaseMachine<any>>(
|
|
|
1375
711
|
|
|
1376
712
|
const instrumentedMachine = createMiddleware(machine, {
|
|
1377
713
|
after: ({ transitionName, prevContext, nextContext }) => {
|
|
1378
|
-
//
|
|
1379
|
-
if (
|
|
714
|
+
// Skip if only capturing on change and context didn't change
|
|
715
|
+
if (onlyOnChange && JSON.stringify(prevContext) === JSON.stringify(nextContext)) {
|
|
1380
716
|
return;
|
|
1381
717
|
}
|
|
1382
718
|
|
|
1383
|
-
// Check if changed (if required)
|
|
1384
|
-
if (onlyIfChanged) {
|
|
1385
|
-
const changed = JSON.stringify(prevContext) !== JSON.stringify(nextContext);
|
|
1386
|
-
if (!changed) {
|
|
1387
|
-
return;
|
|
1388
|
-
}
|
|
1389
|
-
}
|
|
1390
|
-
|
|
1391
|
-
// Create snapshot
|
|
1392
719
|
const snapshot: ContextSnapshot<Context<M>> = {
|
|
1393
720
|
id: `snapshot-${snapshotId++}`,
|
|
1394
721
|
transitionName,
|
|
1395
|
-
before: { ...prevContext
|
|
1396
|
-
after: { ...nextContext
|
|
722
|
+
before: { ...prevContext },
|
|
723
|
+
after: { ...nextContext },
|
|
1397
724
|
timestamp: Date.now()
|
|
1398
725
|
};
|
|
1399
726
|
|
|
1400
|
-
// Serialize if serializer provided
|
|
727
|
+
// Serialize contexts if serializer provided
|
|
1401
728
|
if (serializer) {
|
|
1402
729
|
try {
|
|
1403
|
-
snapshot.serializedBefore = serializer.serialize(prevContext
|
|
1404
|
-
snapshot.serializedAfter = serializer.serialize(nextContext
|
|
730
|
+
snapshot.serializedBefore = serializer.serialize(prevContext);
|
|
731
|
+
snapshot.serializedAfter = serializer.serialize(nextContext);
|
|
1405
732
|
} catch (err) {
|
|
1406
733
|
console.error('Failed to serialize snapshot:', err);
|
|
1407
734
|
}
|
|
@@ -1410,348 +737,273 @@ export function withSnapshot<M extends BaseMachine<any>>(
|
|
|
1410
737
|
// Capture custom snapshot data
|
|
1411
738
|
if (captureSnapshot) {
|
|
1412
739
|
try {
|
|
1413
|
-
snapshot.diff = captureSnapshot(prevContext
|
|
740
|
+
snapshot.diff = captureSnapshot(prevContext, nextContext);
|
|
1414
741
|
} catch (err) {
|
|
1415
742
|
console.error('Failed to capture snapshot:', err);
|
|
1416
743
|
}
|
|
1417
744
|
}
|
|
1418
745
|
|
|
1419
|
-
// Add to snapshots
|
|
1420
746
|
snapshots.push(snapshot);
|
|
1421
747
|
|
|
1422
748
|
// Enforce max size
|
|
1423
749
|
if (maxSize && snapshots.length > maxSize) {
|
|
1424
750
|
snapshots.shift();
|
|
1425
751
|
}
|
|
1426
|
-
|
|
1427
|
-
// Call callback
|
|
1428
|
-
onSnapshot?.(snapshot);
|
|
1429
752
|
}
|
|
1430
|
-
}
|
|
753
|
+
});
|
|
1431
754
|
|
|
1432
|
-
// Helper to restore machine to a previous
|
|
755
|
+
// Helper to restore machine to a previous state
|
|
1433
756
|
const restoreSnapshot = (context: Context<M>): M => {
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
const originalWrappedFn = value;
|
|
1445
|
-
(instrumentedMachine as any)[prop] = function(this: any, ...args: any[]) {
|
|
1446
|
-
const result = originalWrappedFn.apply(this, args);
|
|
1447
|
-
// If result is a machine, re-wrap it with snapshot tracking using the shared snapshots array
|
|
1448
|
-
if (result && typeof result === 'object' && 'context' in result && !('snapshots' in result)) {
|
|
1449
|
-
// Manually handle snapshot tracking without calling createMiddleware again
|
|
1450
|
-
// to avoid infinite recursion and complex wrapping issues
|
|
1451
|
-
|
|
1452
|
-
// Create a proxy that intercepts transition calls to record snapshots
|
|
1453
|
-
|
|
1454
|
-
// Wrap each transition to record snapshots
|
|
1455
|
-
for (const transProp in result) {
|
|
1456
|
-
if (!Object.prototype.hasOwnProperty.call(result, transProp)) continue;
|
|
1457
|
-
const transValue = result[transProp];
|
|
1458
|
-
if (typeof transValue === 'function' && !transProp.startsWith('_') && transProp !== 'context' && !['snapshots', 'clearSnapshots', 'restoreSnapshot', 'history', 'clearHistory'].includes(transProp)) {
|
|
1459
|
-
const origTransFn = transValue;
|
|
1460
|
-
(result as any)[transProp] = function(this: any, ...transArgs: any[]) {
|
|
1461
|
-
const prevCtx = result.context;
|
|
1462
|
-
const transResult = origTransFn.apply(this, transArgs);
|
|
1463
|
-
|
|
1464
|
-
// Record snapshot if we got a machine back
|
|
1465
|
-
if (transResult && typeof transResult === 'object' && 'context' in transResult) {
|
|
1466
|
-
const nextCtx = transResult.context;
|
|
1467
|
-
|
|
1468
|
-
// Check filter
|
|
1469
|
-
if (!(filter && !filter(transProp))) {
|
|
1470
|
-
// Check if changed (if required)
|
|
1471
|
-
let shouldRecord = true;
|
|
1472
|
-
if (onlyIfChanged) {
|
|
1473
|
-
const changed = JSON.stringify(prevCtx) !== JSON.stringify(nextCtx);
|
|
1474
|
-
shouldRecord = changed;
|
|
1475
|
-
}
|
|
1476
|
-
|
|
1477
|
-
if (shouldRecord) {
|
|
1478
|
-
// Create snapshot
|
|
1479
|
-
const snapshot: ContextSnapshot<Context<M>> = {
|
|
1480
|
-
id: `snapshot-${snapshotId++}`,
|
|
1481
|
-
transitionName: transProp,
|
|
1482
|
-
before: { ...prevCtx as Context<M> },
|
|
1483
|
-
after: { ...nextCtx as Context<M> },
|
|
1484
|
-
timestamp: Date.now()
|
|
1485
|
-
};
|
|
1486
|
-
|
|
1487
|
-
// Serialize if serializer provided
|
|
1488
|
-
if (serializer) {
|
|
1489
|
-
try {
|
|
1490
|
-
snapshot.serializedBefore = serializer.serialize(prevCtx as Context<M>);
|
|
1491
|
-
snapshot.serializedAfter = serializer.serialize(nextCtx as Context<M>);
|
|
1492
|
-
} catch (err) {
|
|
1493
|
-
console.error('Failed to serialize snapshot:', err);
|
|
1494
|
-
}
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
|
-
// Capture custom snapshot data
|
|
1498
|
-
if (captureSnapshot) {
|
|
1499
|
-
try {
|
|
1500
|
-
snapshot.diff = captureSnapshot(prevCtx as Context<M>, nextCtx as Context<M>);
|
|
1501
|
-
} catch (err) {
|
|
1502
|
-
console.error('Failed to capture snapshot:', err);
|
|
1503
|
-
}
|
|
1504
|
-
}
|
|
1505
|
-
|
|
1506
|
-
// Add to snapshots
|
|
1507
|
-
snapshots.push(snapshot);
|
|
1508
|
-
|
|
1509
|
-
// Enforce max size
|
|
1510
|
-
if (maxSize && snapshots.length > maxSize) {
|
|
1511
|
-
snapshots.shift();
|
|
1512
|
-
}
|
|
1513
|
-
|
|
1514
|
-
// Call callback
|
|
1515
|
-
onSnapshot?.(snapshot);
|
|
1516
|
-
}
|
|
1517
|
-
}
|
|
1518
|
-
}
|
|
1519
|
-
|
|
1520
|
-
return transResult;
|
|
1521
|
-
};
|
|
1522
|
-
}
|
|
1523
|
-
}
|
|
1524
|
-
|
|
1525
|
-
// Attach the shared snapshots and history
|
|
1526
|
-
const resultWithTracking = Object.assign(result, {
|
|
1527
|
-
snapshots,
|
|
1528
|
-
clearSnapshots: () => {
|
|
1529
|
-
snapshots.length = 0;
|
|
1530
|
-
snapshotId = 0;
|
|
1531
|
-
},
|
|
1532
|
-
restoreSnapshot
|
|
1533
|
-
});
|
|
1534
|
-
|
|
1535
|
-
// Also propagate history if it exists on the input machine
|
|
1536
|
-
if ((machine as any).history) {
|
|
1537
|
-
resultWithTracking.history = (machine as any).history;
|
|
1538
|
-
resultWithTracking.clearHistory = (machine as any).clearHistory;
|
|
1539
|
-
}
|
|
757
|
+
// Find the machine's transition functions (excluding context and snapshot properties)
|
|
758
|
+
const transitions = Object.fromEntries(
|
|
759
|
+
Object.entries(machine).filter(([key]) =>
|
|
760
|
+
key !== 'context' &&
|
|
761
|
+
key !== 'snapshots' &&
|
|
762
|
+
key !== 'clearSnapshots' &&
|
|
763
|
+
key !== 'restoreSnapshot' &&
|
|
764
|
+
typeof machine[key as keyof M] === 'function'
|
|
765
|
+
)
|
|
766
|
+
);
|
|
1540
767
|
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
return result;
|
|
1544
|
-
};
|
|
1545
|
-
}
|
|
1546
|
-
}
|
|
1547
|
-
}
|
|
768
|
+
return Object.assign({ context }, transitions) as M;
|
|
769
|
+
};
|
|
1548
770
|
|
|
1549
|
-
// Attach snapshot
|
|
771
|
+
// Attach snapshot properties to the machine
|
|
1550
772
|
return Object.assign(instrumentedMachine, {
|
|
1551
773
|
snapshots,
|
|
1552
|
-
clearSnapshots: () => {
|
|
1553
|
-
snapshots.length = 0;
|
|
1554
|
-
snapshotId = 0;
|
|
1555
|
-
},
|
|
774
|
+
clearSnapshots: () => { snapshots.length = 0; snapshotId = 0; },
|
|
1556
775
|
restoreSnapshot
|
|
1557
776
|
});
|
|
1558
777
|
}
|
|
1559
778
|
|
|
779
|
+
// =============================================================================
|
|
780
|
+
// SECTION: TIME TRAVEL DEBUGGING
|
|
781
|
+
// =============================================================================
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* A machine enhanced with history tracking capabilities.
|
|
785
|
+
*/
|
|
786
|
+
export type WithHistory<M extends BaseMachine<any>> = M & {
|
|
787
|
+
/** History of all transitions */
|
|
788
|
+
history: HistoryEntry[];
|
|
789
|
+
/** Clear all history */
|
|
790
|
+
clearHistory: () => void;
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* A machine enhanced with snapshot tracking capabilities.
|
|
795
|
+
*/
|
|
796
|
+
export type WithSnapshot<M extends BaseMachine<any>> = M & {
|
|
797
|
+
/** Snapshots of context before/after each transition */
|
|
798
|
+
snapshots: ContextSnapshot<Context<M>>[];
|
|
799
|
+
/** Clear all snapshots */
|
|
800
|
+
clearSnapshots: () => void;
|
|
801
|
+
/** Restore machine to a previous context state */
|
|
802
|
+
restoreSnapshot: (context: Context<M>) => M;
|
|
803
|
+
};
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* A machine enhanced with time travel capabilities.
|
|
807
|
+
*/
|
|
808
|
+
export type WithTimeTravel<M extends BaseMachine<any>> = M & {
|
|
809
|
+
/** History of all transitions */
|
|
810
|
+
history: HistoryEntry[];
|
|
811
|
+
/** Snapshots of context before/after each transition */
|
|
812
|
+
snapshots: ContextSnapshot<Context<M>>[];
|
|
813
|
+
/** Clear all history and snapshots */
|
|
814
|
+
clearTimeTravel: () => void;
|
|
815
|
+
/** Clear just the history */
|
|
816
|
+
clearHistory: () => void;
|
|
817
|
+
/** Clear just the snapshots */
|
|
818
|
+
clearSnapshots: () => void;
|
|
819
|
+
/** Restore machine to a previous context state */
|
|
820
|
+
restoreSnapshot: (context: Context<M>) => M;
|
|
821
|
+
/** Replay transitions from a specific point in history */
|
|
822
|
+
replayFrom: (startIndex: number) => M;
|
|
823
|
+
};
|
|
824
|
+
|
|
1560
825
|
/**
|
|
1561
|
-
*
|
|
1562
|
-
*
|
|
826
|
+
* Creates a machine with full time travel debugging capabilities.
|
|
827
|
+
* Combines history tracking, snapshots, and replay functionality.
|
|
1563
828
|
*
|
|
1564
829
|
* @template M - The machine type
|
|
1565
|
-
* @param machine - The machine to
|
|
830
|
+
* @param machine - The machine to enhance
|
|
1566
831
|
* @param options - Configuration options
|
|
1567
|
-
* @returns
|
|
832
|
+
* @returns A machine with time travel capabilities
|
|
1568
833
|
*
|
|
1569
834
|
* @example
|
|
1570
|
-
*
|
|
1571
|
-
*
|
|
1572
|
-
* serializer: {
|
|
1573
|
-
* serialize: (data) => JSON.stringify(data),
|
|
1574
|
-
* deserialize: (str) => JSON.parse(str)
|
|
1575
|
-
* }
|
|
1576
|
-
* });
|
|
1577
|
-
*
|
|
1578
|
-
* tracker.increment();
|
|
1579
|
-
* tracker.add(5);
|
|
1580
|
-
*
|
|
1581
|
-
* console.log(tracker.history); // All transitions
|
|
1582
|
-
* console.log(tracker.snapshots); // All state changes
|
|
835
|
+
* ```typescript
|
|
836
|
+
* const debugMachine = withTimeTravel(counter);
|
|
1583
837
|
*
|
|
1584
|
-
* //
|
|
1585
|
-
*
|
|
838
|
+
* // Make some transitions
|
|
839
|
+
* debugMachine.increment();
|
|
840
|
+
* debugMachine.increment();
|
|
841
|
+
* debugMachine.decrement();
|
|
1586
842
|
*
|
|
1587
|
-
* //
|
|
1588
|
-
* const
|
|
843
|
+
* // Time travel to previous states
|
|
844
|
+
* const previousState = debugMachine.replayFrom(0); // Replay from start
|
|
845
|
+
* const undoLast = debugMachine.restoreSnapshot(debugMachine.snapshots[1].before);
|
|
1589
846
|
*
|
|
1590
|
-
* //
|
|
1591
|
-
*
|
|
847
|
+
* // Inspect history
|
|
848
|
+
* console.log(debugMachine.history);
|
|
849
|
+
* console.log(debugMachine.snapshots);
|
|
850
|
+
* ```
|
|
1592
851
|
*/
|
|
1593
852
|
export function withTimeTravel<M extends BaseMachine<any>>(
|
|
1594
853
|
machine: M,
|
|
1595
854
|
options: {
|
|
1596
|
-
/** Maximum
|
|
855
|
+
/** Maximum number of history entries/snapshots to keep */
|
|
1597
856
|
maxSize?: number;
|
|
1598
|
-
/**
|
|
1599
|
-
serializer?: Serializer
|
|
1600
|
-
/** Callback
|
|
857
|
+
/** Optional serializer for persistence */
|
|
858
|
+
serializer?: Serializer;
|
|
859
|
+
/** Callback when history/snapshot events occur */
|
|
1601
860
|
onRecord?: (type: 'history' | 'snapshot', data: any) => void;
|
|
1602
861
|
} = {}
|
|
1603
|
-
): WithTimeTravel<M
|
|
862
|
+
): WithTimeTravel<M> {
|
|
1604
863
|
const { maxSize, serializer, onRecord } = options;
|
|
1605
864
|
|
|
865
|
+
// Create separate history and snapshot tracking
|
|
1606
866
|
const history: HistoryEntry[] = [];
|
|
1607
867
|
const snapshots: ContextSnapshot<Context<M>>[] = [];
|
|
1608
|
-
let
|
|
868
|
+
let historyId = 0;
|
|
1609
869
|
let snapshotId = 0;
|
|
1610
870
|
|
|
1611
|
-
//
|
|
1612
|
-
const
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
871
|
+
// Create middleware that handles both history and snapshots
|
|
872
|
+
const instrumentedMachine = createMiddleware(machine, {
|
|
873
|
+
before: ({ transitionName, args }) => {
|
|
874
|
+
const entry: HistoryEntry = {
|
|
875
|
+
id: `entry-${historyId++}`,
|
|
876
|
+
transitionName,
|
|
877
|
+
args: [...args],
|
|
878
|
+
timestamp: Date.now()
|
|
879
|
+
};
|
|
1619
880
|
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
881
|
+
if (serializer) {
|
|
882
|
+
try {
|
|
883
|
+
entry.serializedArgs = serializer.serialize(args);
|
|
884
|
+
} catch (err) {
|
|
885
|
+
console.error('Failed to serialize history args:', err);
|
|
886
|
+
}
|
|
1625
887
|
}
|
|
1626
|
-
}
|
|
1627
888
|
|
|
1628
|
-
|
|
1629
|
-
if (maxSize && history.length > maxSize) {
|
|
1630
|
-
history.shift();
|
|
1631
|
-
}
|
|
889
|
+
history.push(entry);
|
|
1632
890
|
|
|
1633
|
-
|
|
1634
|
-
|
|
891
|
+
// Enforce max size
|
|
892
|
+
if (maxSize && history.length > maxSize) {
|
|
893
|
+
history.shift();
|
|
894
|
+
}
|
|
1635
895
|
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
896
|
+
onRecord?.('history', entry);
|
|
897
|
+
},
|
|
898
|
+
after: ({ transitionName, prevContext, nextContext }) => {
|
|
899
|
+
const snapshot: ContextSnapshot<Context<M>> = {
|
|
900
|
+
id: `snapshot-${snapshotId++}`,
|
|
901
|
+
transitionName,
|
|
902
|
+
before: { ...prevContext },
|
|
903
|
+
after: { ...nextContext },
|
|
904
|
+
timestamp: Date.now()
|
|
905
|
+
};
|
|
1644
906
|
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
907
|
+
// Serialize contexts if serializer provided
|
|
908
|
+
if (serializer) {
|
|
909
|
+
try {
|
|
910
|
+
snapshot.serializedBefore = serializer.serialize(prevContext);
|
|
911
|
+
snapshot.serializedAfter = serializer.serialize(nextContext);
|
|
912
|
+
} catch (err) {
|
|
913
|
+
console.error('Failed to serialize snapshot:', err);
|
|
914
|
+
}
|
|
1651
915
|
}
|
|
1652
|
-
}
|
|
1653
|
-
|
|
1654
|
-
snapshots.push(snapshot);
|
|
1655
|
-
if (maxSize && snapshots.length > maxSize) {
|
|
1656
|
-
snapshots.shift();
|
|
1657
|
-
}
|
|
1658
916
|
|
|
1659
|
-
|
|
1660
|
-
};
|
|
917
|
+
snapshots.push(snapshot);
|
|
1661
918
|
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
};
|
|
919
|
+
// Enforce max size
|
|
920
|
+
if (maxSize && snapshots.length > maxSize) {
|
|
921
|
+
snapshots.shift();
|
|
922
|
+
}
|
|
1667
923
|
|
|
1668
|
-
|
|
1669
|
-
const replayFrom = (snapshotIndex: number = 0): M => {
|
|
1670
|
-
if (snapshotIndex < 0 || snapshotIndex >= snapshots.length) {
|
|
1671
|
-
throw new Error(`Invalid snapshot index: ${snapshotIndex}`);
|
|
924
|
+
onRecord?.('snapshot', snapshot);
|
|
1672
925
|
}
|
|
926
|
+
});
|
|
1673
927
|
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
// Find the
|
|
1677
|
-
const
|
|
1678
|
-
|
|
1679
|
-
|
|
928
|
+
// Helper to restore machine to a previous state
|
|
929
|
+
const restoreSnapshot = (context: Context<M>): M => {
|
|
930
|
+
// Find the machine's transition functions (excluding context and snapshot properties)
|
|
931
|
+
const transitions = Object.fromEntries(
|
|
932
|
+
Object.entries(machine).filter(([key]) =>
|
|
933
|
+
key !== 'context' &&
|
|
934
|
+
key !== 'history' &&
|
|
935
|
+
key !== 'snapshots' &&
|
|
936
|
+
key !== 'clearHistory' &&
|
|
937
|
+
key !== 'clearSnapshots' &&
|
|
938
|
+
key !== 'restoreSnapshot' &&
|
|
939
|
+
key !== 'clearTimeTravel' &&
|
|
940
|
+
key !== 'replayFrom' &&
|
|
941
|
+
typeof machine[key as keyof M] === 'function'
|
|
942
|
+
)
|
|
1680
943
|
);
|
|
1681
944
|
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
}
|
|
1685
|
-
|
|
1686
|
-
// Replay all transitions from that point
|
|
1687
|
-
for (let i = historyStartIndex; i < history.length; i++) {
|
|
1688
|
-
const entry = history[i];
|
|
1689
|
-
const transition = (current as any)[entry.transitionName];
|
|
945
|
+
return Object.assign({ context }, transitions) as M;
|
|
946
|
+
};
|
|
1690
947
|
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
console.error(`Replay failed at step ${i}:`, err);
|
|
1696
|
-
throw err;
|
|
1697
|
-
}
|
|
1698
|
-
}
|
|
948
|
+
// Create replay functionality
|
|
949
|
+
const replayFrom = (startIndex: number): M => {
|
|
950
|
+
if (startIndex < 0 || startIndex >= history.length) {
|
|
951
|
+
throw new Error(`Invalid replay start index: ${startIndex}`);
|
|
1699
952
|
}
|
|
1700
953
|
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
const wrapped: any = { ...machine };
|
|
1707
|
-
|
|
1708
|
-
// Wrap transition functions
|
|
1709
|
-
for (const prop in machine) {
|
|
1710
|
-
if (!Object.prototype.hasOwnProperty.call(machine, prop)) continue;
|
|
1711
|
-
const value = machine[prop];
|
|
1712
|
-
if (typeof value === 'function' && !prop.startsWith('_') && prop !== 'context' &&
|
|
1713
|
-
!['history', 'snapshots', 'clearHistory', 'clearSnapshots', 'clearTimeTravel', 'restoreSnapshot', 'replayFrom'].includes(prop)) {
|
|
1714
|
-
wrapped[prop] = function(this: any, ...args: any[]) {
|
|
1715
|
-
// Record history before transition
|
|
1716
|
-
recordHistory(prop, args);
|
|
1717
|
-
|
|
1718
|
-
const prevContext = wrapped.context;
|
|
1719
|
-
const result = value.apply(this, args);
|
|
1720
|
-
|
|
1721
|
-
// Record snapshot after transition
|
|
1722
|
-
if (result && typeof result === 'object' && 'context' in result) {
|
|
1723
|
-
recordSnapshot(prop, prevContext, result.context);
|
|
1724
|
-
}
|
|
954
|
+
// Start from the context at the specified history index
|
|
955
|
+
let currentContext = snapshots[startIndex]?.before;
|
|
956
|
+
if (!currentContext) {
|
|
957
|
+
throw new Error(`No snapshot available for index ${startIndex}`);
|
|
958
|
+
}
|
|
1725
959
|
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
960
|
+
// Get all transitions from start index to end
|
|
961
|
+
const transitionsToReplay = history.slice(startIndex);
|
|
962
|
+
|
|
963
|
+
// Create a fresh machine instance
|
|
964
|
+
const freshMachine = Object.assign(
|
|
965
|
+
{ context: currentContext },
|
|
966
|
+
Object.fromEntries(
|
|
967
|
+
Object.entries(machine).filter(([key]) =>
|
|
968
|
+
key !== 'context' &&
|
|
969
|
+
typeof machine[key as keyof M] === 'function'
|
|
970
|
+
)
|
|
971
|
+
)
|
|
972
|
+
) as M;
|
|
973
|
+
|
|
974
|
+
// Replay each transition
|
|
975
|
+
let replayedMachine = freshMachine;
|
|
976
|
+
for (const entry of transitionsToReplay) {
|
|
977
|
+
const transitionFn = replayedMachine[entry.transitionName as keyof M] as Function;
|
|
978
|
+
if (transitionFn) {
|
|
979
|
+
replayedMachine = transitionFn.apply(replayedMachine.context, entry.args);
|
|
1732
980
|
}
|
|
1733
981
|
}
|
|
1734
982
|
|
|
1735
|
-
|
|
1736
|
-
return Object.assign(wrapped, {
|
|
1737
|
-
history,
|
|
1738
|
-
snapshots,
|
|
1739
|
-
clearHistory: () => { history.length = 0; entryId = 0; },
|
|
1740
|
-
clearSnapshots: () => { snapshots.length = 0; snapshotId = 0; },
|
|
1741
|
-
clearTimeTravel: () => {
|
|
1742
|
-
history.length = 0;
|
|
1743
|
-
snapshots.length = 0;
|
|
1744
|
-
entryId = 0;
|
|
1745
|
-
snapshotId = 0;
|
|
1746
|
-
},
|
|
1747
|
-
restoreSnapshot,
|
|
1748
|
-
replayFrom
|
|
1749
|
-
});
|
|
983
|
+
return replayedMachine;
|
|
1750
984
|
};
|
|
1751
985
|
|
|
1752
|
-
|
|
986
|
+
// Return machine with all time travel capabilities
|
|
987
|
+
return Object.assign(instrumentedMachine, {
|
|
988
|
+
history,
|
|
989
|
+
snapshots,
|
|
990
|
+
clearHistory: () => { history.length = 0; historyId = 0; },
|
|
991
|
+
clearSnapshots: () => { snapshots.length = 0; snapshotId = 0; },
|
|
992
|
+
clearTimeTravel: () => {
|
|
993
|
+
history.length = 0;
|
|
994
|
+
snapshots.length = 0;
|
|
995
|
+
historyId = 0;
|
|
996
|
+
snapshotId = 0;
|
|
997
|
+
},
|
|
998
|
+
restoreSnapshot,
|
|
999
|
+
replayFrom
|
|
1000
|
+
}) as WithTimeTravel<M>;
|
|
1753
1001
|
}
|
|
1754
1002
|
|
|
1003
|
+
// =============================================================================
|
|
1004
|
+
// SECTION: MIDDLEWARE COMPOSITION
|
|
1005
|
+
// =============================================================================
|
|
1006
|
+
|
|
1755
1007
|
/**
|
|
1756
1008
|
* Compose multiple middleware functions into a single middleware stack.
|
|
1757
1009
|
* Middleware is applied left-to-right (first middleware wraps outermost).
|
|
@@ -1760,15 +1012,6 @@ export function withTimeTravel<M extends BaseMachine<any>>(
|
|
|
1760
1012
|
* @param machine - The base machine
|
|
1761
1013
|
* @param middlewares - Array of middleware functions
|
|
1762
1014
|
* @returns A new machine with all middleware applied
|
|
1763
|
-
*
|
|
1764
|
-
* @example
|
|
1765
|
-
* const instrumented = compose(
|
|
1766
|
-
* counter,
|
|
1767
|
-
* withLogging,
|
|
1768
|
-
* withAnalytics(analytics.track),
|
|
1769
|
-
* withValidation(validator),
|
|
1770
|
-
* withErrorReporting(Sentry.captureException)
|
|
1771
|
-
* );
|
|
1772
1015
|
*/
|
|
1773
1016
|
export function compose<M extends BaseMachine<any>>(
|
|
1774
1017
|
machine: M,
|
|
@@ -1778,43 +1021,48 @@ export function compose<M extends BaseMachine<any>>(
|
|
|
1778
1021
|
}
|
|
1779
1022
|
|
|
1780
1023
|
/**
|
|
1781
|
-
*
|
|
1782
|
-
*
|
|
1024
|
+
* Type-safe middleware composition with perfect inference.
|
|
1025
|
+
* Composes multiple middlewares into a single transformation chain.
|
|
1783
1026
|
*
|
|
1784
|
-
* @template M - The machine type
|
|
1785
|
-
* @
|
|
1786
|
-
* @param
|
|
1787
|
-
* @
|
|
1788
|
-
*
|
|
1789
|
-
* @example
|
|
1790
|
-
* const myMiddleware = createCustomMiddleware({
|
|
1791
|
-
* before: ({ transitionName }) => console.log('Before:', transitionName),
|
|
1792
|
-
* after: ({ transitionName }) => console.log('After:', transitionName)
|
|
1793
|
-
* });
|
|
1794
|
-
*
|
|
1795
|
-
* const machine1 = myMiddleware(counter1);
|
|
1796
|
-
* const machine2 = myMiddleware(counter2);
|
|
1027
|
+
* @template M - The input machine type
|
|
1028
|
+
* @template Ms - Array of middleware functions
|
|
1029
|
+
* @param machine - The machine to enhance
|
|
1030
|
+
* @param middlewares - Middleware functions to apply in order
|
|
1031
|
+
* @returns The machine with all middlewares applied, with precise type inference
|
|
1797
1032
|
*/
|
|
1798
|
-
export function
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1033
|
+
export function composeTyped<
|
|
1034
|
+
M extends BaseMachine<any>,
|
|
1035
|
+
Ms extends readonly MiddlewareFn<any, any>[]
|
|
1036
|
+
>(
|
|
1037
|
+
machine: M,
|
|
1038
|
+
...middlewares: Ms
|
|
1039
|
+
): ComposeResult<M, Ms> {
|
|
1040
|
+
return middlewares.reduce((acc, middleware) => middleware(acc), machine) as ComposeResult<M, Ms>;
|
|
1803
1041
|
}
|
|
1804
1042
|
|
|
1805
1043
|
// =============================================================================
|
|
1806
|
-
// SECTION:
|
|
1044
|
+
// SECTION: TYPE-LEVEL COMPOSITION
|
|
1807
1045
|
// =============================================================================
|
|
1808
1046
|
|
|
1809
1047
|
/**
|
|
1810
|
-
*
|
|
1811
|
-
*
|
|
1812
|
-
* @template R - The output machine type (usually extends M)
|
|
1048
|
+
* Type-level utility for composing middleware return types.
|
|
1049
|
+
* This enables perfect TypeScript inference when chaining middlewares.
|
|
1813
1050
|
*/
|
|
1051
|
+
export type ComposeResult<
|
|
1052
|
+
M extends BaseMachine<any>,
|
|
1053
|
+
Ms extends readonly MiddlewareFn<any, any>[]
|
|
1054
|
+
> = Ms extends readonly [infer First, ...infer Rest]
|
|
1055
|
+
? First extends MiddlewareFn<any, infer R>
|
|
1056
|
+
? Rest extends readonly MiddlewareFn<any, any>[]
|
|
1057
|
+
? ComposeResult<R, Rest>
|
|
1058
|
+
: R
|
|
1059
|
+
: M
|
|
1060
|
+
: M;
|
|
1061
|
+
|
|
1814
1062
|
/**
|
|
1815
1063
|
* A middleware function that transforms a machine.
|
|
1816
|
-
* @template M -
|
|
1817
|
-
* @template R -
|
|
1064
|
+
* @template M - The input machine type
|
|
1065
|
+
* @template R - The output machine type (usually extends M)
|
|
1818
1066
|
*/
|
|
1819
1067
|
export type MiddlewareFn<M extends BaseMachine<any>, R extends BaseMachine<any> = M> = (machine: M) => R;
|
|
1820
1068
|
|
|
@@ -1848,109 +1096,22 @@ export type NamedMiddleware<M extends BaseMachine<any>> = {
|
|
|
1848
1096
|
* Configuration for middleware pipeline execution.
|
|
1849
1097
|
*/
|
|
1850
1098
|
export interface PipelineConfig {
|
|
1851
|
-
/** Whether to continue
|
|
1099
|
+
/** Whether to continue execution if a middleware throws an error */
|
|
1852
1100
|
continueOnError?: boolean;
|
|
1853
|
-
/** Whether to log errors
|
|
1101
|
+
/** Whether to log errors from middlewares */
|
|
1854
1102
|
logErrors?: boolean;
|
|
1855
1103
|
/** Custom error handler */
|
|
1856
|
-
onError?: (error: Error, middlewareName?: string) => void;
|
|
1104
|
+
onError?: (error: Error, middlewareIndex: number, middlewareName?: string) => void;
|
|
1857
1105
|
}
|
|
1858
1106
|
|
|
1859
1107
|
/**
|
|
1860
1108
|
* Result of pipeline execution.
|
|
1861
1109
|
*/
|
|
1862
|
-
export
|
|
1863
|
-
/** The final machine after all middlewares */
|
|
1864
|
-
machine: M;
|
|
1865
|
-
/** Any errors that occurred during execution */
|
|
1866
|
-
errors: Array<{ error: Error; middlewareIndex: number; middlewareName?: string }>;
|
|
1867
|
-
/** Whether the pipeline completed successfully */
|
|
1868
|
-
success: boolean;
|
|
1869
|
-
}
|
|
1870
|
-
|
|
1871
|
-
/**
|
|
1872
|
-
* Compose multiple middlewares with improved type inference.
|
|
1873
|
-
* This is a more typesafe version of the basic compose function.
|
|
1874
|
-
*
|
|
1875
|
-
* @template M - The initial machine type
|
|
1876
|
-
* @template Ms - Array of middleware functions
|
|
1877
|
-
* @param machine - The initial machine
|
|
1878
|
-
* @param middlewares - Array of middleware functions to apply
|
|
1879
|
-
* @returns The machine with all middlewares applied
|
|
1880
|
-
*
|
|
1881
|
-
* @example
|
|
1882
|
-
* const enhanced = composeTyped(
|
|
1883
|
-
* counter,
|
|
1884
|
-
* withHistory(),
|
|
1885
|
-
* withSnapshot(),
|
|
1886
|
-
* withTimeTravel()
|
|
1887
|
-
* );
|
|
1888
|
-
*/
|
|
1889
|
-
/**
|
|
1890
|
-
* Recursively applies middlewares to infer the final machine type.
|
|
1891
|
-
* Provides precise type inference for middleware composition chains.
|
|
1892
|
-
*/
|
|
1893
|
-
type ComposeResult<
|
|
1894
|
-
M extends BaseMachine<any>,
|
|
1895
|
-
Ms extends readonly MiddlewareFn<any, any>[]
|
|
1896
|
-
> = Ms extends readonly []
|
|
1897
|
-
? M
|
|
1898
|
-
: Ms extends readonly [infer First, ...infer Rest]
|
|
1899
|
-
? First extends MiddlewareFn<any, infer Output>
|
|
1900
|
-
? Rest extends readonly MiddlewareFn<any, any>[]
|
|
1901
|
-
? ComposeResult<Output, Rest>
|
|
1902
|
-
: Output
|
|
1903
|
-
: M
|
|
1904
|
-
: M;
|
|
1110
|
+
export type PipelineResult<M extends BaseMachine<any>> = M;
|
|
1905
1111
|
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
*
|
|
1910
|
-
* @template M - The input machine type
|
|
1911
|
-
* @template Ms - Array of middleware functions
|
|
1912
|
-
* @param machine - The machine to enhance
|
|
1913
|
-
* @param middlewares - Middleware functions to apply in order
|
|
1914
|
-
* @returns The machine with all middlewares applied, with precise type inference
|
|
1915
|
-
*
|
|
1916
|
-
* @example
|
|
1917
|
-
* ```typescript
|
|
1918
|
-
* const enhanced = composeTyped(
|
|
1919
|
-
* counter,
|
|
1920
|
-
* withHistory(),
|
|
1921
|
-
* withSnapshot(),
|
|
1922
|
-
* withTimeTravel()
|
|
1923
|
-
* );
|
|
1924
|
-
* // enhanced: WithTimeTravel<WithSnapshot<WithHistory<Counter>>>
|
|
1925
|
-
* // Perfect IntelliSense for all methods and properties
|
|
1926
|
-
* ```
|
|
1927
|
-
*/
|
|
1928
|
-
export function composeTyped<
|
|
1929
|
-
M extends BaseMachine<any>,
|
|
1930
|
-
Ms extends readonly MiddlewareFn<any, any>[]
|
|
1931
|
-
>(
|
|
1932
|
-
machine: M,
|
|
1933
|
-
...middlewares: Ms
|
|
1934
|
-
): ComposeResult<M, Ms> {
|
|
1935
|
-
return middlewares.reduce((acc, middleware) => middleware(acc), machine) as ComposeResult<M, Ms>;
|
|
1936
|
-
}
|
|
1937
|
-
|
|
1938
|
-
/**
|
|
1939
|
-
* Type-safe middleware composition with fluent API.
|
|
1940
|
-
* Allows building middleware chains with method chaining.
|
|
1941
|
-
*
|
|
1942
|
-
* @example
|
|
1943
|
-
* ```typescript
|
|
1944
|
-
* const enhanced = chain(counter)
|
|
1945
|
-
* .with(withHistory())
|
|
1946
|
-
* .with(withSnapshot())
|
|
1947
|
-
* .with(withTimeTravel())
|
|
1948
|
-
* .build();
|
|
1949
|
-
* ```
|
|
1950
|
-
*/
|
|
1951
|
-
export function chain<M extends BaseMachine<any>>(machine: M) {
|
|
1952
|
-
return new MiddlewareChainBuilder(machine);
|
|
1953
|
-
}
|
|
1112
|
+
// =============================================================================
|
|
1113
|
+
// SECTION: FLUENT API
|
|
1114
|
+
// =============================================================================
|
|
1954
1115
|
|
|
1955
1116
|
/**
|
|
1956
1117
|
* Fluent middleware composer for building complex middleware chains.
|
|
@@ -1980,121 +1141,97 @@ class MiddlewareChainBuilder<M extends BaseMachine<any>> {
|
|
|
1980
1141
|
}
|
|
1981
1142
|
|
|
1982
1143
|
/**
|
|
1983
|
-
*
|
|
1984
|
-
* These types help with inference when using popular middleware combinations.
|
|
1985
|
-
*/
|
|
1986
|
-
export type WithDebugging<M extends BaseMachine<any>> = WithTimeTravel<WithSnapshot<WithHistory<M>>>;
|
|
1987
|
-
|
|
1988
|
-
/**
|
|
1989
|
-
* Convenience function for the most common debugging middleware stack.
|
|
1990
|
-
* Combines history, snapshots, and time travel for full debugging capabilities.
|
|
1144
|
+
* Create a fluent middleware chain builder.
|
|
1991
1145
|
*
|
|
1992
1146
|
* @example
|
|
1993
1147
|
* ```typescript
|
|
1994
|
-
* const
|
|
1995
|
-
*
|
|
1996
|
-
*
|
|
1997
|
-
*
|
|
1998
|
-
*
|
|
1148
|
+
* const enhanced = chain(counter)
|
|
1149
|
+
* .with(withHistory())
|
|
1150
|
+
* .with(withSnapshot())
|
|
1151
|
+
* .with(withTimeTravel())
|
|
1152
|
+
* .build();
|
|
1999
1153
|
* ```
|
|
2000
1154
|
*/
|
|
2001
|
-
export function
|
|
2002
|
-
return
|
|
1155
|
+
export function chain<M extends BaseMachine<any>>(machine: M) {
|
|
1156
|
+
return new MiddlewareChainBuilder(machine);
|
|
2003
1157
|
}
|
|
2004
1158
|
|
|
1159
|
+
// =============================================================================
|
|
1160
|
+
// SECTION: CONDITIONAL MIDDLEWARE
|
|
1161
|
+
// =============================================================================
|
|
1162
|
+
|
|
2005
1163
|
/**
|
|
2006
|
-
* Create a middleware
|
|
1164
|
+
* Create a conditional middleware that only applies when a predicate is true.
|
|
2007
1165
|
*
|
|
2008
1166
|
* @template M - The machine type
|
|
2009
|
-
* @param
|
|
2010
|
-
* @
|
|
2011
|
-
*
|
|
2012
|
-
* @example
|
|
2013
|
-
* const pipeline = createPipeline({ continueOnError: true });
|
|
2014
|
-
*
|
|
2015
|
-
* const result = pipeline(
|
|
2016
|
-
* counter,
|
|
2017
|
-
* withHistory(),
|
|
2018
|
-
* withSnapshot(),
|
|
2019
|
-
* { middleware: withLogging(), when: (m) => m.context.debug }
|
|
2020
|
-
* );
|
|
1167
|
+
* @param middleware - The middleware to conditionally apply
|
|
1168
|
+
* @param predicate - Function that determines when to apply the middleware
|
|
1169
|
+
* @returns A conditional middleware that can be called directly or used in pipelines
|
|
2021
1170
|
*/
|
|
2022
|
-
export function
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
} {
|
|
2030
|
-
const {
|
|
2031
|
-
continueOnError = false,
|
|
2032
|
-
logErrors = true,
|
|
2033
|
-
onError
|
|
2034
|
-
} = config;
|
|
2035
|
-
|
|
2036
|
-
return (machine: M, ...middlewares: Array<MiddlewareFn<M> | ConditionalMiddleware<M>>): PipelineResult<M> => {
|
|
2037
|
-
let currentMachine = machine;
|
|
2038
|
-
const errors: Array<{ error: Error; middlewareIndex: number; middlewareName?: string }> = [];
|
|
2039
|
-
|
|
2040
|
-
for (let i = 0; i < middlewares.length; i++) {
|
|
2041
|
-
const middleware = middlewares[i];
|
|
2042
|
-
|
|
2043
|
-
try {
|
|
2044
|
-
// Handle conditional middleware
|
|
2045
|
-
if ('middleware' in middleware && 'when' in middleware) {
|
|
2046
|
-
if (!middleware.when(currentMachine)) {
|
|
2047
|
-
continue; // Skip this middleware
|
|
2048
|
-
}
|
|
2049
|
-
currentMachine = middleware.middleware(currentMachine);
|
|
2050
|
-
} else {
|
|
2051
|
-
// Regular middleware
|
|
2052
|
-
currentMachine = (middleware as MiddlewareFn<M>)(currentMachine);
|
|
2053
|
-
}
|
|
2054
|
-
} catch (error) {
|
|
2055
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
2056
|
-
errors.push({ error: err, middlewareIndex: i });
|
|
2057
|
-
|
|
2058
|
-
if (logErrors) {
|
|
2059
|
-
console.error(`Middleware pipeline error at index ${i}:`, err);
|
|
2060
|
-
}
|
|
2061
|
-
|
|
2062
|
-
onError?.(err, `middleware-${i}`);
|
|
1171
|
+
export function when<M extends BaseMachine<any>>(
|
|
1172
|
+
middleware: MiddlewareFn<M>,
|
|
1173
|
+
predicate: (machine: M) => boolean
|
|
1174
|
+
): ConditionalMiddleware<M> & MiddlewareFn<M> {
|
|
1175
|
+
const conditional: ConditionalMiddleware<M> & MiddlewareFn<M> = function(machine: M) {
|
|
1176
|
+
return predicate(machine) ? middleware(machine) : machine;
|
|
1177
|
+
};
|
|
2063
1178
|
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
}
|
|
2067
|
-
}
|
|
2068
|
-
}
|
|
1179
|
+
conditional.middleware = middleware;
|
|
1180
|
+
conditional.when = predicate;
|
|
2069
1181
|
|
|
2070
|
-
|
|
2071
|
-
machine: currentMachine,
|
|
2072
|
-
errors,
|
|
2073
|
-
success: errors.length === 0
|
|
2074
|
-
};
|
|
2075
|
-
};
|
|
1182
|
+
return conditional;
|
|
2076
1183
|
}
|
|
2077
1184
|
|
|
2078
1185
|
/**
|
|
2079
|
-
* Create a middleware
|
|
2080
|
-
* Useful for building complex middleware stacks from reusable components.
|
|
1186
|
+
* Create a middleware that only applies in development mode.
|
|
2081
1187
|
*
|
|
2082
1188
|
* @template M - The machine type
|
|
1189
|
+
* @param middleware - The middleware to apply in development
|
|
1190
|
+
* @returns A conditional middleware for development mode
|
|
1191
|
+
*/
|
|
1192
|
+
export function inDevelopment<M extends BaseMachine<any>>(
|
|
1193
|
+
middleware: MiddlewareFn<M>
|
|
1194
|
+
): ConditionalMiddleware<M> & MiddlewareFn<M> {
|
|
1195
|
+
return when(middleware, () => {
|
|
1196
|
+
return typeof process !== 'undefined'
|
|
1197
|
+
? process.env.NODE_ENV === 'development'
|
|
1198
|
+
: typeof window !== 'undefined'
|
|
1199
|
+
? !window.location.hostname.includes('production')
|
|
1200
|
+
: false;
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
/**
|
|
1205
|
+
* Create a middleware that only applies when a context property matches a value.
|
|
2083
1206
|
*
|
|
2084
|
-
* @
|
|
2085
|
-
*
|
|
2086
|
-
*
|
|
2087
|
-
*
|
|
2088
|
-
*
|
|
2089
|
-
*
|
|
2090
|
-
|
|
1207
|
+
* @template M - The machine type
|
|
1208
|
+
* @template K - The context key
|
|
1209
|
+
* @param key - The context property key
|
|
1210
|
+
* @param value - The value to match
|
|
1211
|
+
* @param middleware - The middleware to apply when the condition matches
|
|
1212
|
+
* @returns A conditional middleware
|
|
1213
|
+
*/
|
|
1214
|
+
export function whenContext<M extends BaseMachine<any>, K extends keyof Context<M>>(
|
|
1215
|
+
key: K,
|
|
1216
|
+
value: Context<M>[K],
|
|
1217
|
+
middleware: MiddlewareFn<M>
|
|
1218
|
+
): ConditionalMiddleware<M> & MiddlewareFn<M> {
|
|
1219
|
+
return when(middleware, (machine) => machine.context[key] === value);
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// =============================================================================
|
|
1223
|
+
// SECTION: MIDDLEWARE REGISTRY
|
|
1224
|
+
// =============================================================================
|
|
1225
|
+
|
|
1226
|
+
/**
|
|
1227
|
+
* Create a middleware registry for managing reusable middleware configurations.
|
|
2091
1228
|
*/
|
|
2092
1229
|
export function createMiddlewareRegistry<M extends BaseMachine<any>>() {
|
|
2093
1230
|
const registry = new Map<string, NamedMiddleware<M>>();
|
|
2094
1231
|
|
|
2095
1232
|
return {
|
|
2096
1233
|
/**
|
|
2097
|
-
* Register a middleware
|
|
1234
|
+
* Register a middleware by name.
|
|
2098
1235
|
*/
|
|
2099
1236
|
register(
|
|
2100
1237
|
name: string,
|
|
@@ -2166,104 +1303,80 @@ export function createMiddlewareRegistry<M extends BaseMachine<any>>() {
|
|
|
2166
1303
|
};
|
|
2167
1304
|
}
|
|
2168
1305
|
|
|
1306
|
+
// =============================================================================
|
|
1307
|
+
// SECTION: PIPELINES
|
|
1308
|
+
// =============================================================================
|
|
1309
|
+
|
|
2169
1310
|
/**
|
|
2170
|
-
* Create a
|
|
1311
|
+
* Create a middleware pipeline with error handling and conditional execution.
|
|
2171
1312
|
*
|
|
2172
1313
|
* @template M - The machine type
|
|
2173
|
-
* @param
|
|
2174
|
-
* @
|
|
2175
|
-
* @returns A conditional middleware that can be called directly or used in pipelines
|
|
2176
|
-
*
|
|
2177
|
-
* @example
|
|
2178
|
-
* const debugMiddleware = when(
|
|
2179
|
-
* withTimeTravel(),
|
|
2180
|
-
* (machine) => machine.context.debugMode
|
|
2181
|
-
* );
|
|
2182
|
-
*
|
|
2183
|
-
* // Can be called directly
|
|
2184
|
-
* const machine = debugMiddleware(baseMachine);
|
|
2185
|
-
*
|
|
2186
|
-
* // Can also be used in pipelines
|
|
2187
|
-
* const pipeline = createPipeline();
|
|
2188
|
-
* const result = pipeline(machine, debugMiddleware);
|
|
1314
|
+
* @param config - Pipeline configuration
|
|
1315
|
+
* @returns A function that executes middlewares in a pipeline
|
|
2189
1316
|
*/
|
|
2190
|
-
export function
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
};
|
|
1317
|
+
export function createPipeline<M extends BaseMachine<any>>(
|
|
1318
|
+
config: PipelineConfig = {}
|
|
1319
|
+
): {
|
|
1320
|
+
<Ms extends Array<MiddlewareFn<M> | ConditionalMiddleware<M>>>(
|
|
1321
|
+
machine: M,
|
|
1322
|
+
...middlewares: Ms
|
|
1323
|
+
): { machine: M; errors: Array<{ error: Error; middlewareIndex: number; middlewareName?: string }>; success: boolean };
|
|
1324
|
+
} {
|
|
1325
|
+
const {
|
|
1326
|
+
continueOnError = false,
|
|
1327
|
+
logErrors = true,
|
|
1328
|
+
onError
|
|
1329
|
+
} = config;
|
|
2197
1330
|
|
|
2198
|
-
|
|
2199
|
-
|
|
1331
|
+
return (machine: M, ...middlewares: Array<MiddlewareFn<M> | ConditionalMiddleware<M>>) => {
|
|
1332
|
+
let currentMachine = machine;
|
|
1333
|
+
const errors: Array<{ error: Error; middlewareIndex: number; middlewareName?: string }> = [];
|
|
1334
|
+
let success = true;
|
|
2200
1335
|
|
|
2201
|
-
|
|
2202
|
-
|
|
1336
|
+
for (let i = 0; i < middlewares.length; i++) {
|
|
1337
|
+
const middleware = middlewares[i];
|
|
2203
1338
|
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
return when(middleware, () => {
|
|
2221
|
-
return typeof process !== 'undefined'
|
|
2222
|
-
? process.env.NODE_ENV === 'development'
|
|
2223
|
-
: typeof window !== 'undefined'
|
|
2224
|
-
? !window.location.hostname.includes('production')
|
|
2225
|
-
: false;
|
|
2226
|
-
});
|
|
2227
|
-
}
|
|
1339
|
+
try {
|
|
1340
|
+
// Handle conditional middleware
|
|
1341
|
+
if ('middleware' in middleware && 'when' in middleware) {
|
|
1342
|
+
if (!middleware.when(currentMachine)) {
|
|
1343
|
+
continue; // Skip this middleware
|
|
1344
|
+
}
|
|
1345
|
+
currentMachine = middleware.middleware(currentMachine);
|
|
1346
|
+
} else {
|
|
1347
|
+
// Regular middleware
|
|
1348
|
+
currentMachine = (middleware as MiddlewareFn<M>)(currentMachine);
|
|
1349
|
+
}
|
|
1350
|
+
} catch (error) {
|
|
1351
|
+
success = false;
|
|
1352
|
+
if (!continueOnError) {
|
|
1353
|
+
throw error;
|
|
1354
|
+
}
|
|
2228
1355
|
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
export function whenContext<M extends BaseMachine<any>, K extends keyof Context<M>>(
|
|
2246
|
-
key: K,
|
|
2247
|
-
value: Context<M>[K],
|
|
2248
|
-
middleware: MiddlewareFn<M>
|
|
2249
|
-
): ConditionalMiddleware<M> & MiddlewareFn<M> {
|
|
2250
|
-
return when(middleware, (machine) => machine.context[key] === value);
|
|
1356
|
+
errors.push({
|
|
1357
|
+
error: error as Error,
|
|
1358
|
+
middlewareIndex: i,
|
|
1359
|
+
middlewareName: (middleware as any).name
|
|
1360
|
+
});
|
|
1361
|
+
|
|
1362
|
+
if (logErrors) {
|
|
1363
|
+
console.error(`Pipeline middleware error at index ${i}:`, error);
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
onError?.(error as Error, i, (middleware as any).name);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
return { machine: currentMachine, errors, success };
|
|
1371
|
+
};
|
|
2251
1372
|
}
|
|
2252
1373
|
|
|
1374
|
+
// =============================================================================
|
|
1375
|
+
// SECTION: UTILITY FUNCTIONS
|
|
1376
|
+
// =============================================================================
|
|
1377
|
+
|
|
2253
1378
|
/**
|
|
2254
1379
|
* Combine multiple middlewares with short-circuiting.
|
|
2255
|
-
* If any middleware returns a different type, the composition stops.
|
|
2256
|
-
*
|
|
2257
|
-
* @template M - The machine type
|
|
2258
|
-
* @param middlewares - Array of middlewares to combine
|
|
2259
|
-
* @returns A combined middleware function
|
|
2260
|
-
*
|
|
2261
|
-
* @example
|
|
2262
|
-
* const combined = combine(
|
|
2263
|
-
* withHistory(),
|
|
2264
|
-
* withSnapshot(),
|
|
2265
|
-
* withValidation()
|
|
2266
|
-
* );
|
|
2267
1380
|
*/
|
|
2268
1381
|
export function combine<M extends BaseMachine<any>>(
|
|
2269
1382
|
...middlewares: Array<MiddlewareFn<M>>
|
|
@@ -2273,18 +1386,6 @@ export function combine<M extends BaseMachine<any>>(
|
|
|
2273
1386
|
|
|
2274
1387
|
/**
|
|
2275
1388
|
* Create a middleware that applies different middlewares based on context.
|
|
2276
|
-
*
|
|
2277
|
-
* @template M - The machine type
|
|
2278
|
-
* @param branches - Array of [predicate, middleware] pairs
|
|
2279
|
-
* @param fallback - Optional fallback middleware if no predicates match
|
|
2280
|
-
* @returns A branching middleware
|
|
2281
|
-
*
|
|
2282
|
-
* @example
|
|
2283
|
-
* const smartMiddleware = branch(
|
|
2284
|
-
* [(m) => m.context.userType === 'admin', withAdminFeatures()],
|
|
2285
|
-
* [(m) => m.context.debug, withTimeTravel()],
|
|
2286
|
-
* withBasicLogging() // fallback
|
|
2287
|
-
* );
|
|
2288
1389
|
*/
|
|
2289
1390
|
export function branch<M extends BaseMachine<any>>(
|
|
2290
1391
|
branches: Array<[predicate: (machine: M) => boolean, middleware: MiddlewareFn<M>]>,
|
|
@@ -2300,6 +1401,10 @@ export function branch<M extends BaseMachine<any>>(
|
|
|
2300
1401
|
};
|
|
2301
1402
|
}
|
|
2302
1403
|
|
|
1404
|
+
// =============================================================================
|
|
1405
|
+
// SECTION: TYPE GUARDS
|
|
1406
|
+
// =============================================================================
|
|
1407
|
+
|
|
2303
1408
|
/**
|
|
2304
1409
|
* Type guard to check if a value is a middleware function.
|
|
2305
1410
|
*/
|
|
@@ -2323,3 +1428,19 @@ export function isConditionalMiddleware<M extends BaseMachine<any>>(
|
|
|
2323
1428
|
typeof value.when === 'function'
|
|
2324
1429
|
);
|
|
2325
1430
|
}
|
|
1431
|
+
|
|
1432
|
+
// =============================================================================
|
|
1433
|
+
// SECTION: COMMON COMBINATIONS
|
|
1434
|
+
// =============================================================================
|
|
1435
|
+
|
|
1436
|
+
/**
|
|
1437
|
+
* Common middleware combination types for better DX.
|
|
1438
|
+
*/
|
|
1439
|
+
export type WithDebugging<M extends BaseMachine<any>> = WithTimeTravel<WithSnapshot<WithHistory<M>>>;
|
|
1440
|
+
|
|
1441
|
+
/**
|
|
1442
|
+
* Convenience function for the most common debugging middleware stack.
|
|
1443
|
+
*/
|
|
1444
|
+
export function withDebugging<M extends BaseMachine<any>>(machine: M): WithDebugging<M> {
|
|
1445
|
+
return withTimeTravel(withSnapshot(withHistory(machine)));
|
|
1446
|
+
}
|