@doeixd/machine 0.0.12 → 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 +90 -15
- package/dist/cjs/development/core.js +1852 -0
- package/dist/cjs/development/core.js.map +7 -0
- package/dist/cjs/development/index.js +1348 -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 +1348 -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 +60 -5
- package/dist/types/functional-combinators.d.ts.map +1 -1
- package/dist/types/index.d.ts +242 -19
- 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 +76 -3
- package/src/index.ts +376 -102
- 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,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Snapshot tracking middleware for context state
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Context, BaseMachine } from '../index';
|
|
6
|
+
import { createMiddleware } from './core';
|
|
7
|
+
import type { Serializer } from './history';
|
|
8
|
+
|
|
9
|
+
// =============================================================================
|
|
10
|
+
// SECTION: SNAPSHOT TYPES
|
|
11
|
+
// =============================================================================
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A snapshot of machine context before and after a transition.
|
|
15
|
+
*/
|
|
16
|
+
export interface ContextSnapshot<C extends object> {
|
|
17
|
+
/** Unique ID for this snapshot */
|
|
18
|
+
id: string;
|
|
19
|
+
/** Name of the transition that caused this snapshot */
|
|
20
|
+
transitionName: string;
|
|
21
|
+
/** Context before the transition */
|
|
22
|
+
before: C;
|
|
23
|
+
/** Context after the transition */
|
|
24
|
+
after: C;
|
|
25
|
+
/** Timestamp of the snapshot */
|
|
26
|
+
timestamp: number;
|
|
27
|
+
/** Optional serialized versions of contexts */
|
|
28
|
+
serializedBefore?: string;
|
|
29
|
+
serializedAfter?: string;
|
|
30
|
+
/** Optional diff information */
|
|
31
|
+
diff?: any;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// =============================================================================
|
|
35
|
+
// SECTION: SNAPSHOT MIDDLEWARE
|
|
36
|
+
// =============================================================================
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Creates a machine with snapshot tracking capabilities.
|
|
40
|
+
* Records context state before and after each transition for debugging and inspection.
|
|
41
|
+
*
|
|
42
|
+
* @template M - The machine type
|
|
43
|
+
* @param machine - The machine to track
|
|
44
|
+
* @param options - Configuration options
|
|
45
|
+
* @returns A new machine with snapshot tracking
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```typescript
|
|
49
|
+
* const tracked = withSnapshot(counter, {
|
|
50
|
+
* maxSize: 50,
|
|
51
|
+
* serializer: {
|
|
52
|
+
* serialize: (ctx) => JSON.stringify(ctx),
|
|
53
|
+
* deserialize: (str) => JSON.parse(str)
|
|
54
|
+
* }
|
|
55
|
+
* });
|
|
56
|
+
*
|
|
57
|
+
* tracked.increment();
|
|
58
|
+
* console.log(tracked.snapshots); // [{ before: { count: 0 }, after: { count: 1 }, ... }]
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export function withSnapshot<M extends BaseMachine<any>>(
|
|
62
|
+
machine: M,
|
|
63
|
+
options: {
|
|
64
|
+
/** Maximum number of snapshots to keep (default: unlimited) */
|
|
65
|
+
maxSize?: number;
|
|
66
|
+
/** Optional serializer for context */
|
|
67
|
+
serializer?: Serializer<Context<M>>;
|
|
68
|
+
/** Custom function to capture additional snapshot data */
|
|
69
|
+
captureSnapshot?: (before: Context<M>, after: Context<M>) => any;
|
|
70
|
+
/** Only capture snapshots where context actually changed */
|
|
71
|
+
onlyOnChange?: boolean;
|
|
72
|
+
} = {}
|
|
73
|
+
): M & {
|
|
74
|
+
snapshots: ContextSnapshot<Context<M>>[];
|
|
75
|
+
clearSnapshots: () => void;
|
|
76
|
+
restoreSnapshot: (snapshot: ContextSnapshot<Context<M>>['before']) => M;
|
|
77
|
+
} {
|
|
78
|
+
const {
|
|
79
|
+
maxSize,
|
|
80
|
+
serializer,
|
|
81
|
+
captureSnapshot,
|
|
82
|
+
onlyOnChange = false
|
|
83
|
+
} = options;
|
|
84
|
+
|
|
85
|
+
const snapshots: ContextSnapshot<Context<M>>[] = [];
|
|
86
|
+
let snapshotId = 0;
|
|
87
|
+
|
|
88
|
+
const instrumentedMachine = createMiddleware(machine, {
|
|
89
|
+
after: ({ transitionName, prevContext, nextContext }) => {
|
|
90
|
+
// Skip if only capturing on change and context didn't change
|
|
91
|
+
if (onlyOnChange && JSON.stringify(prevContext) === JSON.stringify(nextContext)) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const snapshot: ContextSnapshot<Context<M>> = {
|
|
96
|
+
id: `snapshot-${snapshotId++}`,
|
|
97
|
+
transitionName,
|
|
98
|
+
before: { ...prevContext },
|
|
99
|
+
after: { ...nextContext },
|
|
100
|
+
timestamp: Date.now()
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Serialize contexts if serializer provided
|
|
104
|
+
if (serializer) {
|
|
105
|
+
try {
|
|
106
|
+
snapshot.serializedBefore = serializer.serialize(prevContext);
|
|
107
|
+
snapshot.serializedAfter = serializer.serialize(nextContext);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
console.error('Failed to serialize snapshot:', err);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Capture custom snapshot data
|
|
114
|
+
if (captureSnapshot) {
|
|
115
|
+
try {
|
|
116
|
+
snapshot.diff = captureSnapshot(prevContext, nextContext);
|
|
117
|
+
} catch (err) {
|
|
118
|
+
console.error('Failed to capture snapshot:', err);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
snapshots.push(snapshot);
|
|
123
|
+
|
|
124
|
+
// Enforce max size
|
|
125
|
+
if (maxSize && snapshots.length > maxSize) {
|
|
126
|
+
snapshots.shift();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Helper to restore machine to a previous state
|
|
132
|
+
const restoreSnapshot = (context: Context<M>): M => {
|
|
133
|
+
// Find the machine's transition functions (excluding context and snapshot properties)
|
|
134
|
+
const transitions = Object.fromEntries(
|
|
135
|
+
Object.entries(machine).filter(([key]) =>
|
|
136
|
+
key !== 'context' &&
|
|
137
|
+
key !== 'snapshots' &&
|
|
138
|
+
key !== 'clearSnapshots' &&
|
|
139
|
+
key !== 'restoreSnapshot' &&
|
|
140
|
+
typeof machine[key as keyof M] === 'function'
|
|
141
|
+
)
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
return Object.assign({ context }, transitions) as M;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// Attach snapshot properties to the machine
|
|
148
|
+
return Object.assign(instrumentedMachine, {
|
|
149
|
+
snapshots,
|
|
150
|
+
clearSnapshots: () => { snapshots.length = 0; snapshotId = 0; },
|
|
151
|
+
restoreSnapshot
|
|
152
|
+
});
|
|
153
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Time travel middleware combining history, snapshots, and replay capabilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Context, BaseMachine } from '../index';
|
|
6
|
+
import { createMiddleware } from './core';
|
|
7
|
+
import { type HistoryEntry, type Serializer } from './history';
|
|
8
|
+
import { type ContextSnapshot } from './snapshot';
|
|
9
|
+
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// SECTION: TIME TRAVEL TYPES
|
|
12
|
+
// =============================================================================
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A machine enhanced with history tracking capabilities.
|
|
16
|
+
*/
|
|
17
|
+
export type WithHistory<M extends BaseMachine<any>> = M & {
|
|
18
|
+
/** History of all transitions */
|
|
19
|
+
history: HistoryEntry[];
|
|
20
|
+
/** Clear all history */
|
|
21
|
+
clearHistory: () => void;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A machine enhanced with snapshot tracking capabilities.
|
|
26
|
+
*/
|
|
27
|
+
export type WithSnapshot<M extends BaseMachine<any>> = M & {
|
|
28
|
+
/** Snapshots of context before/after each transition */
|
|
29
|
+
snapshots: ContextSnapshot<Context<M>>[];
|
|
30
|
+
/** Clear all snapshots */
|
|
31
|
+
clearSnapshots: () => void;
|
|
32
|
+
/** Restore machine to a previous context state */
|
|
33
|
+
restoreSnapshot: (context: Context<M>) => M;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A machine enhanced with time travel capabilities.
|
|
38
|
+
*/
|
|
39
|
+
export type WithTimeTravel<M extends BaseMachine<any>> = M & {
|
|
40
|
+
/** History of all transitions */
|
|
41
|
+
history: HistoryEntry[];
|
|
42
|
+
/** Snapshots of context before/after each transition */
|
|
43
|
+
snapshots: ContextSnapshot<Context<M>>[];
|
|
44
|
+
/** Clear all history and snapshots */
|
|
45
|
+
clearTimeTravel: () => void;
|
|
46
|
+
/** Clear just the history */
|
|
47
|
+
clearHistory: () => void;
|
|
48
|
+
/** Clear just the snapshots */
|
|
49
|
+
clearSnapshots: () => void;
|
|
50
|
+
/** Restore machine to a previous context state */
|
|
51
|
+
restoreSnapshot: (context: Context<M>) => M;
|
|
52
|
+
/** Replay transitions from a specific point in history */
|
|
53
|
+
replayFrom: (startIndex: number) => M;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// =============================================================================
|
|
57
|
+
// SECTION: TIME TRAVEL MIDDLEWARE
|
|
58
|
+
// =============================================================================
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Creates a machine with full time travel debugging capabilities.
|
|
62
|
+
* Combines history tracking, snapshots, and replay functionality.
|
|
63
|
+
*
|
|
64
|
+
* @template M - The machine type
|
|
65
|
+
* @param machine - The machine to enhance
|
|
66
|
+
* @param options - Configuration options
|
|
67
|
+
* @returns A machine with time travel capabilities
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```typescript
|
|
71
|
+
* const debugMachine = withTimeTravel(counter);
|
|
72
|
+
*
|
|
73
|
+
* // Make some transitions
|
|
74
|
+
* debugMachine.increment();
|
|
75
|
+
* debugMachine.increment();
|
|
76
|
+
* debugMachine.decrement();
|
|
77
|
+
*
|
|
78
|
+
* // Time travel to previous states
|
|
79
|
+
* const previousState = debugMachine.replayFrom(0); // Replay from start
|
|
80
|
+
* const undoLast = debugMachine.restoreSnapshot(debugMachine.snapshots[1].before);
|
|
81
|
+
*
|
|
82
|
+
* // Inspect history
|
|
83
|
+
* console.log(debugMachine.history);
|
|
84
|
+
* console.log(debugMachine.snapshots);
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export function withTimeTravel<M extends BaseMachine<any>>(
|
|
88
|
+
machine: M,
|
|
89
|
+
options: {
|
|
90
|
+
/** Maximum number of history entries/snapshots to keep */
|
|
91
|
+
maxSize?: number;
|
|
92
|
+
/** Optional serializer for persistence */
|
|
93
|
+
serializer?: Serializer;
|
|
94
|
+
/** Callback when history/snapshot events occur */
|
|
95
|
+
onRecord?: (type: 'history' | 'snapshot', data: any) => void;
|
|
96
|
+
} = {}
|
|
97
|
+
): WithTimeTravel<M> {
|
|
98
|
+
const { maxSize, serializer, onRecord } = options;
|
|
99
|
+
|
|
100
|
+
// Create separate history and snapshot tracking
|
|
101
|
+
const history: HistoryEntry[] = [];
|
|
102
|
+
const snapshots: ContextSnapshot<Context<M>>[] = [];
|
|
103
|
+
let historyId = 0;
|
|
104
|
+
let snapshotId = 0;
|
|
105
|
+
|
|
106
|
+
// Create middleware that handles both history and snapshots
|
|
107
|
+
const instrumentedMachine = createMiddleware(machine, {
|
|
108
|
+
before: ({ transitionName, args }: { transitionName: string; args: any[] }) => {
|
|
109
|
+
const entry: HistoryEntry = {
|
|
110
|
+
id: `entry-${historyId++}`,
|
|
111
|
+
transitionName,
|
|
112
|
+
args: [...args],
|
|
113
|
+
timestamp: Date.now()
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
if (serializer) {
|
|
117
|
+
try {
|
|
118
|
+
entry.serializedArgs = serializer.serialize(args);
|
|
119
|
+
} catch (err) {
|
|
120
|
+
console.error('Failed to serialize history args:', err);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
history.push(entry);
|
|
125
|
+
|
|
126
|
+
// Enforce max size
|
|
127
|
+
if (maxSize && history.length > maxSize) {
|
|
128
|
+
history.shift();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
onRecord?.('history', entry);
|
|
132
|
+
},
|
|
133
|
+
after: ({ transitionName, prevContext, nextContext }: { transitionName: string; prevContext: Context<M>; nextContext: Context<M> }) => {
|
|
134
|
+
const snapshot: ContextSnapshot<Context<M>> = {
|
|
135
|
+
id: `snapshot-${snapshotId++}`,
|
|
136
|
+
transitionName,
|
|
137
|
+
before: { ...prevContext },
|
|
138
|
+
after: { ...nextContext },
|
|
139
|
+
timestamp: Date.now()
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// Serialize contexts if serializer provided
|
|
143
|
+
if (serializer) {
|
|
144
|
+
try {
|
|
145
|
+
snapshot.serializedBefore = serializer.serialize(prevContext);
|
|
146
|
+
snapshot.serializedAfter = serializer.serialize(nextContext);
|
|
147
|
+
} catch (err) {
|
|
148
|
+
console.error('Failed to serialize snapshot:', err);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
snapshots.push(snapshot);
|
|
153
|
+
|
|
154
|
+
// Enforce max size
|
|
155
|
+
if (maxSize && snapshots.length > maxSize) {
|
|
156
|
+
snapshots.shift();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
onRecord?.('snapshot', snapshot);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Helper to restore machine to a previous state
|
|
164
|
+
const restoreSnapshot = (context: Context<M>): M => {
|
|
165
|
+
// Find the machine's transition functions (excluding context and snapshot properties)
|
|
166
|
+
const transitions = Object.fromEntries(
|
|
167
|
+
Object.entries(machine).filter(([key]) =>
|
|
168
|
+
key !== 'context' &&
|
|
169
|
+
key !== 'history' &&
|
|
170
|
+
key !== 'snapshots' &&
|
|
171
|
+
key !== 'clearHistory' &&
|
|
172
|
+
key !== 'clearSnapshots' &&
|
|
173
|
+
key !== 'restoreSnapshot' &&
|
|
174
|
+
key !== 'clearTimeTravel' &&
|
|
175
|
+
key !== 'replayFrom' &&
|
|
176
|
+
typeof machine[key as keyof M] === 'function'
|
|
177
|
+
)
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
return Object.assign({ context }, transitions) as M;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// Create replay functionality
|
|
184
|
+
const replayFrom = (startIndex: number): M => {
|
|
185
|
+
if (startIndex < 0 || startIndex >= history.length) {
|
|
186
|
+
throw new Error(`Invalid replay start index: ${startIndex}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Start from the context at the specified history index
|
|
190
|
+
let currentContext = snapshots[startIndex]?.before;
|
|
191
|
+
if (!currentContext) {
|
|
192
|
+
throw new Error(`No snapshot available for index ${startIndex}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Get all transitions from start index to end
|
|
196
|
+
const transitionsToReplay = history.slice(startIndex);
|
|
197
|
+
|
|
198
|
+
// Create a fresh machine instance
|
|
199
|
+
const freshMachine = Object.assign(
|
|
200
|
+
{ context: currentContext },
|
|
201
|
+
Object.fromEntries(
|
|
202
|
+
Object.entries(machine).filter(([key]) =>
|
|
203
|
+
key !== 'context' &&
|
|
204
|
+
typeof machine[key as keyof M] === 'function'
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
) as M;
|
|
208
|
+
|
|
209
|
+
// Replay each transition
|
|
210
|
+
let replayedMachine = freshMachine;
|
|
211
|
+
for (const entry of transitionsToReplay) {
|
|
212
|
+
const transitionFn = replayedMachine[entry.transitionName as keyof M] as Function;
|
|
213
|
+
if (transitionFn) {
|
|
214
|
+
replayedMachine = transitionFn.apply(replayedMachine.context, entry.args);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return replayedMachine;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// Return machine with all time travel capabilities
|
|
222
|
+
return Object.assign(instrumentedMachine, {
|
|
223
|
+
history,
|
|
224
|
+
snapshots,
|
|
225
|
+
clearHistory: () => { history.length = 0; historyId = 0; },
|
|
226
|
+
clearSnapshots: () => { snapshots.length = 0; snapshotId = 0; },
|
|
227
|
+
clearTimeTravel: () => {
|
|
228
|
+
history.length = 0;
|
|
229
|
+
snapshots.length = 0;
|
|
230
|
+
historyId = 0;
|
|
231
|
+
snapshotId = 0;
|
|
232
|
+
},
|
|
233
|
+
restoreSnapshot,
|
|
234
|
+
replayFrom
|
|
235
|
+
}) as WithTimeTravel<M>;
|
|
236
|
+
}
|