effect-machine 0.3.1 → 0.4.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.
Files changed (62) hide show
  1. package/README.md +24 -0
  2. package/dist/_virtual/_rolldown/runtime.js +18 -0
  3. package/dist/actor.d.ts +256 -0
  4. package/dist/actor.js +402 -0
  5. package/dist/cluster/entity-machine.d.ts +90 -0
  6. package/dist/cluster/entity-machine.js +80 -0
  7. package/dist/cluster/index.d.ts +3 -0
  8. package/dist/cluster/index.js +4 -0
  9. package/dist/cluster/to-entity.d.ts +64 -0
  10. package/dist/cluster/to-entity.js +53 -0
  11. package/dist/errors.d.ts +61 -0
  12. package/dist/errors.js +38 -0
  13. package/dist/index.d.ts +13 -0
  14. package/dist/index.js +14 -0
  15. package/dist/inspection.d.ts +125 -0
  16. package/dist/inspection.js +50 -0
  17. package/dist/internal/brands.d.ts +40 -0
  18. package/dist/internal/brands.js +0 -0
  19. package/dist/internal/inspection.d.ts +11 -0
  20. package/dist/internal/inspection.js +15 -0
  21. package/dist/internal/transition.d.ts +160 -0
  22. package/dist/internal/transition.js +238 -0
  23. package/dist/internal/utils.d.ts +60 -0
  24. package/dist/internal/utils.js +46 -0
  25. package/dist/machine.d.ts +278 -0
  26. package/dist/machine.js +317 -0
  27. package/{src/persistence/adapter.ts → dist/persistence/adapter.d.ts} +40 -72
  28. package/dist/persistence/adapter.js +27 -0
  29. package/dist/persistence/adapters/in-memory.d.ts +32 -0
  30. package/dist/persistence/adapters/in-memory.js +176 -0
  31. package/dist/persistence/index.d.ts +5 -0
  32. package/dist/persistence/index.js +6 -0
  33. package/dist/persistence/persistent-actor.d.ts +50 -0
  34. package/dist/persistence/persistent-actor.js +358 -0
  35. package/{src/persistence/persistent-machine.ts → dist/persistence/persistent-machine.d.ts} +28 -54
  36. package/dist/persistence/persistent-machine.js +24 -0
  37. package/dist/schema.d.ts +141 -0
  38. package/dist/schema.js +165 -0
  39. package/dist/slot.d.ts +130 -0
  40. package/dist/slot.js +99 -0
  41. package/dist/testing.d.ts +142 -0
  42. package/dist/testing.js +138 -0
  43. package/package.json +28 -14
  44. package/src/actor.ts +0 -1058
  45. package/src/cluster/entity-machine.ts +0 -201
  46. package/src/cluster/index.ts +0 -43
  47. package/src/cluster/to-entity.ts +0 -99
  48. package/src/errors.ts +0 -64
  49. package/src/index.ts +0 -105
  50. package/src/inspection.ts +0 -178
  51. package/src/internal/brands.ts +0 -51
  52. package/src/internal/inspection.ts +0 -18
  53. package/src/internal/transition.ts +0 -489
  54. package/src/internal/utils.ts +0 -80
  55. package/src/machine.ts +0 -836
  56. package/src/persistence/adapters/in-memory.ts +0 -294
  57. package/src/persistence/index.ts +0 -24
  58. package/src/persistence/persistent-actor.ts +0 -791
  59. package/src/schema.ts +0 -362
  60. package/src/slot.ts +0 -281
  61. package/src/testing.ts +0 -284
  62. package/tsconfig.json +0 -65
package/src/machine.ts DELETED
@@ -1,836 +0,0 @@
1
- /**
2
- * Machine namespace - fluent builder API for state machines.
3
- *
4
- * @example
5
- * ```ts
6
- * import { Machine, State, Event, Slot } from "effect-machine"
7
- *
8
- * const MyState = State({ Idle: {}, Running: { count: Schema.Number } })
9
- * const MyEvent = Event({ Start: {}, Stop: {} })
10
- *
11
- * const MyGuards = Slot.Guards({
12
- * canStart: { threshold: Schema.Number },
13
- * })
14
- *
15
- * const MyEffects = Slot.Effects({
16
- * notify: { message: Schema.String },
17
- * })
18
- *
19
- * const machine = Machine.make({
20
- * state: MyState,
21
- * event: MyEvent,
22
- * guards: MyGuards,
23
- * effects: MyEffects,
24
- * initial: MyState.Idle,
25
- * })
26
- * .on(MyState.Idle, MyEvent.Start, ({ state, guards, effects }) =>
27
- * Effect.gen(function* () {
28
- * if (yield* guards.canStart({ threshold: 5 })) {
29
- * yield* effects.notify({ message: "Starting!" })
30
- * return MyState.Running({ count: 0 })
31
- * }
32
- * return state
33
- * })
34
- * )
35
- * .on(MyState.Running, MyEvent.Stop, () => MyState.Idle)
36
- * .final(MyState.Idle)
37
- * .build({
38
- * canStart: ({ threshold }) => Effect.succeed(threshold > 0),
39
- * notify: ({ message }) => Effect.log(message),
40
- * })
41
- * ```
42
- *
43
- * @module
44
- */
45
- import type { Schema, Schedule, Context } from "effect";
46
- import { Cause, Effect, Exit, Option, Scope } from "effect";
47
-
48
- import type { TransitionResult } from "./internal/utils.js";
49
- import { getTag } from "./internal/utils.js";
50
- import type { TaggedOrConstructor, BrandedState, BrandedEvent } from "./internal/brands.js";
51
- import type { MachineStateSchema, MachineEventSchema, VariantsUnion } from "./schema.js";
52
- import type { PersistentMachine, WithPersistenceConfig } from "./persistence/persistent-machine.js";
53
- import { persist as persistImpl } from "./persistence/persistent-machine.js";
54
- import { SlotProvisionError, ProvisionValidationError } from "./errors.js";
55
- import { invalidateIndex } from "./internal/transition.js";
56
- import type {
57
- GuardsSchema,
58
- EffectsSchema,
59
- GuardsDef,
60
- EffectsDef,
61
- GuardSlots,
62
- EffectSlots,
63
- GuardHandlers,
64
- EffectHandlers as SlotEffectHandlers,
65
- MachineContext,
66
- } from "./slot.js";
67
- import { MachineContextTag } from "./slot.js";
68
-
69
- // ============================================================================
70
- // Core types
71
- // ============================================================================
72
-
73
- /**
74
- * Self reference for sending events back to the machine
75
- */
76
- export interface MachineRef<Event> {
77
- readonly send: (event: Event) => Effect.Effect<void>;
78
- }
79
-
80
- /**
81
- * Handler context passed to transition handlers
82
- */
83
- export interface HandlerContext<State, Event, GD extends GuardsDef, ED extends EffectsDef> {
84
- readonly state: State;
85
- readonly event: Event;
86
- readonly guards: GuardSlots<GD>;
87
- readonly effects: EffectSlots<ED>;
88
- }
89
-
90
- /**
91
- * Handler context passed to state effect handlers (onEnter, spawn, background)
92
- */
93
- export interface StateHandlerContext<State, Event, ED extends EffectsDef> {
94
- readonly state: State;
95
- readonly event: Event;
96
- readonly self: MachineRef<Event>;
97
- readonly effects: EffectSlots<ED>;
98
- }
99
-
100
- /**
101
- * Transition handler function
102
- */
103
- export type TransitionHandler<S, E, NewState, GD extends GuardsDef, ED extends EffectsDef, R> = (
104
- ctx: HandlerContext<S, E, GD, ED>,
105
- ) => TransitionResult<NewState, R>;
106
-
107
- /**
108
- * State effect handler function
109
- */
110
- export type StateEffectHandler<S, E, ED extends EffectsDef, R> = (
111
- ctx: StateHandlerContext<S, E, ED>,
112
- ) => Effect.Effect<void, never, R>;
113
-
114
- /**
115
- * Transition definition
116
- */
117
- export interface Transition<State, Event, GD extends GuardsDef, ED extends EffectsDef, R> {
118
- readonly stateTag: string;
119
- readonly eventTag: string;
120
- readonly handler: TransitionHandler<State, Event, State, GD, ED, R>;
121
- readonly reenter?: boolean;
122
- }
123
-
124
- /**
125
- * Spawn effect - state-scoped forked effect
126
- */
127
- export interface SpawnEffect<State, Event, ED extends EffectsDef, R> {
128
- readonly stateTag: string;
129
- readonly handler: StateEffectHandler<State, Event, ED, R>;
130
- }
131
-
132
- /**
133
- * Background effect - runs for entire machine lifetime
134
- */
135
- export interface BackgroundEffect<State, Event, ED extends EffectsDef, R> {
136
- readonly handler: StateEffectHandler<State, Event, ED, R>;
137
- }
138
-
139
- // ============================================================================
140
- // Options types
141
- // ============================================================================
142
-
143
- /** Options for `persist` */
144
- export interface PersistOptions {
145
- readonly snapshotSchedule: Schedule.Schedule<unknown, { readonly _tag: string }>;
146
- readonly journalEvents: boolean;
147
- readonly machineType?: string;
148
- }
149
-
150
- // ============================================================================
151
- // Internal helpers
152
- // ============================================================================
153
-
154
- type IsAny<T> = 0 extends 1 & T ? true : false;
155
- type IsUnknown<T> = unknown extends T ? ([T] extends [unknown] ? true : false) : false;
156
- type NormalizeR<T> = IsAny<T> extends true ? T : IsUnknown<T> extends true ? never : T;
157
-
158
- // ============================================================================
159
- // MakeConfig
160
- // ============================================================================
161
-
162
- export interface MakeConfig<
163
- SD extends Record<string, Schema.Struct.Fields>,
164
- ED extends Record<string, Schema.Struct.Fields>,
165
- S extends BrandedState,
166
- E extends BrandedEvent,
167
- GD extends GuardsDef,
168
- EFD extends EffectsDef,
169
- > {
170
- readonly state: MachineStateSchema<SD> & { Type: S };
171
- readonly event: MachineEventSchema<ED> & { Type: E };
172
- readonly guards?: GuardsSchema<GD>;
173
- readonly effects?: EffectsSchema<EFD>;
174
- readonly initial: S;
175
- }
176
-
177
- // ============================================================================
178
- // Provide types
179
- // ============================================================================
180
-
181
- /** Check if a GuardsDef has any actual keys */
182
- type HasGuardKeys<GD extends GuardsDef> = [keyof GD] extends [never]
183
- ? false
184
- : GD extends Record<string, never>
185
- ? false
186
- : true;
187
-
188
- /** Check if an EffectsDef has any actual keys */
189
- type HasEffectKeys<EFD extends EffectsDef> = [keyof EFD] extends [never]
190
- ? false
191
- : EFD extends Record<string, never>
192
- ? false
193
- : true;
194
-
195
- /** Context type passed to guard/effect handlers */
196
- export type SlotContext<State, Event> = MachineContext<State, Event, MachineRef<Event>>;
197
-
198
- /** Combined handlers for build() - guards and effects only */
199
- export type ProvideHandlers<
200
- State,
201
- Event,
202
- GD extends GuardsDef,
203
- EFD extends EffectsDef,
204
- R,
205
- > = (HasGuardKeys<GD> extends true ? GuardHandlers<GD, SlotContext<State, Event>, R> : object) &
206
- (HasEffectKeys<EFD> extends true
207
- ? SlotEffectHandlers<EFD, SlotContext<State, Event>, R>
208
- : object);
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
-
247
- // ============================================================================
248
- // Machine class
249
- // ============================================================================
250
-
251
- /**
252
- * Machine definition with fluent builder API.
253
- *
254
- * Type parameters:
255
- * - `State`: The state union type
256
- * - `Event`: The event union type
257
- * - `R`: Effect requirements
258
- * - `_SD`: State schema definition (for compile-time validation)
259
- * - `_ED`: Event schema definition (for compile-time validation)
260
- * - `GD`: Guard definitions
261
- * - `EFD`: Effect definitions
262
- */
263
- export class Machine<
264
- State,
265
- Event,
266
- R = never,
267
- _SD extends Record<string, Schema.Struct.Fields> = Record<string, Schema.Struct.Fields>,
268
- _ED extends Record<string, Schema.Struct.Fields> = Record<string, Schema.Struct.Fields>,
269
- GD extends GuardsDef = Record<string, never>,
270
- EFD extends EffectsDef = Record<string, never>,
271
- > {
272
- readonly initial: State;
273
- /** @internal */ readonly _transitions: Array<Transition<State, Event, GD, EFD, R>>;
274
- /** @internal */ readonly _spawnEffects: Array<SpawnEffect<State, Event, EFD, R>>;
275
- /** @internal */ readonly _backgroundEffects: Array<BackgroundEffect<State, Event, EFD, R>>;
276
- /** @internal */ readonly _finalStates: Set<string>;
277
- /** @internal */ readonly _guardsSchema?: GuardsSchema<GD>;
278
- /** @internal */ readonly _effectsSchema?: EffectsSchema<EFD>;
279
- /** @internal */ readonly _guardHandlers: Map<
280
- string,
281
- (params: unknown, ctx: SlotContext<State, Event>) => boolean | Effect.Effect<boolean, never, R>
282
- >;
283
- /** @internal */ readonly _effectHandlers: Map<
284
- string,
285
- (params: unknown, ctx: SlotContext<State, Event>) => Effect.Effect<void, never, R>
286
- >;
287
- /** @internal */ readonly _slots: {
288
- guards: GuardSlots<GD>;
289
- effects: EffectSlots<EFD>;
290
- };
291
- readonly stateSchema?: Schema.Schema<State, unknown, never>;
292
- readonly eventSchema?: Schema.Schema<Event, unknown, never>;
293
-
294
- /**
295
- * Context tag for accessing machine state/event/self in slot handlers.
296
- * Uses shared module-level tag for all machines.
297
- */
298
- readonly Context: Context.Tag<
299
- MachineContext<State, Event, MachineRef<Event>>,
300
- MachineContext<State, Event, MachineRef<Event>>
301
- > = MachineContextTag as Context.Tag<
302
- MachineContext<State, Event, MachineRef<Event>>,
303
- MachineContext<State, Event, MachineRef<Event>>
304
- >;
305
-
306
- // Public readonly views
307
- get transitions(): ReadonlyArray<Transition<State, Event, GD, EFD, R>> {
308
- return this._transitions;
309
- }
310
- get spawnEffects(): ReadonlyArray<SpawnEffect<State, Event, EFD, R>> {
311
- return this._spawnEffects;
312
- }
313
- get backgroundEffects(): ReadonlyArray<BackgroundEffect<State, Event, EFD, R>> {
314
- return this._backgroundEffects;
315
- }
316
- get finalStates(): ReadonlySet<string> {
317
- return this._finalStates;
318
- }
319
- get guardsSchema(): GuardsSchema<GD> | undefined {
320
- return this._guardsSchema;
321
- }
322
- get effectsSchema(): EffectsSchema<EFD> | undefined {
323
- return this._effectsSchema;
324
- }
325
-
326
- /** @internal */
327
- constructor(
328
- initial: State,
329
- stateSchema?: Schema.Schema<State, unknown, never>,
330
- eventSchema?: Schema.Schema<Event, unknown, never>,
331
- guardsSchema?: GuardsSchema<GD>,
332
- effectsSchema?: EffectsSchema<EFD>,
333
- ) {
334
- this.initial = initial;
335
- this._transitions = [];
336
- this._spawnEffects = [];
337
- this._backgroundEffects = [];
338
- this._finalStates = new Set();
339
- this._guardsSchema = guardsSchema;
340
- this._effectsSchema = effectsSchema;
341
- this._guardHandlers = new Map();
342
- this._effectHandlers = new Map();
343
- this.stateSchema = stateSchema;
344
- this.eventSchema = eventSchema;
345
-
346
- const guardSlots =
347
- this._guardsSchema !== undefined
348
- ? this._guardsSchema._createSlots((name: string, params: unknown) =>
349
- Effect.flatMap(Effect.serviceOptional(this.Context).pipe(Effect.orDie), (ctx) => {
350
- const handler = this._guardHandlers.get(name);
351
- if (handler === undefined) {
352
- return Effect.die(new SlotProvisionError({ slotName: name, slotType: "guard" }));
353
- }
354
- const result = handler(params, ctx);
355
- const normalized = typeof result === "boolean" ? Effect.succeed(result) : result;
356
- return normalized as Effect.Effect<boolean, never, never>;
357
- }),
358
- )
359
- : ({} as GuardSlots<GD>);
360
-
361
- const effectSlots =
362
- this._effectsSchema !== undefined
363
- ? this._effectsSchema._createSlots((name: string, params: unknown) =>
364
- Effect.flatMap(Effect.serviceOptional(this.Context).pipe(Effect.orDie), (ctx) => {
365
- const handler = this._effectHandlers.get(name);
366
- if (handler === undefined) {
367
- return Effect.die(new SlotProvisionError({ slotName: name, slotType: "effect" }));
368
- }
369
- return handler(params, ctx) as Effect.Effect<void, never, never>;
370
- }),
371
- )
372
- : ({} as EffectSlots<EFD>);
373
-
374
- this._slots = { guards: guardSlots, effects: effectSlots };
375
- }
376
-
377
- // ---- on ----
378
-
379
- /** Register transition for a single state */
380
- on<
381
- NS extends VariantsUnion<_SD> & BrandedState,
382
- NE extends VariantsUnion<_ED> & BrandedEvent,
383
- RS extends VariantsUnion<_SD> & BrandedState,
384
- >(
385
- state: TaggedOrConstructor<NS>,
386
- event: TaggedOrConstructor<NE>,
387
- handler: TransitionHandler<NS, NE, RS, GD, EFD, never>,
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;
413
- }
414
-
415
- // ---- reenter ----
416
-
417
- /**
418
- * Like `on()`, but forces onEnter/spawn to run even when transitioning to the same state tag.
419
- * Use this to restart timers, re-run spawned effects, or reset state-scoped effects.
420
- */
421
- /** Single state */
422
- reenter<
423
- NS extends VariantsUnion<_SD> & BrandedState,
424
- NE extends VariantsUnion<_ED> & BrandedEvent,
425
- RS extends VariantsUnion<_SD> & BrandedState,
426
- >(
427
- state: TaggedOrConstructor<NS>,
428
- event: TaggedOrConstructor<NE>,
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>,
471
- ): Machine<State, Event, R, _SD, _ED, GD, EFD> {
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;
483
- }
484
-
485
- /** @internal */
486
- private addTransition<NS extends BrandedState, NE extends BrandedEvent>(
487
- state: TaggedOrConstructor<NS>,
488
- event: TaggedOrConstructor<NE>,
489
- handler: TransitionHandler<NS, NE, BrandedState, GD, EFD, never>,
490
- reenter: boolean,
491
- ): Machine<State, Event, R, _SD, _ED, GD, EFD> {
492
- const stateTag = getTag(state);
493
- const eventTag = getTag(event);
494
-
495
- const transition: Transition<State, Event, GD, EFD, R> = {
496
- stateTag,
497
- eventTag,
498
- handler: handler as unknown as Transition<State, Event, GD, EFD, R>["handler"],
499
- reenter,
500
- };
501
-
502
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
503
- (this._transitions as any[]).push(transition);
504
- invalidateIndex(this);
505
-
506
- return this;
507
- }
508
-
509
- // ---- spawn ----
510
-
511
- /**
512
- * State-scoped effect that is forked on state entry and automatically cancelled on state exit.
513
- * Use effect slots defined via `Slot.Effects` for the actual work.
514
- *
515
- * @example
516
- * ```ts
517
- * const MyEffects = Slot.Effects({
518
- * fetchData: { url: Schema.String },
519
- * });
520
- *
521
- * machine
522
- * .spawn(State.Loading, ({ effects, state }) => effects.fetchData({ url: state.url }))
523
- * .build({
524
- * fetchData: ({ url }, { self }) =>
525
- * Effect.gen(function* () {
526
- * yield* Effect.addFinalizer(() => Effect.log("Leaving Loading"));
527
- * const data = yield* Http.get(url);
528
- * yield* self.send(Event.Loaded({ data }));
529
- * }),
530
- * });
531
- * ```
532
- */
533
- spawn<NS extends VariantsUnion<_SD> & BrandedState>(
534
- state: TaggedOrConstructor<NS>,
535
- handler: StateEffectHandler<NS, VariantsUnion<_ED> & BrandedEvent, EFD, Scope.Scope>,
536
- ): Machine<State, Event, R, _SD, _ED, GD, EFD> {
537
- const stateTag = getTag(state);
538
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
539
- (this._spawnEffects as any[]).push({
540
- stateTag,
541
- handler: handler as unknown as SpawnEffect<State, Event, EFD, R>["handler"],
542
- });
543
- invalidateIndex(this);
544
- return this;
545
- }
546
-
547
- // ---- task ----
548
-
549
- /**
550
- * State-scoped task that runs on entry and sends success/failure events.
551
- * Interrupts do not emit failure events.
552
- */
553
- task<
554
- NS extends VariantsUnion<_SD> & BrandedState,
555
- A,
556
- E1,
557
- ES extends VariantsUnion<_ED> & BrandedEvent,
558
- EF extends VariantsUnion<_ED> & BrandedEvent,
559
- >(
560
- state: TaggedOrConstructor<NS>,
561
- run: (
562
- ctx: StateHandlerContext<NS, VariantsUnion<_ED> & BrandedEvent, EFD>,
563
- ) => Effect.Effect<A, E1, Scope.Scope>,
564
- options: {
565
- readonly onSuccess: (
566
- value: A,
567
- ctx: StateHandlerContext<NS, VariantsUnion<_ED> & BrandedEvent, EFD>,
568
- ) => ES;
569
- readonly onFailure?: (
570
- cause: Cause.Cause<E1>,
571
- ctx: StateHandlerContext<NS, VariantsUnion<_ED> & BrandedEvent, EFD>,
572
- ) => EF;
573
- },
574
- ): Machine<State, Event, R, _SD, _ED, GD, EFD> {
575
- const handler = Effect.fn("effect-machine.task")(function* (
576
- ctx: StateHandlerContext<NS, VariantsUnion<_ED> & BrandedEvent, EFD>,
577
- ) {
578
- const exit = yield* Effect.exit(run(ctx));
579
- if (Exit.isSuccess(exit)) {
580
- yield* ctx.self.send(options.onSuccess(exit.value, ctx));
581
- yield* Effect.yieldNow();
582
- return;
583
- }
584
-
585
- const cause = exit.cause;
586
- if (Cause.isInterruptedOnly(cause)) {
587
- return;
588
- }
589
- if (options.onFailure !== undefined) {
590
- yield* ctx.self.send(options.onFailure(cause, ctx));
591
- yield* Effect.yieldNow();
592
- return;
593
- }
594
- return yield* Effect.failCause(cause).pipe(Effect.orDie);
595
- });
596
-
597
- return this.spawn(state, handler);
598
- }
599
-
600
- // ---- background ----
601
-
602
- /**
603
- * Machine-lifetime effect that is forked on actor spawn and runs until the actor stops.
604
- * Use effect slots defined via `Slot.Effects` for the actual work.
605
- *
606
- * @example
607
- * ```ts
608
- * const MyEffects = Slot.Effects({
609
- * heartbeat: {},
610
- * });
611
- *
612
- * machine
613
- * .background(({ effects }) => effects.heartbeat())
614
- * .build({
615
- * heartbeat: (_, { self }) =>
616
- * Effect.forever(
617
- * Effect.sleep("30 seconds").pipe(Effect.andThen(self.send(Event.Ping)))
618
- * ),
619
- * });
620
- * ```
621
- */
622
- background(
623
- handler: StateEffectHandler<State, Event, EFD, Scope.Scope>,
624
- ): Machine<State, Event, R, _SD, _ED, GD, EFD> {
625
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
626
- (this._backgroundEffects as any[]).push({
627
- handler: handler as unknown as BackgroundEffect<State, Event, EFD, R>["handler"],
628
- });
629
- return this;
630
- }
631
-
632
- // ---- final ----
633
-
634
- final<NS extends VariantsUnion<_SD> & BrandedState>(
635
- state: TaggedOrConstructor<NS>,
636
- ): Machine<State, Event, R, _SD, _ED, GD, EFD> {
637
- const stateTag = getTag(state);
638
- this._finalStates.add(stateTag);
639
- return this;
640
- }
641
-
642
- // ---- build ----
643
-
644
- /**
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.
649
- */
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
- }
663
- }
664
- if (this._effectsSchema !== undefined) {
665
- for (const name of Object.keys(this._effectsSchema.definitions)) {
666
- requiredSlots.add(name);
667
- }
668
- }
669
-
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[] = [];
674
-
675
- for (const name of requiredSlots) {
676
- if (!providedSlots.has(name)) {
677
- missing.push(name);
678
- }
679
- }
680
- for (const name of providedSlots) {
681
- if (!requiredSlots.has(name)) {
682
- extra.push(name);
683
- }
684
- }
685
-
686
- // Report all validation errors at once
687
- if (missing.length > 0 || extra.length > 0) {
688
- throw new ProvisionValidationError({ missing, extra });
689
- }
690
-
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
- }
717
- }
718
- if (this._effectsSchema !== undefined) {
719
- for (const name of Object.keys(this._effectsSchema.definitions)) {
720
- result._effectHandlers.set(name, anyHandlers[name]);
721
- }
722
- }
723
-
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);
728
- }
729
-
730
- // ---- persist (on Machine, for unbuilt usage in testing) ----
731
-
732
- /** @internal Persist from raw Machine — prefer BuiltMachine.persist() */
733
- persist(
734
- config: PersistOptions,
735
- ): PersistentMachine<State & { readonly _tag: string }, Event & { readonly _tag: string }, R> {
736
- return persistImpl(config as WithPersistenceConfig)(
737
- this as unknown as Machine<BrandedState, BrandedEvent, R>,
738
- ) as unknown as PersistentMachine<
739
- State & { readonly _tag: string },
740
- Event & { readonly _tag: string },
741
- R
742
- >;
743
- }
744
-
745
- // ---- Static factory ----
746
-
747
- static make<
748
- SD extends Record<string, Schema.Struct.Fields>,
749
- ED extends Record<string, Schema.Struct.Fields>,
750
- S extends BrandedState,
751
- E extends BrandedEvent,
752
- GD extends GuardsDef = Record<string, never>,
753
- EFD extends EffectsDef = Record<string, never>,
754
- >(config: MakeConfig<SD, ED, S, E, GD, EFD>): Machine<S, E, never, SD, ED, GD, EFD> {
755
- return new Machine<S, E, never, SD, ED, GD, EFD>(
756
- config.initial,
757
- config.state as unknown as Schema.Schema<S, unknown, never>,
758
- config.event as unknown as Schema.Schema<E, unknown, never>,
759
- config.guards as GuardsSchema<GD> | undefined,
760
- config.effects as EffectsSchema<EFD> | undefined,
761
- );
762
- }
763
- }
764
-
765
- // ============================================================================
766
- // make function (alias for Machine.make)
767
- // ============================================================================
768
-
769
- export const make = Machine.make;
770
-
771
- // ============================================================================
772
- // spawn function - simple actor creation without ActorSystem
773
- // ============================================================================
774
-
775
- import type { ActorRef } from "./actor.js";
776
- import { createActor } from "./actor.js";
777
-
778
- /**
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.
784
- *
785
- * For registry, lookup by ID, persistence, or multi-actor coordination,
786
- * use `ActorSystemService` / `system.spawn` instead.
787
- *
788
- * @example
789
- * ```ts
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;
795
- *
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
- * }));
802
- * ```
803
- */
804
- const spawnImpl = Effect.fn("effect-machine.spawn")(function* <
805
- S extends { readonly _tag: string },
806
- E extends { readonly _tag: string },
807
- R,
808
- >(built: BuiltMachine<S, E, R>, id?: string) {
809
- const actorId = id ?? `actor-${Math.random().toString(36).slice(2)}`;
810
- const actor = yield* createActor(actorId, built._inner);
811
-
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
- }
817
-
818
- return actor;
819
- });
820
-
821
- export const spawn: {
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>,
828
- id: string,
829
- ): Effect.Effect<ActorRef<S, E>, never, R>;
830
- } = spawnImpl;
831
-
832
- // Transition lookup (introspection)
833
- export { findTransitions } from "./internal/transition.js";
834
-
835
- // Persistence types
836
- export type { PersistenceConfig, PersistentMachine } from "./persistence/index.js";