effect-machine 0.1.0 → 0.2.1
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 +5 -4
- package/src/persistence/adapters/in-memory.ts +201 -182
- package/src/persistence/persistent-actor.ts +582 -386
- package/src/testing.ts +92 -98
- package/tsconfig.json +2 -5
package/src/machine.ts
CHANGED
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
* @module
|
|
44
44
|
*/
|
|
45
45
|
import type { Schema, Schedule, Scope, Context } from "effect";
|
|
46
|
-
import { Effect } from "effect";
|
|
46
|
+
import { Cause, Effect, Exit } from "effect";
|
|
47
47
|
import type { Pipeable } from "effect/Pipeable";
|
|
48
48
|
import { pipeArguments } from "effect/Pipeable";
|
|
49
49
|
|
|
@@ -54,6 +54,8 @@ import type { MachineStateSchema, MachineEventSchema, VariantsUnion } from "./sc
|
|
|
54
54
|
import type { PersistentMachine, WithPersistenceConfig } from "./persistence/persistent-machine.js";
|
|
55
55
|
import { persist as persistImpl } from "./persistence/persistent-machine.js";
|
|
56
56
|
import { SlotProvisionError, ProvisionValidationError } from "./errors.js";
|
|
57
|
+
import type { UnprovidedSlotsError } from "./errors.js";
|
|
58
|
+
import { invalidateIndex } from "./internal/transition.js";
|
|
57
59
|
import type {
|
|
58
60
|
GuardsSchema,
|
|
59
61
|
EffectsSchema,
|
|
@@ -248,6 +250,10 @@ export class Machine<
|
|
|
248
250
|
string,
|
|
249
251
|
(params: unknown, ctx: SlotContext<State, Event>) => Effect.Effect<void, never, R>
|
|
250
252
|
>;
|
|
253
|
+
/** @internal */ readonly _slots: {
|
|
254
|
+
guards: GuardSlots<GD>;
|
|
255
|
+
effects: EffectSlots<EFD>;
|
|
256
|
+
};
|
|
251
257
|
readonly stateSchema?: Schema.Schema<State, unknown, never>;
|
|
252
258
|
readonly eventSchema?: Schema.Schema<Event, unknown, never>;
|
|
253
259
|
|
|
@@ -302,6 +308,36 @@ export class Machine<
|
|
|
302
308
|
this._effectHandlers = new Map();
|
|
303
309
|
this.stateSchema = stateSchema;
|
|
304
310
|
this.eventSchema = eventSchema;
|
|
311
|
+
|
|
312
|
+
const guardSlots =
|
|
313
|
+
this._guardsSchema !== undefined
|
|
314
|
+
? this._guardsSchema._createSlots((name: string, params: unknown) =>
|
|
315
|
+
Effect.flatMap(Effect.serviceOptional(this.Context).pipe(Effect.orDie), (ctx) => {
|
|
316
|
+
const handler = this._guardHandlers.get(name);
|
|
317
|
+
if (handler === undefined) {
|
|
318
|
+
return Effect.die(new SlotProvisionError({ slotName: name, slotType: "guard" }));
|
|
319
|
+
}
|
|
320
|
+
const result = handler(params, ctx);
|
|
321
|
+
const normalized = typeof result === "boolean" ? Effect.succeed(result) : result;
|
|
322
|
+
return normalized as Effect.Effect<boolean, never, never>;
|
|
323
|
+
}),
|
|
324
|
+
)
|
|
325
|
+
: ({} as GuardSlots<GD>);
|
|
326
|
+
|
|
327
|
+
const effectSlots =
|
|
328
|
+
this._effectsSchema !== undefined
|
|
329
|
+
? this._effectsSchema._createSlots((name: string, params: unknown) =>
|
|
330
|
+
Effect.flatMap(Effect.serviceOptional(this.Context).pipe(Effect.orDie), (ctx) => {
|
|
331
|
+
const handler = this._effectHandlers.get(name);
|
|
332
|
+
if (handler === undefined) {
|
|
333
|
+
return Effect.die(new SlotProvisionError({ slotName: name, slotType: "effect" }));
|
|
334
|
+
}
|
|
335
|
+
return handler(params, ctx) as Effect.Effect<void, never, never>;
|
|
336
|
+
}),
|
|
337
|
+
)
|
|
338
|
+
: ({} as EffectSlots<EFD>);
|
|
339
|
+
|
|
340
|
+
this._slots = { guards: guardSlots, effects: effectSlots };
|
|
305
341
|
}
|
|
306
342
|
|
|
307
343
|
pipe() {
|
|
@@ -360,6 +396,7 @@ export class Machine<
|
|
|
360
396
|
|
|
361
397
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
362
398
|
(this._transitions as any[]).push(transition);
|
|
399
|
+
invalidateIndex(this);
|
|
363
400
|
|
|
364
401
|
return this;
|
|
365
402
|
}
|
|
@@ -398,9 +435,63 @@ export class Machine<
|
|
|
398
435
|
stateTag,
|
|
399
436
|
handler: handler as unknown as SpawnEffect<State, Event, EFD, R>["handler"],
|
|
400
437
|
});
|
|
438
|
+
invalidateIndex(this);
|
|
401
439
|
return this;
|
|
402
440
|
}
|
|
403
441
|
|
|
442
|
+
// ---- task ----
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* State-scoped task that runs on entry and sends success/failure events.
|
|
446
|
+
* Interrupts do not emit failure events.
|
|
447
|
+
*/
|
|
448
|
+
task<
|
|
449
|
+
NS extends VariantsUnion<_SD> & BrandedState,
|
|
450
|
+
A,
|
|
451
|
+
E1,
|
|
452
|
+
ES extends VariantsUnion<_ED> & BrandedEvent,
|
|
453
|
+
EF extends VariantsUnion<_ED> & BrandedEvent,
|
|
454
|
+
>(
|
|
455
|
+
state: TaggedOrConstructor<NS>,
|
|
456
|
+
run: (
|
|
457
|
+
ctx: StateHandlerContext<NS, VariantsUnion<_ED> & BrandedEvent, EFD>,
|
|
458
|
+
) => Effect.Effect<A, E1, Scope.Scope>,
|
|
459
|
+
options: {
|
|
460
|
+
readonly onSuccess: (
|
|
461
|
+
value: A,
|
|
462
|
+
ctx: StateHandlerContext<NS, VariantsUnion<_ED> & BrandedEvent, EFD>,
|
|
463
|
+
) => ES;
|
|
464
|
+
readonly onFailure?: (
|
|
465
|
+
cause: Cause.Cause<E1>,
|
|
466
|
+
ctx: StateHandlerContext<NS, VariantsUnion<_ED> & BrandedEvent, EFD>,
|
|
467
|
+
) => EF;
|
|
468
|
+
},
|
|
469
|
+
): Machine<State, Event, R, _SD, _ED, GD, EFD> {
|
|
470
|
+
const handler = Effect.fn("effect-machine.task")(function* (
|
|
471
|
+
ctx: StateHandlerContext<NS, VariantsUnion<_ED> & BrandedEvent, EFD>,
|
|
472
|
+
) {
|
|
473
|
+
const exit = yield* Effect.exit(run(ctx));
|
|
474
|
+
if (Exit.isSuccess(exit)) {
|
|
475
|
+
yield* ctx.self.send(options.onSuccess(exit.value, ctx));
|
|
476
|
+
yield* Effect.yieldNow();
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const cause = exit.cause;
|
|
481
|
+
if (Cause.isInterruptedOnly(cause)) {
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
if (options.onFailure !== undefined) {
|
|
485
|
+
yield* ctx.self.send(options.onFailure(cause, ctx));
|
|
486
|
+
yield* Effect.yieldNow();
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
return yield* Effect.failCause(cause).pipe(Effect.orDie);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
return this.spawn(state, handler);
|
|
493
|
+
}
|
|
494
|
+
|
|
404
495
|
// ---- background ----
|
|
405
496
|
|
|
406
497
|
/**
|
|
@@ -495,15 +586,15 @@ export class Machine<
|
|
|
495
586
|
this._effectsSchema,
|
|
496
587
|
);
|
|
497
588
|
|
|
498
|
-
//
|
|
589
|
+
// Copy arrays/sets to avoid mutation bleed
|
|
499
590
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
500
|
-
(result as any)._transitions = this._transitions;
|
|
591
|
+
(result as any)._transitions = [...this._transitions];
|
|
501
592
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
502
|
-
(result as any)._finalStates = this._finalStates;
|
|
593
|
+
(result as any)._finalStates = new Set(this._finalStates);
|
|
503
594
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
504
|
-
(result as any)._spawnEffects = this._spawnEffects;
|
|
595
|
+
(result as any)._spawnEffects = [...this._spawnEffects];
|
|
505
596
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
506
|
-
(result as any)._backgroundEffects = this._backgroundEffects;
|
|
597
|
+
(result as any)._backgroundEffects = [...this._backgroundEffects];
|
|
507
598
|
|
|
508
599
|
// Register handlers from provided object
|
|
509
600
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -537,50 +628,22 @@ export class Machine<
|
|
|
537
628
|
}
|
|
538
629
|
|
|
539
630
|
/**
|
|
540
|
-
*
|
|
541
|
-
* @internal Used by
|
|
631
|
+
* Missing slot handlers (guards + effects).
|
|
632
|
+
* @internal Used by actor creation to fail fast.
|
|
542
633
|
*/
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
return Effect.die(new SlotProvisionError({ slotName: name, slotType: "guard" }));
|
|
557
|
-
}
|
|
558
|
-
const result = handler(params, ctx);
|
|
559
|
-
// Handler may return boolean or Effect<boolean>
|
|
560
|
-
const normalized = typeof result === "boolean" ? Effect.succeed(result) : result;
|
|
561
|
-
return normalized.pipe(Effect.provideService(machine.Context, ctx)) as Effect.Effect<
|
|
562
|
-
boolean,
|
|
563
|
-
never,
|
|
564
|
-
never
|
|
565
|
-
>;
|
|
566
|
-
})
|
|
567
|
-
: ({} as GuardSlots<GD>);
|
|
568
|
-
|
|
569
|
-
// Create effect slots that resolve to actual handlers
|
|
570
|
-
const effects =
|
|
571
|
-
this._effectsSchema !== undefined
|
|
572
|
-
? this._effectsSchema._createSlots((name: string, params: unknown) => {
|
|
573
|
-
const handler = machine._effectHandlers.get(name);
|
|
574
|
-
if (handler === undefined) {
|
|
575
|
-
return Effect.die(new SlotProvisionError({ slotName: name, slotType: "effect" }));
|
|
576
|
-
}
|
|
577
|
-
return handler(params, ctx).pipe(
|
|
578
|
-
Effect.provideService(machine.Context, ctx),
|
|
579
|
-
) as Effect.Effect<void, never, never>;
|
|
580
|
-
})
|
|
581
|
-
: ({} as EffectSlots<EFD>);
|
|
582
|
-
|
|
583
|
-
return { guards, effects };
|
|
634
|
+
_missingSlots(): string[] {
|
|
635
|
+
const missing: string[] = [];
|
|
636
|
+
if (this._guardsSchema !== undefined) {
|
|
637
|
+
for (const name of Object.keys(this._guardsSchema.definitions)) {
|
|
638
|
+
if (!this._guardHandlers.has(name)) missing.push(name);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
if (this._effectsSchema !== undefined) {
|
|
642
|
+
for (const name of Object.keys(this._effectsSchema.definitions)) {
|
|
643
|
+
if (!this._effectHandlers.has(name)) missing.push(name);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
return missing;
|
|
584
647
|
}
|
|
585
648
|
|
|
586
649
|
// ---- Static factory ----
|
|
@@ -634,6 +697,30 @@ import { createActor } from "./actor.js";
|
|
|
634
697
|
* Effect.runPromise(Effect.scoped(program));
|
|
635
698
|
* ```
|
|
636
699
|
*/
|
|
700
|
+
const spawnImpl = Effect.fn("effect-machine.spawn")(function* <
|
|
701
|
+
S extends { readonly _tag: string },
|
|
702
|
+
E extends { readonly _tag: string },
|
|
703
|
+
R,
|
|
704
|
+
GD extends GuardsDef,
|
|
705
|
+
EFD extends EffectsDef,
|
|
706
|
+
>(
|
|
707
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Schema fields need wide acceptance
|
|
708
|
+
machine: Machine<S, E, R, any, any, GD, EFD>,
|
|
709
|
+
id?: string,
|
|
710
|
+
) {
|
|
711
|
+
const actorId = id ?? `actor-${Math.random().toString(36).slice(2)}`;
|
|
712
|
+
const actor = yield* createActor(actorId, machine);
|
|
713
|
+
|
|
714
|
+
// Register cleanup on scope finalization
|
|
715
|
+
yield* Effect.addFinalizer(
|
|
716
|
+
Effect.fn("effect-machine.spawn.finalizer")(function* () {
|
|
717
|
+
yield* actor.stop;
|
|
718
|
+
}),
|
|
719
|
+
);
|
|
720
|
+
|
|
721
|
+
return actor;
|
|
722
|
+
});
|
|
723
|
+
|
|
637
724
|
export const spawn: {
|
|
638
725
|
<
|
|
639
726
|
S extends { readonly _tag: string },
|
|
@@ -644,7 +731,7 @@ export const spawn: {
|
|
|
644
731
|
>(
|
|
645
732
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Schema fields need wide acceptance
|
|
646
733
|
machine: Machine<S, E, R, any, any, GD, EFD>,
|
|
647
|
-
): Effect.Effect<ActorRef<S, E>,
|
|
734
|
+
): Effect.Effect<ActorRef<S, E>, UnprovidedSlotsError, R | Scope.Scope>;
|
|
648
735
|
|
|
649
736
|
<
|
|
650
737
|
S extends { readonly _tag: string },
|
|
@@ -656,27 +743,8 @@ export const spawn: {
|
|
|
656
743
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Schema fields need wide acceptance
|
|
657
744
|
machine: Machine<S, E, R, any, any, GD, EFD>,
|
|
658
745
|
id: string,
|
|
659
|
-
): Effect.Effect<ActorRef<S, E>,
|
|
660
|
-
} =
|
|
661
|
-
S extends { readonly _tag: string },
|
|
662
|
-
E extends { readonly _tag: string },
|
|
663
|
-
R,
|
|
664
|
-
GD extends GuardsDef,
|
|
665
|
-
EFD extends EffectsDef,
|
|
666
|
-
>(
|
|
667
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Schema fields need wide acceptance
|
|
668
|
-
machine: Machine<S, E, R, any, any, GD, EFD>,
|
|
669
|
-
id?: string,
|
|
670
|
-
): Effect.Effect<ActorRef<S, E>, never, R | Scope.Scope> =>
|
|
671
|
-
Effect.gen(function* () {
|
|
672
|
-
const actorId = id ?? `actor-${Math.random().toString(36).slice(2)}`;
|
|
673
|
-
const actor = yield* createActor(actorId, machine);
|
|
674
|
-
|
|
675
|
-
// Register cleanup on scope finalization
|
|
676
|
-
yield* Effect.addFinalizer(() => actor.stop);
|
|
677
|
-
|
|
678
|
-
return actor;
|
|
679
|
-
});
|
|
746
|
+
): Effect.Effect<ActorRef<S, E>, UnprovidedSlotsError, R | Scope.Scope>;
|
|
747
|
+
} = spawnImpl;
|
|
680
748
|
|
|
681
749
|
// Transition lookup (introspection)
|
|
682
750
|
export { findTransitions } from "./internal/transition.js";
|
|
@@ -2,7 +2,7 @@ import { Context, Schema } from "effect";
|
|
|
2
2
|
import type { Effect, Option } from "effect";
|
|
3
3
|
|
|
4
4
|
import type { PersistentActorRef } from "./persistent-actor.js";
|
|
5
|
-
import type { DuplicateActorError } from "../errors.js";
|
|
5
|
+
import type { DuplicateActorError, UnprovidedSlotsError } from "../errors.js";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Metadata for a persisted actor.
|
|
@@ -26,8 +26,9 @@ export interface ActorMetadata {
|
|
|
26
26
|
export interface RestoreResult<
|
|
27
27
|
S extends { readonly _tag: string },
|
|
28
28
|
E extends { readonly _tag: string },
|
|
29
|
+
R = never,
|
|
29
30
|
> {
|
|
30
|
-
readonly restored: ReadonlyArray<PersistentActorRef<S, E>>;
|
|
31
|
+
readonly restored: ReadonlyArray<PersistentActorRef<S, E, R>>;
|
|
31
32
|
readonly failed: ReadonlyArray<RestoreFailure>;
|
|
32
33
|
}
|
|
33
34
|
|
|
@@ -36,7 +37,7 @@ export interface RestoreResult<
|
|
|
36
37
|
*/
|
|
37
38
|
export interface RestoreFailure {
|
|
38
39
|
readonly id: string;
|
|
39
|
-
readonly error: PersistenceError | DuplicateActorError;
|
|
40
|
+
readonly error: PersistenceError | DuplicateActorError | UnprovidedSlotsError;
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
/**
|
|
@@ -165,5 +166,5 @@ export class VersionConflictError extends Schema.TaggedError<VersionConflictErro
|
|
|
165
166
|
* PersistenceAdapter service tag
|
|
166
167
|
*/
|
|
167
168
|
export class PersistenceAdapterTag extends Context.Tag(
|
|
168
|
-
"effect-machine/persistence/adapter/PersistenceAdapterTag",
|
|
169
|
+
"effect-machine/src/persistence/adapter/PersistenceAdapterTag",
|
|
169
170
|
)<PersistenceAdapterTag, PersistenceAdapter>() {}
|