effect-machine 0.1.0 → 0.2.0
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 +17 -0
- package/package.json +1 -1
- package/src/actor.ts +544 -455
- package/src/cluster/entity-machine.ts +81 -82
- package/src/index.ts +1 -0
- package/src/inspection.ts +17 -0
- package/src/internal/inspection.ts +18 -0
- package/src/internal/transition.ts +150 -94
- package/src/machine.ts +139 -71
- package/src/persistence/adapter.ts +4 -3
- package/src/persistence/adapters/in-memory.ts +201 -182
- package/src/persistence/persistent-actor.ts +582 -386
- package/src/testing.ts +92 -98
package/src/testing.ts
CHANGED
|
@@ -33,7 +33,7 @@ export interface SimulationResult<S> {
|
|
|
33
33
|
* expect(result.states).toHaveLength(3) // Idle -> Loading -> Success
|
|
34
34
|
* ```
|
|
35
35
|
*/
|
|
36
|
-
export const simulate = <
|
|
36
|
+
export const simulate = Effect.fn("effect-machine.simulate")(function* <
|
|
37
37
|
S extends { readonly _tag: string },
|
|
38
38
|
E extends { readonly _tag: string },
|
|
39
39
|
R,
|
|
@@ -43,34 +43,33 @@ export const simulate = <
|
|
|
43
43
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Schema fields need wide acceptance
|
|
44
44
|
machine: Machine<S, E, R, any, any, GD, EFD>,
|
|
45
45
|
events: ReadonlyArray<E>,
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
};
|
|
46
|
+
) {
|
|
47
|
+
// Create a dummy self for slot accessors
|
|
48
|
+
const dummySelf: MachineRef<E> = {
|
|
49
|
+
send: Effect.fn("effect-machine.testing.simulate.send")((_event: E) => Effect.void),
|
|
50
|
+
};
|
|
52
51
|
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
let currentState = machine.initial;
|
|
53
|
+
const states: S[] = [currentState];
|
|
55
54
|
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
for (const event of events) {
|
|
56
|
+
const result = yield* executeTransition(machine, currentState, event, dummySelf);
|
|
58
57
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
if (!result.transitioned) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
62
61
|
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
currentState = result.newState;
|
|
63
|
+
states.push(currentState);
|
|
65
64
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
65
|
+
// Stop if final state
|
|
66
|
+
if (machine.finalStates.has(currentState._tag)) {
|
|
67
|
+
break;
|
|
70
68
|
}
|
|
69
|
+
}
|
|
71
70
|
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
return { states, finalState: currentState };
|
|
72
|
+
});
|
|
74
73
|
|
|
75
74
|
// AssertionError is exported from errors.ts
|
|
76
75
|
export { AssertionError } from "./errors.js";
|
|
@@ -78,7 +77,7 @@ export { AssertionError } from "./errors.js";
|
|
|
78
77
|
/**
|
|
79
78
|
* Assert that a machine can reach a specific state given a sequence of events
|
|
80
79
|
*/
|
|
81
|
-
export const assertReaches = <
|
|
80
|
+
export const assertReaches = Effect.fn("effect-machine.assertReaches")(function* <
|
|
82
81
|
S extends { readonly _tag: string },
|
|
83
82
|
E extends { readonly _tag: string },
|
|
84
83
|
R,
|
|
@@ -89,18 +88,17 @@ export const assertReaches = <
|
|
|
89
88
|
machine: Machine<S, E, R, any, any, GD, EFD>,
|
|
90
89
|
events: ReadonlyArray<E>,
|
|
91
90
|
expectedTag: string,
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
});
|
|
91
|
+
) {
|
|
92
|
+
const result = yield* simulate(machine, events);
|
|
93
|
+
if (result.finalState._tag !== expectedTag) {
|
|
94
|
+
return yield* new AssertionError({
|
|
95
|
+
message:
|
|
96
|
+
`Expected final state "${expectedTag}" but got "${result.finalState._tag}". ` +
|
|
97
|
+
`States visited: ${result.states.map((s) => s._tag).join(" -> ")}`,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return result.finalState;
|
|
101
|
+
});
|
|
104
102
|
|
|
105
103
|
/**
|
|
106
104
|
* Assert that a machine follows a specific path of state tags
|
|
@@ -114,7 +112,7 @@ export const assertReaches = <
|
|
|
114
112
|
* )
|
|
115
113
|
* ```
|
|
116
114
|
*/
|
|
117
|
-
export const assertPath = <
|
|
115
|
+
export const assertPath = Effect.fn("effect-machine.assertPath")(function* <
|
|
118
116
|
S extends { readonly _tag: string },
|
|
119
117
|
E extends { readonly _tag: string },
|
|
120
118
|
R,
|
|
@@ -125,33 +123,32 @@ export const assertPath = <
|
|
|
125
123
|
machine: Machine<S, E, R, any, any, GD, EFD>,
|
|
126
124
|
events: ReadonlyArray<E>,
|
|
127
125
|
expectedPath: ReadonlyArray<string>,
|
|
128
|
-
)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
126
|
+
) {
|
|
127
|
+
const result = yield* simulate(machine, events);
|
|
128
|
+
const actualPath = result.states.map((s) => s._tag);
|
|
129
|
+
|
|
130
|
+
if (actualPath.length !== expectedPath.length) {
|
|
131
|
+
return yield* new AssertionError({
|
|
132
|
+
message:
|
|
133
|
+
`Path length mismatch. Expected ${expectedPath.length} states but got ${actualPath.length}.\n` +
|
|
134
|
+
`Expected: ${expectedPath.join(" -> ")}\n` +
|
|
135
|
+
`Actual: ${actualPath.join(" -> ")}`,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
132
138
|
|
|
133
|
-
|
|
139
|
+
for (let i = 0; i < expectedPath.length; i++) {
|
|
140
|
+
if (actualPath[i] !== expectedPath[i]) {
|
|
134
141
|
return yield* new AssertionError({
|
|
135
142
|
message:
|
|
136
|
-
`Path
|
|
143
|
+
`Path mismatch at position ${i}. Expected "${expectedPath[i]}" but got "${actualPath[i]}".\n` +
|
|
137
144
|
`Expected: ${expectedPath.join(" -> ")}\n` +
|
|
138
145
|
`Actual: ${actualPath.join(" -> ")}`,
|
|
139
146
|
});
|
|
140
147
|
}
|
|
148
|
+
}
|
|
141
149
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
return yield* new AssertionError({
|
|
145
|
-
message:
|
|
146
|
-
`Path mismatch at position ${i}. Expected "${expectedPath[i]}" but got "${actualPath[i]}".\n` +
|
|
147
|
-
`Expected: ${expectedPath.join(" -> ")}\n` +
|
|
148
|
-
`Actual: ${actualPath.join(" -> ")}`,
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return result;
|
|
154
|
-
});
|
|
150
|
+
return result;
|
|
151
|
+
});
|
|
155
152
|
|
|
156
153
|
/**
|
|
157
154
|
* Assert that a machine never reaches a specific state given a sequence of events
|
|
@@ -166,7 +163,7 @@ export const assertPath = <
|
|
|
166
163
|
* )
|
|
167
164
|
* ```
|
|
168
165
|
*/
|
|
169
|
-
export const assertNeverReaches = <
|
|
166
|
+
export const assertNeverReaches = Effect.fn("effect-machine.assertNeverReaches")(function* <
|
|
170
167
|
S extends { readonly _tag: string },
|
|
171
168
|
E extends { readonly _tag: string },
|
|
172
169
|
R,
|
|
@@ -177,21 +174,20 @@ export const assertNeverReaches = <
|
|
|
177
174
|
machine: Machine<S, E, R, any, any, GD, EFD>,
|
|
178
175
|
events: ReadonlyArray<E>,
|
|
179
176
|
forbiddenTag: string,
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
const result = yield* simulate(machine, events);
|
|
177
|
+
) {
|
|
178
|
+
const result = yield* simulate(machine, events);
|
|
183
179
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
180
|
+
const visitedIndex = result.states.findIndex((s) => s._tag === forbiddenTag);
|
|
181
|
+
if (visitedIndex !== -1) {
|
|
182
|
+
return yield* new AssertionError({
|
|
183
|
+
message:
|
|
184
|
+
`Machine reached forbidden state "${forbiddenTag}" at position ${visitedIndex}.\n` +
|
|
185
|
+
`States visited: ${result.states.map((s) => s._tag).join(" -> ")}`,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
192
188
|
|
|
193
|
-
|
|
194
|
-
|
|
189
|
+
return result;
|
|
190
|
+
});
|
|
195
191
|
|
|
196
192
|
/**
|
|
197
193
|
* Create a controllable test harness for a machine
|
|
@@ -234,7 +230,7 @@ export interface TestHarnessOptions<S, E> {
|
|
|
234
230
|
* })
|
|
235
231
|
* ```
|
|
236
232
|
*/
|
|
237
|
-
export const createTestHarness = <
|
|
233
|
+
export const createTestHarness = Effect.fn("effect-machine.createTestHarness")(function* <
|
|
238
234
|
S extends { readonly _tag: string },
|
|
239
235
|
E extends { readonly _tag: string },
|
|
240
236
|
R,
|
|
@@ -244,39 +240,37 @@ export const createTestHarness = <
|
|
|
244
240
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Schema fields need wide acceptance
|
|
245
241
|
machine: Machine<S, E, R, any, any, GD, EFD>,
|
|
246
242
|
options?: TestHarnessOptions<S, E>,
|
|
247
|
-
)
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
};
|
|
243
|
+
) {
|
|
244
|
+
// Create a dummy self for slot accessors
|
|
245
|
+
const dummySelf: MachineRef<E> = {
|
|
246
|
+
send: Effect.fn("effect-machine.testing.harness.send")((_event: E) => Effect.void),
|
|
247
|
+
};
|
|
253
248
|
|
|
254
|
-
|
|
249
|
+
const stateRef = yield* SubscriptionRef.make(machine.initial);
|
|
255
250
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const currentState = yield* SubscriptionRef.get(stateRef);
|
|
251
|
+
const send = Effect.fn("effect-machine.testHarness.send")(function* (event: E) {
|
|
252
|
+
const currentState = yield* SubscriptionRef.get(stateRef);
|
|
259
253
|
|
|
260
|
-
|
|
254
|
+
const result = yield* executeTransition(machine, currentState, event, dummySelf);
|
|
261
255
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const newState = result.newState;
|
|
267
|
-
yield* SubscriptionRef.set(stateRef, newState);
|
|
256
|
+
if (!result.transitioned) {
|
|
257
|
+
return currentState;
|
|
258
|
+
}
|
|
268
259
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
options.onTransition(currentState, event, newState);
|
|
272
|
-
}
|
|
260
|
+
const newState = result.newState;
|
|
261
|
+
yield* SubscriptionRef.set(stateRef, newState);
|
|
273
262
|
|
|
274
|
-
|
|
275
|
-
|
|
263
|
+
// Call transition observer
|
|
264
|
+
if (options?.onTransition !== undefined) {
|
|
265
|
+
options.onTransition(currentState, event, newState);
|
|
266
|
+
}
|
|
276
267
|
|
|
277
|
-
return
|
|
278
|
-
state: stateRef,
|
|
279
|
-
send,
|
|
280
|
-
getState: SubscriptionRef.get(stateRef),
|
|
281
|
-
};
|
|
268
|
+
return newState;
|
|
282
269
|
});
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
state: stateRef,
|
|
273
|
+
send,
|
|
274
|
+
getState: SubscriptionRef.get(stateRef),
|
|
275
|
+
};
|
|
276
|
+
});
|