@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.
Files changed (50) hide show
  1. package/README.md +90 -15
  2. package/dist/cjs/development/core.js +1852 -0
  3. package/dist/cjs/development/core.js.map +7 -0
  4. package/dist/cjs/development/index.js +1348 -1374
  5. package/dist/cjs/development/index.js.map +4 -4
  6. package/dist/cjs/production/core.js +1 -0
  7. package/dist/cjs/production/index.js +5 -5
  8. package/dist/esm/development/core.js +1829 -0
  9. package/dist/esm/development/core.js.map +7 -0
  10. package/dist/esm/development/index.js +1348 -1374
  11. package/dist/esm/development/index.js.map +4 -4
  12. package/dist/esm/production/core.js +1 -0
  13. package/dist/esm/production/index.js +5 -5
  14. package/dist/types/core.d.ts +18 -0
  15. package/dist/types/core.d.ts.map +1 -0
  16. package/dist/types/functional-combinators.d.ts +60 -5
  17. package/dist/types/functional-combinators.d.ts.map +1 -1
  18. package/dist/types/index.d.ts +242 -19
  19. package/dist/types/index.d.ts.map +1 -1
  20. package/dist/types/middleware/composition.d.ts +460 -0
  21. package/dist/types/middleware/composition.d.ts.map +1 -0
  22. package/dist/types/middleware/core.d.ts +196 -0
  23. package/dist/types/middleware/core.d.ts.map +1 -0
  24. package/dist/types/middleware/history.d.ts +54 -0
  25. package/dist/types/middleware/history.d.ts.map +1 -0
  26. package/dist/types/middleware/index.d.ts +10 -0
  27. package/dist/types/middleware/index.d.ts.map +1 -0
  28. package/dist/types/middleware/snapshot.d.ts +63 -0
  29. package/dist/types/middleware/snapshot.d.ts.map +1 -0
  30. package/dist/types/middleware/time-travel.d.ts +81 -0
  31. package/dist/types/middleware/time-travel.d.ts.map +1 -0
  32. package/package.json +19 -6
  33. package/src/core.ts +167 -0
  34. package/src/entry-react.ts +9 -0
  35. package/src/entry-solid.ts +9 -0
  36. package/src/functional-combinators.ts +76 -3
  37. package/src/index.ts +376 -102
  38. package/src/middleware/composition.ts +944 -0
  39. package/src/middleware/core.ts +573 -0
  40. package/src/middleware/history.ts +104 -0
  41. package/src/middleware/index.ts +13 -0
  42. package/src/middleware/snapshot.ts +153 -0
  43. package/src/middleware/time-travel.ts +236 -0
  44. package/src/middleware.ts +735 -1614
  45. package/src/prototype_functional.ts +46 -0
  46. package/src/reproduce_issue.ts +26 -0
  47. package/dist/types/middleware.d.ts +0 -1048
  48. package/dist/types/middleware.d.ts.map +0 -1
  49. package/dist/types/runtime-extract.d.ts +0 -53
  50. 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
+ }