effect-machine 0.2.4 → 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/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
package/src/errors.ts
CHANGED
|
@@ -49,7 +49,7 @@ export class SlotProvisionError extends Schema.TaggedError<SlotProvisionError>()
|
|
|
49
49
|
},
|
|
50
50
|
) {}
|
|
51
51
|
|
|
52
|
-
/** Machine.
|
|
52
|
+
/** Machine.build() validation failed - missing or extra handlers */
|
|
53
53
|
export class ProvisionValidationError extends Schema.TaggedError<ProvisionValidationError>()(
|
|
54
54
|
"ProvisionValidationError",
|
|
55
55
|
{
|
package/src/index.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { Cause, Effect, Exit, Scope } from "effect";
|
|
12
12
|
|
|
13
13
|
import type { Machine, MachineRef, Transition, SpawnEffect, HandlerContext } from "../machine.js";
|
|
14
|
+
import { BuiltMachine } from "../machine.js";
|
|
14
15
|
import type { GuardsDef, EffectsDef, MachineContext } from "../slot.js";
|
|
15
16
|
import { isEffect, INTERNAL_ENTER_EVENT } from "./utils.js";
|
|
16
17
|
|
|
@@ -443,6 +444,7 @@ const getIndex = <
|
|
|
443
444
|
* Find all transitions matching a state/event pair.
|
|
444
445
|
* Returns empty array if no matches.
|
|
445
446
|
*
|
|
447
|
+
* Accepts both `Machine` and `BuiltMachine`.
|
|
446
448
|
* O(1) lookup after first access (index is lazily built).
|
|
447
449
|
*/
|
|
448
450
|
export const findTransitions = <
|
|
@@ -453,12 +455,16 @@ export const findTransitions = <
|
|
|
453
455
|
EFD extends EffectsDef = Record<string, never>,
|
|
454
456
|
>(
|
|
455
457
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Schema fields need wide acceptance
|
|
456
|
-
|
|
458
|
+
input: Machine<S, E, R, any, any, GD, EFD> | BuiltMachine<S, E, R>,
|
|
457
459
|
stateTag: string,
|
|
458
460
|
eventTag: string,
|
|
459
461
|
): ReadonlyArray<Transition<S, E, GD, EFD, R>> => {
|
|
462
|
+
const machine = input instanceof BuiltMachine ? input._inner : input;
|
|
460
463
|
const index = getIndex(machine);
|
|
461
|
-
|
|
464
|
+
const specific = index.transitions.get(stateTag)?.get(eventTag) ?? [];
|
|
465
|
+
if (specific.length > 0) return specific;
|
|
466
|
+
// Fallback to wildcard transitions
|
|
467
|
+
return index.transitions.get("*")?.get(eventTag) ?? [];
|
|
462
468
|
};
|
|
463
469
|
|
|
464
470
|
/**
|
package/src/machine.ts
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
* )
|
|
35
35
|
* .on(MyState.Running, MyEvent.Stop, () => MyState.Idle)
|
|
36
36
|
* .final(MyState.Idle)
|
|
37
|
-
* .
|
|
37
|
+
* .build({
|
|
38
38
|
* canStart: ({ threshold }) => Effect.succeed(threshold > 0),
|
|
39
39
|
* notify: ({ message }) => Effect.log(message),
|
|
40
40
|
* })
|
|
@@ -42,10 +42,8 @@
|
|
|
42
42
|
*
|
|
43
43
|
* @module
|
|
44
44
|
*/
|
|
45
|
-
import type { Schema, Schedule,
|
|
46
|
-
import { Cause, Effect, Exit } from "effect";
|
|
47
|
-
import type { Pipeable } from "effect/Pipeable";
|
|
48
|
-
import { pipeArguments } from "effect/Pipeable";
|
|
45
|
+
import type { Schema, Schedule, Context } from "effect";
|
|
46
|
+
import { Cause, Effect, Exit, Option, Scope } from "effect";
|
|
49
47
|
|
|
50
48
|
import type { TransitionResult } from "./internal/utils.js";
|
|
51
49
|
import { getTag } from "./internal/utils.js";
|
|
@@ -54,7 +52,6 @@ import type { MachineStateSchema, MachineEventSchema, VariantsUnion } from "./sc
|
|
|
54
52
|
import type { PersistentMachine, WithPersistenceConfig } from "./persistence/persistent-machine.js";
|
|
55
53
|
import { persist as persistImpl } from "./persistence/persistent-machine.js";
|
|
56
54
|
import { SlotProvisionError, ProvisionValidationError } from "./errors.js";
|
|
57
|
-
import type { UnprovidedSlotsError } from "./errors.js";
|
|
58
55
|
import { invalidateIndex } from "./internal/transition.js";
|
|
59
56
|
import type {
|
|
60
57
|
GuardsSchema,
|
|
@@ -198,7 +195,7 @@ type HasEffectKeys<EFD extends EffectsDef> = [keyof EFD] extends [never]
|
|
|
198
195
|
/** Context type passed to guard/effect handlers */
|
|
199
196
|
export type SlotContext<State, Event> = MachineContext<State, Event, MachineRef<Event>>;
|
|
200
197
|
|
|
201
|
-
/** Combined handlers for
|
|
198
|
+
/** Combined handlers for build() - guards and effects only */
|
|
202
199
|
export type ProvideHandlers<
|
|
203
200
|
State,
|
|
204
201
|
Event,
|
|
@@ -210,6 +207,43 @@ export type ProvideHandlers<
|
|
|
210
207
|
? SlotEffectHandlers<EFD, SlotContext<State, Event>, R>
|
|
211
208
|
: object);
|
|
212
209
|
|
|
210
|
+
/** Whether the machine has any guard or effect slots */
|
|
211
|
+
type HasSlots<GD extends GuardsDef, EFD extends EffectsDef> =
|
|
212
|
+
HasGuardKeys<GD> extends true ? true : HasEffectKeys<EFD>;
|
|
213
|
+
|
|
214
|
+
// ============================================================================
|
|
215
|
+
// BuiltMachine
|
|
216
|
+
// ============================================================================
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* A finalized machine ready for spawning.
|
|
220
|
+
*
|
|
221
|
+
* Created by calling `.build()` on a `Machine`. This is the only type
|
|
222
|
+
* accepted by `Machine.spawn` and `ActorSystem.spawn` (regular overload).
|
|
223
|
+
* Testing utilities (`simulate`, `createTestHarness`, etc.) still accept `Machine`.
|
|
224
|
+
*/
|
|
225
|
+
export class BuiltMachine<State, Event, R = never> {
|
|
226
|
+
/** @internal */
|
|
227
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
228
|
+
readonly _inner: Machine<State, Event, R, any, any, any, any>;
|
|
229
|
+
|
|
230
|
+
/** @internal */
|
|
231
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
232
|
+
constructor(machine: Machine<State, Event, R, any, any, any, any>) {
|
|
233
|
+
this._inner = machine;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
get initial(): State {
|
|
237
|
+
return this._inner.initial;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
persist(
|
|
241
|
+
config: PersistOptions,
|
|
242
|
+
): PersistentMachine<State & { readonly _tag: string }, Event & { readonly _tag: string }, R> {
|
|
243
|
+
return this._inner.persist(config);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
213
247
|
// ============================================================================
|
|
214
248
|
// Machine class
|
|
215
249
|
// ============================================================================
|
|
@@ -234,7 +268,7 @@ export class Machine<
|
|
|
234
268
|
_ED extends Record<string, Schema.Struct.Fields> = Record<string, Schema.Struct.Fields>,
|
|
235
269
|
GD extends GuardsDef = Record<string, never>,
|
|
236
270
|
EFD extends EffectsDef = Record<string, never>,
|
|
237
|
-
>
|
|
271
|
+
> {
|
|
238
272
|
readonly initial: State;
|
|
239
273
|
/** @internal */ readonly _transitions: Array<Transition<State, Event, GD, EFD, R>>;
|
|
240
274
|
/** @internal */ readonly _spawnEffects: Array<SpawnEffect<State, Event, EFD, R>>;
|
|
@@ -340,13 +374,9 @@ export class Machine<
|
|
|
340
374
|
this._slots = { guards: guardSlots, effects: effectSlots };
|
|
341
375
|
}
|
|
342
376
|
|
|
343
|
-
pipe() {
|
|
344
|
-
// eslint-disable-next-line prefer-rest-params
|
|
345
|
-
return pipeArguments(this, arguments);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
377
|
// ---- on ----
|
|
349
378
|
|
|
379
|
+
/** Register transition for a single state */
|
|
350
380
|
on<
|
|
351
381
|
NS extends VariantsUnion<_SD> & BrandedState,
|
|
352
382
|
NE extends VariantsUnion<_ED> & BrandedEvent,
|
|
@@ -355,8 +385,31 @@ export class Machine<
|
|
|
355
385
|
state: TaggedOrConstructor<NS>,
|
|
356
386
|
event: TaggedOrConstructor<NE>,
|
|
357
387
|
handler: TransitionHandler<NS, NE, RS, GD, EFD, never>,
|
|
358
|
-
): Machine<State, Event, R, _SD, _ED, GD, EFD
|
|
359
|
-
|
|
388
|
+
): Machine<State, Event, R, _SD, _ED, GD, EFD>;
|
|
389
|
+
/** Register transition for multiple states (handler receives union of state types) */
|
|
390
|
+
on<
|
|
391
|
+
NS extends ReadonlyArray<TaggedOrConstructor<VariantsUnion<_SD> & BrandedState>>,
|
|
392
|
+
NE extends VariantsUnion<_ED> & BrandedEvent,
|
|
393
|
+
RS extends VariantsUnion<_SD> & BrandedState,
|
|
394
|
+
>(
|
|
395
|
+
states: NS,
|
|
396
|
+
event: TaggedOrConstructor<NE>,
|
|
397
|
+
handler: TransitionHandler<
|
|
398
|
+
NS[number] extends TaggedOrConstructor<infer S> ? S : never,
|
|
399
|
+
NE,
|
|
400
|
+
RS,
|
|
401
|
+
GD,
|
|
402
|
+
EFD,
|
|
403
|
+
never
|
|
404
|
+
>,
|
|
405
|
+
): Machine<State, Event, R, _SD, _ED, GD, EFD>;
|
|
406
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
407
|
+
on(stateOrStates: any, event: any, handler: any): Machine<State, Event, R, _SD, _ED, GD, EFD> {
|
|
408
|
+
const states = Array.isArray(stateOrStates) ? stateOrStates : [stateOrStates];
|
|
409
|
+
for (const s of states) {
|
|
410
|
+
this.addTransition(s, event, handler, false);
|
|
411
|
+
}
|
|
412
|
+
return this;
|
|
360
413
|
}
|
|
361
414
|
|
|
362
415
|
// ---- reenter ----
|
|
@@ -365,6 +418,7 @@ export class Machine<
|
|
|
365
418
|
* Like `on()`, but forces onEnter/spawn to run even when transitioning to the same state tag.
|
|
366
419
|
* Use this to restart timers, re-run spawned effects, or reset state-scoped effects.
|
|
367
420
|
*/
|
|
421
|
+
/** Single state */
|
|
368
422
|
reenter<
|
|
369
423
|
NS extends VariantsUnion<_SD> & BrandedState,
|
|
370
424
|
NE extends VariantsUnion<_ED> & BrandedEvent,
|
|
@@ -373,8 +427,59 @@ export class Machine<
|
|
|
373
427
|
state: TaggedOrConstructor<NS>,
|
|
374
428
|
event: TaggedOrConstructor<NE>,
|
|
375
429
|
handler: TransitionHandler<NS, NE, RS, GD, EFD, never>,
|
|
430
|
+
): Machine<State, Event, R, _SD, _ED, GD, EFD>;
|
|
431
|
+
/** Multiple states */
|
|
432
|
+
reenter<
|
|
433
|
+
NS extends ReadonlyArray<TaggedOrConstructor<VariantsUnion<_SD> & BrandedState>>,
|
|
434
|
+
NE extends VariantsUnion<_ED> & BrandedEvent,
|
|
435
|
+
RS extends VariantsUnion<_SD> & BrandedState,
|
|
436
|
+
>(
|
|
437
|
+
states: NS,
|
|
438
|
+
event: TaggedOrConstructor<NE>,
|
|
439
|
+
handler: TransitionHandler<
|
|
440
|
+
NS[number] extends TaggedOrConstructor<infer S> ? S : never,
|
|
441
|
+
NE,
|
|
442
|
+
RS,
|
|
443
|
+
GD,
|
|
444
|
+
EFD,
|
|
445
|
+
never
|
|
446
|
+
>,
|
|
447
|
+
): Machine<State, Event, R, _SD, _ED, GD, EFD>;
|
|
448
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
449
|
+
reenter(
|
|
450
|
+
stateOrStates: any,
|
|
451
|
+
event: any,
|
|
452
|
+
handler: any,
|
|
453
|
+
): Machine<State, Event, R, _SD, _ED, GD, EFD> {
|
|
454
|
+
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
455
|
+
const states = Array.isArray(stateOrStates) ? stateOrStates : [stateOrStates];
|
|
456
|
+
for (const s of states) {
|
|
457
|
+
this.addTransition(s, event, handler, true);
|
|
458
|
+
}
|
|
459
|
+
return this;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// ---- onAny ----
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Register a wildcard transition that fires from any state when no specific transition matches.
|
|
466
|
+
* Specific `.on()` transitions always take priority over `.onAny()`.
|
|
467
|
+
*/
|
|
468
|
+
onAny<NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(
|
|
469
|
+
event: TaggedOrConstructor<NE>,
|
|
470
|
+
handler: TransitionHandler<VariantsUnion<_SD> & BrandedState, NE, RS, GD, EFD, never>,
|
|
376
471
|
): Machine<State, Event, R, _SD, _ED, GD, EFD> {
|
|
377
|
-
|
|
472
|
+
const eventTag = getTag(event);
|
|
473
|
+
const transition: Transition<State, Event, GD, EFD, R> = {
|
|
474
|
+
stateTag: "*",
|
|
475
|
+
eventTag,
|
|
476
|
+
handler: handler as unknown as Transition<State, Event, GD, EFD, R>["handler"],
|
|
477
|
+
reenter: false,
|
|
478
|
+
};
|
|
479
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
480
|
+
(this._transitions as any[]).push(transition);
|
|
481
|
+
invalidateIndex(this);
|
|
482
|
+
return this;
|
|
378
483
|
}
|
|
379
484
|
|
|
380
485
|
/** @internal */
|
|
@@ -415,7 +520,7 @@ export class Machine<
|
|
|
415
520
|
*
|
|
416
521
|
* machine
|
|
417
522
|
* .spawn(State.Loading, ({ effects, state }) => effects.fetchData({ url: state.url }))
|
|
418
|
-
* .
|
|
523
|
+
* .build({
|
|
419
524
|
* fetchData: ({ url }, { self }) =>
|
|
420
525
|
* Effect.gen(function* () {
|
|
421
526
|
* yield* Effect.addFinalizer(() => Effect.log("Leaving Loading"));
|
|
@@ -506,7 +611,7 @@ export class Machine<
|
|
|
506
611
|
*
|
|
507
612
|
* machine
|
|
508
613
|
* .background(({ effects }) => effects.heartbeat())
|
|
509
|
-
* .
|
|
614
|
+
* .build({
|
|
510
615
|
* heartbeat: (_, { self }) =>
|
|
511
616
|
* Effect.forever(
|
|
512
617
|
* Effect.sleep("30 seconds").pipe(Effect.andThen(self.send(Event.Ping)))
|
|
@@ -534,87 +639,97 @@ export class Machine<
|
|
|
534
639
|
return this;
|
|
535
640
|
}
|
|
536
641
|
|
|
537
|
-
// ----
|
|
642
|
+
// ---- build ----
|
|
538
643
|
|
|
539
644
|
/**
|
|
540
|
-
*
|
|
541
|
-
*
|
|
645
|
+
* Finalize the machine. Returns a `BuiltMachine` — the only type accepted by `Machine.spawn`.
|
|
646
|
+
*
|
|
647
|
+
* - Machines with slots: pass implementations as the first argument.
|
|
648
|
+
* - Machines without slots: call with no arguments.
|
|
542
649
|
*/
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
650
|
+
build<R2 = never>(
|
|
651
|
+
...args: HasSlots<GD, EFD> extends true
|
|
652
|
+
? [handlers: ProvideHandlers<State, Event, GD, EFD, R2>]
|
|
653
|
+
: [handlers?: ProvideHandlers<State, Event, GD, EFD, R2>]
|
|
654
|
+
): BuiltMachine<State, Event, R | NormalizeR<R2>> {
|
|
655
|
+
const handlers = args[0];
|
|
656
|
+
if (handlers !== undefined) {
|
|
657
|
+
// Collect all required slot names in a single pass
|
|
658
|
+
const requiredSlots = new Set<string>();
|
|
659
|
+
if (this._guardsSchema !== undefined) {
|
|
660
|
+
for (const name of Object.keys(this._guardsSchema.definitions)) {
|
|
661
|
+
requiredSlots.add(name);
|
|
662
|
+
}
|
|
551
663
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
664
|
+
if (this._effectsSchema !== undefined) {
|
|
665
|
+
for (const name of Object.keys(this._effectsSchema.definitions)) {
|
|
666
|
+
requiredSlots.add(name);
|
|
667
|
+
}
|
|
556
668
|
}
|
|
557
|
-
}
|
|
558
669
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
670
|
+
// Single-pass validation: collect all missing and extra handlers
|
|
671
|
+
const providedSlots = new Set(Object.keys(handlers));
|
|
672
|
+
const missing: string[] = [];
|
|
673
|
+
const extra: string[] = [];
|
|
563
674
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
675
|
+
for (const name of requiredSlots) {
|
|
676
|
+
if (!providedSlots.has(name)) {
|
|
677
|
+
missing.push(name);
|
|
678
|
+
}
|
|
567
679
|
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
680
|
+
for (const name of providedSlots) {
|
|
681
|
+
if (!requiredSlots.has(name)) {
|
|
682
|
+
extra.push(name);
|
|
683
|
+
}
|
|
572
684
|
}
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
// Report all validation errors at once
|
|
576
|
-
if (missing.length > 0 || extra.length > 0) {
|
|
577
|
-
throw new ProvisionValidationError({ missing, extra });
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
// Create new machine to preserve original for reuse with different providers
|
|
581
|
-
const result = new Machine<State, Event, R | R2, _SD, _ED, GD, EFD>(
|
|
582
|
-
this.initial,
|
|
583
|
-
this.stateSchema as Schema.Schema<State, unknown, never>,
|
|
584
|
-
this.eventSchema as Schema.Schema<Event, unknown, never>,
|
|
585
|
-
this._guardsSchema,
|
|
586
|
-
this._effectsSchema,
|
|
587
|
-
);
|
|
588
685
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
(result as any)._finalStates = new Set(this._finalStates);
|
|
594
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
595
|
-
(result as any)._spawnEffects = [...this._spawnEffects];
|
|
596
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
597
|
-
(result as any)._backgroundEffects = [...this._backgroundEffects];
|
|
686
|
+
// Report all validation errors at once
|
|
687
|
+
if (missing.length > 0 || extra.length > 0) {
|
|
688
|
+
throw new ProvisionValidationError({ missing, extra });
|
|
689
|
+
}
|
|
598
690
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
691
|
+
// Create new machine to preserve original for reuse with different providers
|
|
692
|
+
const result = new Machine<State, Event, R | R2, _SD, _ED, GD, EFD>(
|
|
693
|
+
this.initial,
|
|
694
|
+
this.stateSchema as Schema.Schema<State, unknown, never>,
|
|
695
|
+
this.eventSchema as Schema.Schema<Event, unknown, never>,
|
|
696
|
+
this._guardsSchema,
|
|
697
|
+
this._effectsSchema,
|
|
698
|
+
);
|
|
699
|
+
|
|
700
|
+
// Copy arrays/sets to avoid mutation bleed
|
|
701
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
702
|
+
(result as any)._transitions = [...this._transitions];
|
|
703
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
704
|
+
(result as any)._finalStates = new Set(this._finalStates);
|
|
705
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
706
|
+
(result as any)._spawnEffects = [...this._spawnEffects];
|
|
707
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
708
|
+
(result as any)._backgroundEffects = [...this._backgroundEffects];
|
|
709
|
+
|
|
710
|
+
// Register handlers from provided object
|
|
711
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
712
|
+
const anyHandlers = handlers as Record<string, any>;
|
|
713
|
+
if (this._guardsSchema !== undefined) {
|
|
714
|
+
for (const name of Object.keys(this._guardsSchema.definitions)) {
|
|
715
|
+
result._guardHandlers.set(name, anyHandlers[name]);
|
|
716
|
+
}
|
|
605
717
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
718
|
+
if (this._effectsSchema !== undefined) {
|
|
719
|
+
for (const name of Object.keys(this._effectsSchema.definitions)) {
|
|
720
|
+
result._effectHandlers.set(name, anyHandlers[name]);
|
|
721
|
+
}
|
|
610
722
|
}
|
|
611
|
-
}
|
|
612
723
|
|
|
613
|
-
|
|
724
|
+
return new BuiltMachine(result as unknown as Machine<State, Event, R | NormalizeR<R2>>);
|
|
725
|
+
}
|
|
726
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
727
|
+
return new BuiltMachine(this as any);
|
|
614
728
|
}
|
|
615
729
|
|
|
616
|
-
// ---- persist ----
|
|
730
|
+
// ---- persist (on Machine, for unbuilt usage in testing) ----
|
|
617
731
|
|
|
732
|
+
/** @internal Persist from raw Machine — prefer BuiltMachine.persist() */
|
|
618
733
|
persist(
|
|
619
734
|
config: PersistOptions,
|
|
620
735
|
): PersistentMachine<State & { readonly _tag: string }, Event & { readonly _tag: string }, R> {
|
|
@@ -627,25 +742,6 @@ export class Machine<
|
|
|
627
742
|
>;
|
|
628
743
|
}
|
|
629
744
|
|
|
630
|
-
/**
|
|
631
|
-
* Missing slot handlers (guards + effects).
|
|
632
|
-
* @internal Used by actor creation to fail fast.
|
|
633
|
-
*/
|
|
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;
|
|
647
|
-
}
|
|
648
|
-
|
|
649
745
|
// ---- Static factory ----
|
|
650
746
|
|
|
651
747
|
static make<
|
|
@@ -681,69 +777,56 @@ import { createActor } from "./actor.js";
|
|
|
681
777
|
|
|
682
778
|
/**
|
|
683
779
|
* Spawn an actor directly without ActorSystem ceremony.
|
|
780
|
+
* Accepts only `BuiltMachine` (call `.build()` first).
|
|
781
|
+
*
|
|
782
|
+
* **Single actor, no registry.** Caller manages lifetime via `actor.stop`.
|
|
783
|
+
* If a `Scope` exists in context, cleanup attaches automatically on scope close.
|
|
684
784
|
*
|
|
685
|
-
*
|
|
686
|
-
*
|
|
785
|
+
* For registry, lookup by ID, persistence, or multi-actor coordination,
|
|
786
|
+
* use `ActorSystemService` / `system.spawn` instead.
|
|
687
787
|
*
|
|
688
788
|
* @example
|
|
689
789
|
* ```ts
|
|
690
|
-
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
694
|
-
*
|
|
695
|
-
* });
|
|
790
|
+
* // Fire-and-forget — caller manages lifetime
|
|
791
|
+
* const actor = yield* Machine.spawn(machine.build());
|
|
792
|
+
* yield* actor.send(Event.Start);
|
|
793
|
+
* yield* actor.awaitFinal;
|
|
794
|
+
* yield* actor.stop;
|
|
696
795
|
*
|
|
697
|
-
*
|
|
796
|
+
* // Scope-aware — auto-cleans up on scope close
|
|
797
|
+
* yield* Effect.scoped(Effect.gen(function* () {
|
|
798
|
+
* const actor = yield* Machine.spawn(machine.build());
|
|
799
|
+
* yield* actor.send(Event.Start);
|
|
800
|
+
* // actor.stop called automatically when scope closes
|
|
801
|
+
* }));
|
|
698
802
|
* ```
|
|
699
803
|
*/
|
|
700
804
|
const spawnImpl = Effect.fn("effect-machine.spawn")(function* <
|
|
701
805
|
S extends { readonly _tag: string },
|
|
702
806
|
E extends { readonly _tag: string },
|
|
703
807
|
R,
|
|
704
|
-
|
|
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
|
-
) {
|
|
808
|
+
>(built: BuiltMachine<S, E, R>, id?: string) {
|
|
711
809
|
const actorId = id ?? `actor-${Math.random().toString(36).slice(2)}`;
|
|
712
|
-
const actor = yield* createActor(actorId,
|
|
810
|
+
const actor = yield* createActor(actorId, built._inner);
|
|
713
811
|
|
|
714
|
-
//
|
|
715
|
-
yield* Effect.
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
);
|
|
812
|
+
// If a scope exists in context, attach cleanup automatically
|
|
813
|
+
const maybeScope = yield* Effect.serviceOption(Scope.Scope);
|
|
814
|
+
if (Option.isSome(maybeScope)) {
|
|
815
|
+
yield* Scope.addFinalizer(maybeScope.value, actor.stop);
|
|
816
|
+
}
|
|
720
817
|
|
|
721
818
|
return actor;
|
|
722
819
|
});
|
|
723
820
|
|
|
724
821
|
export const spawn: {
|
|
725
|
-
<
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
>(
|
|
732
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Schema fields need wide acceptance
|
|
733
|
-
machine: Machine<S, E, R, any, any, GD, EFD>,
|
|
734
|
-
): Effect.Effect<ActorRef<S, E>, UnprovidedSlotsError, R | Scope.Scope>;
|
|
735
|
-
|
|
736
|
-
<
|
|
737
|
-
S extends { readonly _tag: string },
|
|
738
|
-
E extends { readonly _tag: string },
|
|
739
|
-
R,
|
|
740
|
-
GD extends GuardsDef,
|
|
741
|
-
EFD extends EffectsDef,
|
|
742
|
-
>(
|
|
743
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Schema fields need wide acceptance
|
|
744
|
-
machine: Machine<S, E, R, any, any, GD, EFD>,
|
|
822
|
+
<S extends { readonly _tag: string }, E extends { readonly _tag: string }, R>(
|
|
823
|
+
machine: BuiltMachine<S, E, R>,
|
|
824
|
+
): Effect.Effect<ActorRef<S, E>, never, R>;
|
|
825
|
+
|
|
826
|
+
<S extends { readonly _tag: string }, E extends { readonly _tag: string }, R>(
|
|
827
|
+
machine: BuiltMachine<S, E, R>,
|
|
745
828
|
id: string,
|
|
746
|
-
): Effect.Effect<ActorRef<S, E>,
|
|
829
|
+
): Effect.Effect<ActorRef<S, E>, never, R>;
|
|
747
830
|
} = spawnImpl;
|
|
748
831
|
|
|
749
832
|
// Transition lookup (introspection)
|
|
@@ -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
|
|
5
|
+
import type { DuplicateActorError } from "../errors.js";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Metadata for a persisted actor.
|
|
@@ -37,7 +37,7 @@ export interface RestoreResult<
|
|
|
37
37
|
*/
|
|
38
38
|
export interface RestoreFailure {
|
|
39
39
|
readonly id: string;
|
|
40
|
-
readonly error: PersistenceError | DuplicateActorError
|
|
40
|
+
readonly error: PersistenceError | DuplicateActorError;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/**
|
|
@@ -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,
|