@doeixd/machine 0.0.13 → 0.0.17
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 +67 -15
- package/dist/cjs/development/core.js +1852 -0
- package/dist/cjs/development/core.js.map +7 -0
- package/dist/cjs/development/index.js +1341 -1374
- 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 +1341 -1374
- 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/functional-combinators.d.ts +3 -5
- package/dist/types/functional-combinators.d.ts.map +1 -1
- package/dist/types/index.d.ts +241 -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/functional-combinators.ts +3 -3
- package/src/index.ts +374 -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
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Core middleware types and basic middleware creation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Context, BaseMachine } from '../index';
|
|
6
|
+
|
|
7
|
+
// =============================================================================
|
|
8
|
+
// SECTION: MIDDLEWARE TYPES
|
|
9
|
+
// =============================================================================
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Context object passed to middleware hooks containing transition metadata.
|
|
13
|
+
* @template C - The context object type
|
|
14
|
+
*/
|
|
15
|
+
export interface MiddlewareContext<C extends object> {
|
|
16
|
+
/** The name of the transition being called */
|
|
17
|
+
transitionName: string;
|
|
18
|
+
/** The current machine context before the transition */
|
|
19
|
+
context: Readonly<C>;
|
|
20
|
+
/** Arguments passed to the transition function */
|
|
21
|
+
args: any[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Result object passed to after hooks containing transition outcome.
|
|
26
|
+
* @template C - The context object type
|
|
27
|
+
*/
|
|
28
|
+
export interface MiddlewareResult<C extends object> {
|
|
29
|
+
/** The name of the transition that was called */
|
|
30
|
+
transitionName: string;
|
|
31
|
+
/** The context before the transition */
|
|
32
|
+
prevContext: Readonly<C>;
|
|
33
|
+
/** The context after the transition */
|
|
34
|
+
nextContext: Readonly<C>;
|
|
35
|
+
/** Arguments that were passed to the transition */
|
|
36
|
+
args: any[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Error context passed to error hooks.
|
|
41
|
+
* @template C - The context object type
|
|
42
|
+
*/
|
|
43
|
+
export interface MiddlewareError<C extends object> {
|
|
44
|
+
/** The name of the transition that failed */
|
|
45
|
+
transitionName: string;
|
|
46
|
+
/** The context when the error occurred */
|
|
47
|
+
context: Readonly<C>;
|
|
48
|
+
/** Arguments that were passed to the transition */
|
|
49
|
+
args: any[];
|
|
50
|
+
/** The error that was thrown */
|
|
51
|
+
error: Error;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Configuration object for middleware hooks.
|
|
56
|
+
* All hooks are optional - provide only the ones you need.
|
|
57
|
+
* @template C - The context object type
|
|
58
|
+
*/
|
|
59
|
+
export interface MiddlewareHooks<C extends object> {
|
|
60
|
+
/**
|
|
61
|
+
* Called before a transition executes.
|
|
62
|
+
* Can be used for validation, logging, analytics, etc.
|
|
63
|
+
*
|
|
64
|
+
* @param ctx - Transition context with machine state and transition details
|
|
65
|
+
* @returns void to continue, CANCEL to abort silently, or Promise for async validation
|
|
66
|
+
*/
|
|
67
|
+
before?: (ctx: MiddlewareContext<C>) => void | typeof CANCEL | Promise<void | typeof CANCEL>;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Called after a transition successfully executes.
|
|
71
|
+
* Receives both the previous and next context.
|
|
72
|
+
* Cannot prevent the transition (it already happened).
|
|
73
|
+
*
|
|
74
|
+
* @param result - Transition result with before/after contexts and transition details
|
|
75
|
+
*/
|
|
76
|
+
after?: (result: MiddlewareResult<C>) => void | Promise<void>;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Called when a transition throws an error.
|
|
80
|
+
* Can be used for error reporting, recovery, etc.
|
|
81
|
+
*
|
|
82
|
+
* @param error - Error context with transition details and error information
|
|
83
|
+
*/
|
|
84
|
+
error?: (error: MiddlewareError<C>) => void | Promise<void>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Options for configuring middleware behavior.
|
|
89
|
+
*/
|
|
90
|
+
export interface MiddlewareOptions {
|
|
91
|
+
/** Whether to continue execution if a hook throws an error */
|
|
92
|
+
continueOnError?: boolean;
|
|
93
|
+
/** Whether to log errors from hooks */
|
|
94
|
+
logErrors?: boolean;
|
|
95
|
+
/** Custom error handler for hook errors */
|
|
96
|
+
onError?: (error: Error, hookName: string, ctx: any) => void;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Symbol used to cancel a transition from a before hook.
|
|
101
|
+
*/
|
|
102
|
+
export const CANCEL = Symbol('CANCEL');
|
|
103
|
+
|
|
104
|
+
// =============================================================================
|
|
105
|
+
// SECTION: CORE MIDDLEWARE FUNCTIONS
|
|
106
|
+
// =============================================================================
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Creates a middleware function that wraps machine transitions with hooks.
|
|
110
|
+
*
|
|
111
|
+
* @template M - The machine type
|
|
112
|
+
* @param machine - The machine to instrument
|
|
113
|
+
* @param hooks - Middleware hooks to execute
|
|
114
|
+
* @param options - Middleware configuration options
|
|
115
|
+
* @returns A new machine with middleware applied
|
|
116
|
+
*/
|
|
117
|
+
export function createMiddleware<M extends BaseMachine<any>>(
|
|
118
|
+
machine: M,
|
|
119
|
+
hooks: MiddlewareHooks<Context<M>>,
|
|
120
|
+
options: MiddlewareOptions = {}
|
|
121
|
+
): M {
|
|
122
|
+
const { continueOnError = false, logErrors = true, onError } = options;
|
|
123
|
+
|
|
124
|
+
// Create a wrapped machine that intercepts all transition calls
|
|
125
|
+
const wrappedMachine: any = { ...machine };
|
|
126
|
+
|
|
127
|
+
// Copy any extra properties from the original machine (for middleware composition)
|
|
128
|
+
for (const prop in machine) {
|
|
129
|
+
if (!Object.prototype.hasOwnProperty.call(machine, prop)) continue;
|
|
130
|
+
if (prop !== 'context' && typeof machine[prop] !== 'function') {
|
|
131
|
+
wrappedMachine[prop] = machine[prop];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Wrap each transition function
|
|
136
|
+
for (const prop in machine) {
|
|
137
|
+
if (!Object.prototype.hasOwnProperty.call(machine, prop)) continue;
|
|
138
|
+
const value = machine[prop];
|
|
139
|
+
if (typeof value === 'function' && prop !== 'context') {
|
|
140
|
+
wrappedMachine[prop] = function (this: any, ...args: any[]) {
|
|
141
|
+
const transitionName = prop;
|
|
142
|
+
const context = wrappedMachine.context;
|
|
143
|
+
|
|
144
|
+
// Helper function to execute the transition and after hooks
|
|
145
|
+
const executeTransition = () => {
|
|
146
|
+
// 2. Execute the actual transition
|
|
147
|
+
let nextMachine: any;
|
|
148
|
+
try {
|
|
149
|
+
nextMachine = value.apply(this, args);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
// 3. Execute error hooks if transition failed
|
|
152
|
+
if (hooks.error) {
|
|
153
|
+
try {
|
|
154
|
+
// Error hooks are called synchronously for now
|
|
155
|
+
hooks.error({
|
|
156
|
+
transitionName,
|
|
157
|
+
context,
|
|
158
|
+
args: [...args],
|
|
159
|
+
error: error as Error
|
|
160
|
+
});
|
|
161
|
+
} catch (hookError) {
|
|
162
|
+
if (!continueOnError) throw hookError;
|
|
163
|
+
if (logErrors) console.error(`Middleware error hook error for ${transitionName}:`, hookError);
|
|
164
|
+
onError?.(hookError as Error, 'error', { transitionName, context, args, error });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
throw error; // Re-throw the original error
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Ensure the returned machine has the same extra properties as the wrapped machine
|
|
171
|
+
const ensureMiddlewareProperties = (machine: any) => {
|
|
172
|
+
if (machine && typeof machine === 'object' && machine.context) {
|
|
173
|
+
// Copy extra properties from the wrapped machine to the returned machine
|
|
174
|
+
for (const prop in wrappedMachine) {
|
|
175
|
+
if (!Object.prototype.hasOwnProperty.call(wrappedMachine, prop)) continue;
|
|
176
|
+
if (prop !== 'context' && !(prop in machine)) {
|
|
177
|
+
machine[prop] = wrappedMachine[prop];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Also wrap the transition functions on the returned machine
|
|
182
|
+
for (const prop in machine) {
|
|
183
|
+
if (!Object.prototype.hasOwnProperty.call(machine, prop)) continue;
|
|
184
|
+
const value = machine[prop];
|
|
185
|
+
if (typeof value === 'function' && prop !== 'context' && wrappedMachine[prop]) {
|
|
186
|
+
machine[prop] = wrappedMachine[prop];
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return machine;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// Check if the transition is async (returns a Promise)
|
|
194
|
+
if (nextMachine && typeof nextMachine.then === 'function') {
|
|
195
|
+
// For async transitions, we need to handle the after hooks after the promise resolves
|
|
196
|
+
const asyncResult = nextMachine.then((resolvedMachine: any) => {
|
|
197
|
+
// Ensure middleware properties are attached
|
|
198
|
+
ensureMiddlewareProperties(resolvedMachine);
|
|
199
|
+
|
|
200
|
+
// Execute after hooks with the resolved machine
|
|
201
|
+
if (hooks.after) {
|
|
202
|
+
try {
|
|
203
|
+
const result = hooks.after({
|
|
204
|
+
transitionName,
|
|
205
|
+
prevContext: context,
|
|
206
|
+
nextContext: resolvedMachine.context,
|
|
207
|
+
args: [...args]
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Handle async after hooks
|
|
211
|
+
if (result && typeof result.then === 'function') {
|
|
212
|
+
return result.then(() => resolvedMachine);
|
|
213
|
+
}
|
|
214
|
+
} catch (error) {
|
|
215
|
+
if (!continueOnError) throw error;
|
|
216
|
+
if (logErrors) console.error(`Middleware after hook error for ${transitionName}:`, error);
|
|
217
|
+
onError?.(error as Error, 'after', {
|
|
218
|
+
transitionName,
|
|
219
|
+
prevContext: context,
|
|
220
|
+
nextContext: resolvedMachine.context,
|
|
221
|
+
args
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return resolvedMachine;
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
return asyncResult;
|
|
229
|
+
} else {
|
|
230
|
+
// Ensure middleware properties are attached to synchronous transitions
|
|
231
|
+
ensureMiddlewareProperties(nextMachine);
|
|
232
|
+
|
|
233
|
+
// Synchronous transition
|
|
234
|
+
// 4. Execute after hooks
|
|
235
|
+
if (hooks.after) {
|
|
236
|
+
try {
|
|
237
|
+
const result = hooks.after({
|
|
238
|
+
transitionName,
|
|
239
|
+
prevContext: context,
|
|
240
|
+
nextContext: nextMachine.context,
|
|
241
|
+
args: [...args]
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// Handle async after hooks
|
|
245
|
+
if (result && typeof result === 'object' && result && 'then' in result) {
|
|
246
|
+
return result.then(() => nextMachine).catch((error: Error) => {
|
|
247
|
+
if (!continueOnError) throw error;
|
|
248
|
+
if (logErrors) console.error(`Middleware after hook error for ${transitionName}:`, error);
|
|
249
|
+
onError?.(error, 'after', {
|
|
250
|
+
transitionName,
|
|
251
|
+
prevContext: context,
|
|
252
|
+
nextContext: nextMachine.context,
|
|
253
|
+
args
|
|
254
|
+
});
|
|
255
|
+
return nextMachine;
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
} catch (error) {
|
|
259
|
+
if (!continueOnError) throw error;
|
|
260
|
+
if (logErrors) console.error(`Middleware after hook error for ${transitionName}:`, error);
|
|
261
|
+
onError?.(error as Error, 'after', {
|
|
262
|
+
transitionName,
|
|
263
|
+
prevContext: context,
|
|
264
|
+
nextContext: nextMachine.context,
|
|
265
|
+
args
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// 5. Return the next machine state
|
|
271
|
+
return nextMachine;
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// 1. Execute before hooks synchronously if possible
|
|
276
|
+
if (hooks.before) {
|
|
277
|
+
try {
|
|
278
|
+
const result = hooks.before({
|
|
279
|
+
transitionName,
|
|
280
|
+
context,
|
|
281
|
+
args: [...args]
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// Handle async hooks
|
|
285
|
+
if (result && typeof result === 'object' && result && 'then' in result) {
|
|
286
|
+
// For async hooks, return a promise that executes the transition after
|
|
287
|
+
return result.then((hookResult: any) => {
|
|
288
|
+
if (hookResult === CANCEL) {
|
|
289
|
+
return wrappedMachine;
|
|
290
|
+
}
|
|
291
|
+
return executeTransition();
|
|
292
|
+
}).catch((error: Error) => {
|
|
293
|
+
if (!continueOnError) throw error;
|
|
294
|
+
if (logErrors) console.error(`Middleware before hook error for ${transitionName}:`, error);
|
|
295
|
+
onError?.(error, 'before', { transitionName, context, args });
|
|
296
|
+
return executeTransition();
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Check if transition should be cancelled
|
|
301
|
+
if (result === CANCEL) {
|
|
302
|
+
return wrappedMachine; // Return the same machine instance
|
|
303
|
+
}
|
|
304
|
+
} catch (error) {
|
|
305
|
+
if (!continueOnError) throw error;
|
|
306
|
+
if (logErrors) console.error(`Middleware before hook error for ${transitionName}:`, error);
|
|
307
|
+
onError?.(error as Error, 'before', { transitionName, context, args });
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
return executeTransition();
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return wrappedMachine;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Creates a simple logging middleware that logs all transitions.
|
|
321
|
+
*
|
|
322
|
+
* @template M - The machine type
|
|
323
|
+
* @param machine - The machine to add logging to
|
|
324
|
+
* @param options - Logging configuration options
|
|
325
|
+
* @returns A new machine with logging middleware
|
|
326
|
+
*/
|
|
327
|
+
export function withLogging<M extends BaseMachine<any>>(
|
|
328
|
+
machine: M,
|
|
329
|
+
options: {
|
|
330
|
+
logger?: (message: string) => void;
|
|
331
|
+
includeArgs?: boolean;
|
|
332
|
+
includeContext?: boolean;
|
|
333
|
+
} = {}
|
|
334
|
+
): M {
|
|
335
|
+
const { logger = console.log, includeArgs = false, includeContext = true } = options;
|
|
336
|
+
|
|
337
|
+
return createMiddleware(machine, {
|
|
338
|
+
before: ({ transitionName, args }) => {
|
|
339
|
+
const message = includeArgs ? `→ ${transitionName} [${args.join(', ')}]` : `→ ${transitionName}`;
|
|
340
|
+
logger(message);
|
|
341
|
+
},
|
|
342
|
+
after: ({ transitionName, nextContext }) => {
|
|
343
|
+
const contextStr = includeContext ? ` ${JSON.stringify(nextContext)}` : '';
|
|
344
|
+
logger(`✓ ${transitionName}${contextStr}`);
|
|
345
|
+
},
|
|
346
|
+
error: ({ transitionName, error }) => {
|
|
347
|
+
console.error(`[Machine] ${transitionName} failed:`, error);
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Creates analytics tracking middleware.
|
|
354
|
+
*
|
|
355
|
+
* @template M - The machine type
|
|
356
|
+
* @param machine - The machine to track
|
|
357
|
+
* @param track - Analytics tracking function
|
|
358
|
+
* @param options - Configuration options
|
|
359
|
+
* @returns A new machine with analytics tracking
|
|
360
|
+
*/
|
|
361
|
+
export function withAnalytics<M extends BaseMachine<any>>(
|
|
362
|
+
machine: M,
|
|
363
|
+
track: (event: string, data?: any) => void,
|
|
364
|
+
options: {
|
|
365
|
+
eventPrefix?: string;
|
|
366
|
+
includePrevContext?: boolean;
|
|
367
|
+
includeArgs?: boolean;
|
|
368
|
+
} = {}
|
|
369
|
+
): M {
|
|
370
|
+
const { eventPrefix = 'state_transition', includePrevContext = false, includeArgs = false } = options;
|
|
371
|
+
|
|
372
|
+
return createMiddleware(machine, {
|
|
373
|
+
after: ({ transitionName, prevContext, nextContext, args }) => {
|
|
374
|
+
const event = `${eventPrefix}.${transitionName}`;
|
|
375
|
+
const data: any = { transition: transitionName };
|
|
376
|
+
if (includePrevContext) data.from = prevContext;
|
|
377
|
+
data.to = nextContext;
|
|
378
|
+
if (includeArgs) data.args = args;
|
|
379
|
+
track(event, data);
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Creates validation middleware that runs before transitions.
|
|
386
|
+
*
|
|
387
|
+
* @template M - The machine type
|
|
388
|
+
* @param machine - The machine to validate
|
|
389
|
+
* @param validator - Validation function
|
|
390
|
+
* @returns A new machine with validation
|
|
391
|
+
*/
|
|
392
|
+
export function withValidation<M extends BaseMachine<any>>(
|
|
393
|
+
machine: M,
|
|
394
|
+
validator: (ctx: MiddlewareContext<Context<M>>) => boolean | void
|
|
395
|
+
): M {
|
|
396
|
+
return createMiddleware(machine, {
|
|
397
|
+
before: (ctx) => {
|
|
398
|
+
const result = validator(ctx);
|
|
399
|
+
if (result === false) {
|
|
400
|
+
throw new Error(`Validation failed for transition: ${ctx.transitionName}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Creates permission-checking middleware.
|
|
408
|
+
*
|
|
409
|
+
* @template M - The machine type
|
|
410
|
+
* @param machine - The machine to protect
|
|
411
|
+
* @param checker - Permission checking function
|
|
412
|
+
* @returns A new machine with permission checks
|
|
413
|
+
*/
|
|
414
|
+
export function withPermissions<M extends BaseMachine<any>>(
|
|
415
|
+
machine: M,
|
|
416
|
+
checker: (ctx: MiddlewareContext<Context<M>>) => boolean
|
|
417
|
+
): M {
|
|
418
|
+
return createMiddleware(machine, {
|
|
419
|
+
before: (ctx) => {
|
|
420
|
+
if (!checker(ctx)) {
|
|
421
|
+
throw new Error(`Unauthorized transition: ${ctx.transitionName}`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Creates error reporting middleware.
|
|
429
|
+
*
|
|
430
|
+
* @template M - The machine type
|
|
431
|
+
* @param machine - The machine to monitor
|
|
432
|
+
* @param reporter - Error reporting function
|
|
433
|
+
* @param options - Configuration options
|
|
434
|
+
* @returns A new machine with error reporting
|
|
435
|
+
*/
|
|
436
|
+
export function withErrorReporting<M extends BaseMachine<any>>(
|
|
437
|
+
machine: M,
|
|
438
|
+
reporter: (error: Error, ctx: any) => void,
|
|
439
|
+
options: { includeArgs?: boolean } = {}
|
|
440
|
+
): M {
|
|
441
|
+
const { includeArgs = false } = options;
|
|
442
|
+
|
|
443
|
+
return createMiddleware(machine, {
|
|
444
|
+
error: (errorCtx) => {
|
|
445
|
+
// Format the context to match test expectations
|
|
446
|
+
const formattedCtx = {
|
|
447
|
+
transition: errorCtx.transitionName,
|
|
448
|
+
context: errorCtx.context,
|
|
449
|
+
...(includeArgs && { args: errorCtx.args })
|
|
450
|
+
};
|
|
451
|
+
reporter(errorCtx.error, formattedCtx);
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Creates performance monitoring middleware.
|
|
458
|
+
*
|
|
459
|
+
* @template M - The machine type
|
|
460
|
+
* @param machine - The machine to monitor
|
|
461
|
+
* @param tracker - Performance tracking function
|
|
462
|
+
* @returns A new machine with performance monitoring
|
|
463
|
+
*/
|
|
464
|
+
export function withPerformanceMonitoring<M extends BaseMachine<any>>(
|
|
465
|
+
machine: M,
|
|
466
|
+
tracker: (metric: { transitionName: string; duration: number; context: Context<M> }) => void
|
|
467
|
+
): M {
|
|
468
|
+
const startTimes = new Map<string, number>();
|
|
469
|
+
|
|
470
|
+
return createMiddleware(machine, {
|
|
471
|
+
before: (ctx) => {
|
|
472
|
+
startTimes.set(ctx.transitionName, Date.now());
|
|
473
|
+
},
|
|
474
|
+
after: (result) => {
|
|
475
|
+
const startTime = startTimes.get(result.transitionName);
|
|
476
|
+
if (startTime) {
|
|
477
|
+
const duration = Date.now() - startTime;
|
|
478
|
+
startTimes.delete(result.transitionName);
|
|
479
|
+
// For test compatibility, pass a single object as expected
|
|
480
|
+
const testResult = {
|
|
481
|
+
transitionName: result.transitionName,
|
|
482
|
+
duration,
|
|
483
|
+
context: result.nextContext || result.prevContext
|
|
484
|
+
};
|
|
485
|
+
tracker(testResult);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Creates retry middleware for failed transitions.
|
|
493
|
+
*
|
|
494
|
+
* @template M - The machine type
|
|
495
|
+
* @param machine - The machine to add retry logic to
|
|
496
|
+
* @param options - Retry configuration
|
|
497
|
+
* @returns A new machine with retry logic
|
|
498
|
+
*/
|
|
499
|
+
export function withRetry<M extends BaseMachine<any>>(
|
|
500
|
+
machine: M,
|
|
501
|
+
options: {
|
|
502
|
+
maxAttempts?: number;
|
|
503
|
+
maxRetries?: number; // alias for maxAttempts
|
|
504
|
+
shouldRetry?: (error: Error, attempt: number) => boolean;
|
|
505
|
+
backoffMs?: number | ((attempt: number) => number);
|
|
506
|
+
delay?: number | ((attempt: number) => number); // alias for backoffMs
|
|
507
|
+
backoffMultiplier?: number; // multiplier for exponential backoff
|
|
508
|
+
onRetry?: (error: Error, attempt: number) => void;
|
|
509
|
+
} = {}
|
|
510
|
+
): M {
|
|
511
|
+
const {
|
|
512
|
+
maxAttempts = options.maxRetries ?? 3,
|
|
513
|
+
shouldRetry = () => true,
|
|
514
|
+
backoffMs = options.delay ?? 100,
|
|
515
|
+
backoffMultiplier = 2,
|
|
516
|
+
onRetry
|
|
517
|
+
} = options;
|
|
518
|
+
|
|
519
|
+
// Create a wrapped machine that adds retry logic
|
|
520
|
+
const wrappedMachine: any = { ...machine };
|
|
521
|
+
|
|
522
|
+
// Wrap each transition function with retry logic
|
|
523
|
+
for (const prop in machine) {
|
|
524
|
+
if (!Object.prototype.hasOwnProperty.call(machine, prop)) continue;
|
|
525
|
+
const value = machine[prop];
|
|
526
|
+
if (typeof value === 'function' && prop !== 'context') {
|
|
527
|
+
wrappedMachine[prop] = async function (this: any, ...args: any[]) {
|
|
528
|
+
let lastError: Error;
|
|
529
|
+
let attempt = 0;
|
|
530
|
+
|
|
531
|
+
while (attempt < maxAttempts) {
|
|
532
|
+
try {
|
|
533
|
+
return await value.apply(this, args);
|
|
534
|
+
} catch (error) {
|
|
535
|
+
lastError = error as Error;
|
|
536
|
+
attempt++;
|
|
537
|
+
|
|
538
|
+
if (attempt < maxAttempts && shouldRetry(lastError, attempt)) {
|
|
539
|
+
onRetry?.(lastError, attempt);
|
|
540
|
+
const baseDelay = typeof backoffMs === 'function' ? backoffMs(attempt) : backoffMs;
|
|
541
|
+
const delay = baseDelay * Math.pow(backoffMultiplier, attempt - 1);
|
|
542
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
543
|
+
} else {
|
|
544
|
+
throw lastError;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
throw lastError!;
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return wrappedMachine;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Creates custom middleware from hooks.
|
|
562
|
+
*
|
|
563
|
+
* @template M - The machine type
|
|
564
|
+
* @param hooks - Middleware hooks
|
|
565
|
+
* @param options - Middleware options
|
|
566
|
+
* @returns A middleware function
|
|
567
|
+
*/
|
|
568
|
+
export function createCustomMiddleware<M extends BaseMachine<any>>(
|
|
569
|
+
hooks: MiddlewareHooks<Context<M>>,
|
|
570
|
+
options?: MiddlewareOptions
|
|
571
|
+
): (machine: M) => M {
|
|
572
|
+
return (machine: M) => createMiddleware(machine, hooks, options);
|
|
573
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file History tracking middleware
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { BaseMachine } from '../index';
|
|
6
|
+
import { createMiddleware } from './core';
|
|
7
|
+
|
|
8
|
+
// =============================================================================
|
|
9
|
+
// SECTION: HISTORY TYPES
|
|
10
|
+
// =============================================================================
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A single history entry recording a transition.
|
|
14
|
+
*/
|
|
15
|
+
export interface HistoryEntry {
|
|
16
|
+
/** Unique ID for this history entry */
|
|
17
|
+
id: string;
|
|
18
|
+
/** Name of the transition that was called */
|
|
19
|
+
transitionName: string;
|
|
20
|
+
/** Arguments passed to the transition */
|
|
21
|
+
args: any[];
|
|
22
|
+
/** Timestamp when the transition occurred */
|
|
23
|
+
timestamp: number;
|
|
24
|
+
/** Optional serialized version of args for persistence */
|
|
25
|
+
serializedArgs?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Serializer interface for converting context/args to/from strings.
|
|
30
|
+
*/
|
|
31
|
+
export interface Serializer<T = any> {
|
|
32
|
+
serialize: (value: T) => string;
|
|
33
|
+
deserialize: (str: string) => T;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// =============================================================================
|
|
37
|
+
// SECTION: HISTORY MIDDLEWARE
|
|
38
|
+
// =============================================================================
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Creates a machine with history tracking capabilities.
|
|
42
|
+
* Records all transitions that occur, allowing you to see the sequence of state changes.
|
|
43
|
+
*
|
|
44
|
+
* @template M - The machine type
|
|
45
|
+
* @param machine - The machine to track
|
|
46
|
+
* @param options - Configuration options
|
|
47
|
+
* @returns A new machine with history tracking
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```typescript
|
|
51
|
+
* const tracked = withHistory(counter, { maxSize: 50 });
|
|
52
|
+
* tracked.increment();
|
|
53
|
+
* console.log(tracked.history); // [{ id: "entry-1", transitionName: "increment", ... }]
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export function withHistory<M extends BaseMachine<any>>(
|
|
57
|
+
machine: M,
|
|
58
|
+
options: {
|
|
59
|
+
/** Maximum number of history entries to keep (default: unlimited) */
|
|
60
|
+
maxSize?: number;
|
|
61
|
+
/** Optional serializer for transition arguments */
|
|
62
|
+
serializer?: Serializer<any[]>;
|
|
63
|
+
/** Callback when a transition occurs */
|
|
64
|
+
onEntry?: (entry: HistoryEntry) => void;
|
|
65
|
+
} = {}
|
|
66
|
+
): M & { history: HistoryEntry[]; clearHistory: () => void } {
|
|
67
|
+
const { maxSize, serializer, onEntry } = options;
|
|
68
|
+
const history: HistoryEntry[] = [];
|
|
69
|
+
let entryId = 0;
|
|
70
|
+
|
|
71
|
+
const instrumentedMachine = createMiddleware(machine, {
|
|
72
|
+
before: ({ transitionName, args }) => {
|
|
73
|
+
const entry: HistoryEntry = {
|
|
74
|
+
id: `entry-${entryId++}`,
|
|
75
|
+
transitionName,
|
|
76
|
+
args: [...args],
|
|
77
|
+
timestamp: Date.now()
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
if (serializer) {
|
|
81
|
+
try {
|
|
82
|
+
entry.serializedArgs = serializer.serialize(args);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
console.error('Failed to serialize history args:', err);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
history.push(entry);
|
|
89
|
+
|
|
90
|
+
// Enforce max size
|
|
91
|
+
if (maxSize && history.length > maxSize) {
|
|
92
|
+
history.shift();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
onEntry?.(entry);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Attach history properties to the machine
|
|
100
|
+
return Object.assign(instrumentedMachine, {
|
|
101
|
+
history,
|
|
102
|
+
clearHistory: () => { history.length = 0; entryId = 0; }
|
|
103
|
+
});
|
|
104
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Middleware module exports
|
|
3
|
+
* @description Unified exports for all middleware functionality
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Core middleware types and functions
|
|
7
|
+
export * from './core';
|
|
8
|
+
|
|
9
|
+
// Specialized middleware modules
|
|
10
|
+
export * from './history';
|
|
11
|
+
export * from './snapshot';
|
|
12
|
+
export * from './time-travel';
|
|
13
|
+
export * from './composition';
|