effect-machine 0.2.3 → 0.3.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 +93 -52
- package/package.json +2 -2
- package/src/actor.ts +126 -111
- package/src/errors.ts +1 -1
- package/src/index.ts +1 -0
- package/src/inspection.ts +24 -15
- package/src/internal/transition.ts +8 -2
- package/src/machine.ts +228 -145
- package/src/persistence/adapter.ts +2 -2
- package/src/persistence/persistent-actor.ts +4 -10
- package/src/schema.ts +52 -6
- package/src/testing.ts +35 -27
|
@@ -28,7 +28,6 @@ import {
|
|
|
28
28
|
} from "../internal/transition.js";
|
|
29
29
|
import type { ProcessEventError } from "../internal/transition.js";
|
|
30
30
|
import type { GuardsDef, EffectsDef } from "../slot.js";
|
|
31
|
-
import { UnprovidedSlotsError } from "../errors.js";
|
|
32
31
|
import { INTERNAL_INIT_EVENT } from "../internal/utils.js";
|
|
33
32
|
import { emitWithTimestamp } from "../internal/inspection.js";
|
|
34
33
|
|
|
@@ -252,11 +251,6 @@ export const createPersistentActor = Effect.fn("effect-machine.persistentActor.s
|
|
|
252
251
|
EFD
|
|
253
252
|
>;
|
|
254
253
|
|
|
255
|
-
const missing = typedMachine._missingSlots();
|
|
256
|
-
if (missing.length > 0) {
|
|
257
|
-
return yield* new UnprovidedSlotsError({ slots: missing });
|
|
258
|
-
}
|
|
259
|
-
|
|
260
254
|
// Get optional inspector from context
|
|
261
255
|
const inspector = Option.getOrUndefined(yield* Effect.serviceOption(InspectorTag)) as
|
|
262
256
|
| Inspector<S, E>
|
|
@@ -332,7 +326,7 @@ export const createPersistentActor = Effect.fn("effect-machine.persistentActor.s
|
|
|
332
326
|
|
|
333
327
|
const snapshotEnabledRef = yield* Ref.make(true);
|
|
334
328
|
const persistenceQueue = yield* Queue.unbounded<Effect.Effect<void, never>>();
|
|
335
|
-
const persistenceFiber = yield* Effect.
|
|
329
|
+
const persistenceFiber = yield* Effect.forkDaemon(persistenceWorker(persistenceQueue));
|
|
336
330
|
|
|
337
331
|
// Save initial metadata
|
|
338
332
|
yield* Queue.offer(
|
|
@@ -342,7 +336,7 @@ export const createPersistentActor = Effect.fn("effect-machine.persistentActor.s
|
|
|
342
336
|
|
|
343
337
|
// Snapshot scheduler
|
|
344
338
|
const snapshotQueue = yield* Queue.unbounded<{ state: S; version: number }>();
|
|
345
|
-
const snapshotFiber = yield* Effect.
|
|
339
|
+
const snapshotFiber = yield* Effect.forkDaemon(
|
|
346
340
|
snapshotWorker(id, persistence, adapter, snapshotQueue, snapshotEnabledRef),
|
|
347
341
|
);
|
|
348
342
|
|
|
@@ -353,7 +347,7 @@ export const createPersistentActor = Effect.fn("effect-machine.persistentActor.s
|
|
|
353
347
|
const { effects: effectSlots } = typedMachine._slots;
|
|
354
348
|
|
|
355
349
|
for (const bg of typedMachine.backgroundEffects) {
|
|
356
|
-
const fiber = yield* Effect.
|
|
350
|
+
const fiber = yield* Effect.forkDaemon(
|
|
357
351
|
bg
|
|
358
352
|
.handler({ state: resolvedInitial, event: initEvent, self, effects: effectSlots })
|
|
359
353
|
.pipe(Effect.provideService(typedMachine.Context, initCtx)),
|
|
@@ -408,7 +402,7 @@ export const createPersistentActor = Effect.fn("effect-machine.persistentActor.s
|
|
|
408
402
|
}
|
|
409
403
|
|
|
410
404
|
// Start the persistent event loop
|
|
411
|
-
const loopFiber = yield* Effect.
|
|
405
|
+
const loopFiber = yield* Effect.forkDaemon(
|
|
412
406
|
persistentEventLoop(
|
|
413
407
|
id,
|
|
414
408
|
persistentMachine,
|
package/src/schema.ts
CHANGED
|
@@ -74,11 +74,30 @@ type IsEmptyFields<Fields extends Schema.Struct.Fields> = keyof Fields extends n
|
|
|
74
74
|
* Constructor functions for each variant.
|
|
75
75
|
* Empty structs: plain values with `_tag`: `State.Idle`
|
|
76
76
|
* Non-empty structs require args: `State.Loading({ url })`
|
|
77
|
+
*
|
|
78
|
+
* Each variant also has a `derive` method for constructing from a source object.
|
|
79
|
+
*/
|
|
80
|
+
/**
|
|
81
|
+
* Constructor functions for each variant.
|
|
82
|
+
* Empty structs: plain values with `_tag`: `State.Idle`
|
|
83
|
+
* Non-empty structs require args: `State.Loading({ url })`
|
|
84
|
+
*
|
|
85
|
+
* Each variant also has a `derive` method for constructing from a source object.
|
|
86
|
+
* The source type uses `object` to accept branded state types without index signature issues.
|
|
77
87
|
*/
|
|
78
88
|
type VariantConstructors<D extends Record<string, Schema.Struct.Fields>, Brand> = {
|
|
79
89
|
readonly [K in keyof D & string]: IsEmptyFields<D[K]> extends true
|
|
80
|
-
? TaggedStructType<K, D[K]> &
|
|
81
|
-
|
|
90
|
+
? TaggedStructType<K, D[K]> &
|
|
91
|
+
Brand & {
|
|
92
|
+
readonly derive: (source: object) => TaggedStructType<K, D[K]> & Brand;
|
|
93
|
+
}
|
|
94
|
+
: ((args: Schema.Struct.Constructor<D[K]>) => TaggedStructType<K, D[K]> & Brand) & {
|
|
95
|
+
readonly derive: (
|
|
96
|
+
source: object,
|
|
97
|
+
partial?: Partial<Schema.Struct.Constructor<D[K]>>,
|
|
98
|
+
) => TaggedStructType<K, D[K]> & Brand;
|
|
99
|
+
readonly _tag: K;
|
|
100
|
+
};
|
|
82
101
|
};
|
|
83
102
|
|
|
84
103
|
/**
|
|
@@ -92,6 +111,11 @@ type MatchCases<D extends Record<string, Schema.Struct.Fields>, R> = {
|
|
|
92
111
|
* Base schema interface with pattern matching helpers
|
|
93
112
|
*/
|
|
94
113
|
interface MachineSchemaBase<D extends Record<string, Schema.Struct.Fields>, Brand> {
|
|
114
|
+
/**
|
|
115
|
+
* Raw definition record for introspection
|
|
116
|
+
*/
|
|
117
|
+
readonly _definition: D;
|
|
118
|
+
|
|
95
119
|
/**
|
|
96
120
|
* Per-variant schemas for fine-grained operations
|
|
97
121
|
*/
|
|
@@ -164,6 +188,7 @@ const buildMachineSchema = <D extends Record<string, Schema.Struct.Fields>>(
|
|
|
164
188
|
schema: Schema.Schema<VariantsUnion<D>, VariantsUnion<D>, never>;
|
|
165
189
|
variants: VariantSchemas<D>;
|
|
166
190
|
constructors: Record<string, (args: Record<string, unknown>) => Record<string, unknown>>;
|
|
191
|
+
_definition: D;
|
|
167
192
|
$is: <Tag extends string>(tag: Tag) => (u: unknown) => boolean;
|
|
168
193
|
$match: (valueOrCases: unknown, maybeCases?: unknown) => unknown;
|
|
169
194
|
} => {
|
|
@@ -184,16 +209,29 @@ const buildMachineSchema = <D extends Record<string, Schema.Struct.Fields>>(
|
|
|
184
209
|
// Create constructor that builds tagged struct directly
|
|
185
210
|
// Like Data.taggedEnum, this doesn't validate at construction time
|
|
186
211
|
// Use Schema.decode for validation when needed
|
|
187
|
-
const
|
|
212
|
+
const fieldNames = new Set(Object.keys(fields));
|
|
213
|
+
const hasFields = fieldNames.size > 0;
|
|
188
214
|
|
|
189
215
|
if (hasFields) {
|
|
190
216
|
// Non-empty: constructor function requiring args
|
|
191
217
|
const constructor = (args: Record<string, unknown>) => ({ ...args, _tag: tag });
|
|
192
218
|
constructor._tag = tag;
|
|
219
|
+
constructor.derive = (source: Record<string, unknown>, partial?: Record<string, unknown>) => {
|
|
220
|
+
const result: Record<string, unknown> = { _tag: tag };
|
|
221
|
+
for (const key of fieldNames) {
|
|
222
|
+
if (key in source) result[key] = source[key];
|
|
223
|
+
}
|
|
224
|
+
if (partial !== undefined) {
|
|
225
|
+
for (const [key, value] of Object.entries(partial)) {
|
|
226
|
+
result[key] = value;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return result;
|
|
230
|
+
};
|
|
193
231
|
constructors[tag] = constructor;
|
|
194
232
|
} else {
|
|
195
233
|
// Empty: plain value, not callable
|
|
196
|
-
constructors[tag] = { _tag: tag } as never;
|
|
234
|
+
constructors[tag] = { _tag: tag, derive: () => ({ _tag: tag }) } as never;
|
|
197
235
|
}
|
|
198
236
|
}
|
|
199
237
|
|
|
@@ -244,6 +282,7 @@ const buildMachineSchema = <D extends Record<string, Schema.Struct.Fields>>(
|
|
|
244
282
|
schema: unionSchema as unknown as Schema.Schema<VariantsUnion<D>, VariantsUnion<D>, never>,
|
|
245
283
|
variants: variants as unknown as VariantSchemas<D>,
|
|
246
284
|
constructors,
|
|
285
|
+
_definition: definition,
|
|
247
286
|
$is,
|
|
248
287
|
$match,
|
|
249
288
|
};
|
|
@@ -254,8 +293,15 @@ const buildMachineSchema = <D extends Record<string, Schema.Struct.Fields>>(
|
|
|
254
293
|
* Builds the schema object with variants, constructors, $is, and $match.
|
|
255
294
|
*/
|
|
256
295
|
const createMachineSchema = <D extends Record<string, Schema.Struct.Fields>>(definition: D) => {
|
|
257
|
-
const { schema, variants, constructors, $is, $match } =
|
|
258
|
-
|
|
296
|
+
const { schema, variants, constructors, _definition, $is, $match } =
|
|
297
|
+
buildMachineSchema(definition);
|
|
298
|
+
return Object.assign(Object.create(schema), {
|
|
299
|
+
variants,
|
|
300
|
+
_definition,
|
|
301
|
+
$is,
|
|
302
|
+
$match,
|
|
303
|
+
...constructors,
|
|
304
|
+
});
|
|
259
305
|
};
|
|
260
306
|
|
|
261
307
|
/**
|
package/src/testing.ts
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { Effect, SubscriptionRef } from "effect";
|
|
2
2
|
|
|
3
3
|
import type { Machine, MachineRef } from "./machine.js";
|
|
4
|
+
import { BuiltMachine } from "./machine.js";
|
|
4
5
|
import { AssertionError } from "./errors.js";
|
|
5
6
|
import type { GuardsDef, EffectsDef } from "./slot.js";
|
|
6
7
|
import { executeTransition } from "./internal/transition.js";
|
|
7
8
|
|
|
9
|
+
/** Accept either Machine or BuiltMachine for testing utilities. */
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
|
+
type MachineInput<S, E, R, GD extends GuardsDef, EFD extends EffectsDef> =
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13
|
+
Machine<S, E, R, any, any, GD, EFD> | BuiltMachine<S, E, R>;
|
|
14
|
+
|
|
8
15
|
/**
|
|
9
16
|
* Result of simulating events through a machine
|
|
10
17
|
*/
|
|
@@ -39,11 +46,17 @@ export const simulate = Effect.fn("effect-machine.simulate")(function* <
|
|
|
39
46
|
R,
|
|
40
47
|
GD extends GuardsDef = Record<string, never>,
|
|
41
48
|
EFD extends EffectsDef = Record<string, never>,
|
|
42
|
-
>(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
49
|
+
>(input: MachineInput<S, E, R, GD, EFD>, events: ReadonlyArray<E>) {
|
|
50
|
+
const machine = (input instanceof BuiltMachine ? input._inner : input) as Machine<
|
|
51
|
+
S,
|
|
52
|
+
E,
|
|
53
|
+
R,
|
|
54
|
+
Record<string, never>,
|
|
55
|
+
Record<string, never>,
|
|
56
|
+
GD,
|
|
57
|
+
EFD
|
|
58
|
+
>;
|
|
59
|
+
|
|
47
60
|
// Create a dummy self for slot accessors
|
|
48
61
|
const dummySelf: MachineRef<E> = {
|
|
49
62
|
send: Effect.fn("effect-machine.testing.simulate.send")((_event: E) => Effect.void),
|
|
@@ -83,13 +96,8 @@ export const assertReaches = Effect.fn("effect-machine.assertReaches")(function*
|
|
|
83
96
|
R,
|
|
84
97
|
GD extends GuardsDef = Record<string, never>,
|
|
85
98
|
EFD extends EffectsDef = Record<string, never>,
|
|
86
|
-
>(
|
|
87
|
-
|
|
88
|
-
machine: Machine<S, E, R, any, any, GD, EFD>,
|
|
89
|
-
events: ReadonlyArray<E>,
|
|
90
|
-
expectedTag: string,
|
|
91
|
-
) {
|
|
92
|
-
const result = yield* simulate(machine, events);
|
|
99
|
+
>(input: MachineInput<S, E, R, GD, EFD>, events: ReadonlyArray<E>, expectedTag: string) {
|
|
100
|
+
const result = yield* simulate(input, events);
|
|
93
101
|
if (result.finalState._tag !== expectedTag) {
|
|
94
102
|
return yield* new AssertionError({
|
|
95
103
|
message:
|
|
@@ -119,12 +127,11 @@ export const assertPath = Effect.fn("effect-machine.assertPath")(function* <
|
|
|
119
127
|
GD extends GuardsDef = Record<string, never>,
|
|
120
128
|
EFD extends EffectsDef = Record<string, never>,
|
|
121
129
|
>(
|
|
122
|
-
|
|
123
|
-
machine: Machine<S, E, R, any, any, GD, EFD>,
|
|
130
|
+
input: MachineInput<S, E, R, GD, EFD>,
|
|
124
131
|
events: ReadonlyArray<E>,
|
|
125
132
|
expectedPath: ReadonlyArray<string>,
|
|
126
133
|
) {
|
|
127
|
-
const result = yield* simulate(
|
|
134
|
+
const result = yield* simulate(input, events);
|
|
128
135
|
const actualPath = result.states.map((s) => s._tag);
|
|
129
136
|
|
|
130
137
|
if (actualPath.length !== expectedPath.length) {
|
|
@@ -169,13 +176,8 @@ export const assertNeverReaches = Effect.fn("effect-machine.assertNeverReaches")
|
|
|
169
176
|
R,
|
|
170
177
|
GD extends GuardsDef = Record<string, never>,
|
|
171
178
|
EFD extends EffectsDef = Record<string, never>,
|
|
172
|
-
>(
|
|
173
|
-
|
|
174
|
-
machine: Machine<S, E, R, any, any, GD, EFD>,
|
|
175
|
-
events: ReadonlyArray<E>,
|
|
176
|
-
forbiddenTag: string,
|
|
177
|
-
) {
|
|
178
|
-
const result = yield* simulate(machine, events);
|
|
179
|
+
>(input: MachineInput<S, E, R, GD, EFD>, events: ReadonlyArray<E>, forbiddenTag: string) {
|
|
180
|
+
const result = yield* simulate(input, events);
|
|
179
181
|
|
|
180
182
|
const visitedIndex = result.states.findIndex((s) => s._tag === forbiddenTag);
|
|
181
183
|
if (visitedIndex !== -1) {
|
|
@@ -236,11 +238,17 @@ export const createTestHarness = Effect.fn("effect-machine.createTestHarness")(f
|
|
|
236
238
|
R,
|
|
237
239
|
GD extends GuardsDef = Record<string, never>,
|
|
238
240
|
EFD extends EffectsDef = Record<string, never>,
|
|
239
|
-
>(
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
241
|
+
>(input: MachineInput<S, E, R, GD, EFD>, options?: TestHarnessOptions<S, E>) {
|
|
242
|
+
const machine = (input instanceof BuiltMachine ? input._inner : input) as Machine<
|
|
243
|
+
S,
|
|
244
|
+
E,
|
|
245
|
+
R,
|
|
246
|
+
Record<string, never>,
|
|
247
|
+
Record<string, never>,
|
|
248
|
+
GD,
|
|
249
|
+
EFD
|
|
250
|
+
>;
|
|
251
|
+
|
|
244
252
|
// Create a dummy self for slot accessors
|
|
245
253
|
const dummySelf: MachineRef<E> = {
|
|
246
254
|
send: Effect.fn("effect-machine.testing.harness.send")((_event: E) => Effect.void),
|